Skip to content

feat(cli): read a package's TypeScript project the way tsc reads it - #7681

Merged
josephfarina merged 6 commits into
mainfrom
josephfarina/doctor-typescript-project
Oct 19, 2026
Merged

josephfarina merged 6 commits into
mainfrom
josephfarina/doctor-typescript-project

Conversation

@josephfarina

@josephfarina josephfarina commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

User need

The documented-props comparison (#7663) reads a package's props with the package's own TypeScript, so that it agrees with tsc. That holds only if it reads the package's TypeScript project the way tsc does. Read the wrong config, or none, and it compares the doc with the wrong types: a path alias reads as a missing package, a monorepo's settings are dropped, or an app's config above an installed package remaps the package's own imports to the app's files.

Problem and solution fit

This PR adds that reading as foundation groundwork, and #7663, stacked on it, uses it. They were one PR until #7663 passed 2,000 lines, and it split along this seam.

  • resolveProjectTypeScript finds the project's own typescript. When the installed TypeScript has no JavaScript compiler API to read types with (TypeScript 7), it names that version.
  • readPackageTsconfig reads the package's TypeScript project as tsc reads it:
    • The config is the nearest tsconfig.json: the package's own, or a monorepo root's for a local package. An app's config above an installed package (one below node_modules) is not the package's, so it isn't read.
    • A solution-style config ("files": [] with references) is followed through its references, at any depth and each config once, to the config that includes the sources. When none does, a reference that's missing or doesn't parse is the project's error, as tsc reports it.
    • The declaration files the config includes, included files that declare globals or augment a module, and included scripts (a .ts file TypeScript's own module detection reads as no module) join the files to read, so ambient types resolve as tsc resolves them.
    • Without a config, defaults read TSX the way bundlers do. A config with errors is reported, never read as defaults. That includes a files entry tsc can't find (TS6053, or TS6231 for a folder). feat(cli): compare documented component props with the TypeScript source #7663 then lists every table with the error.
  • unresolvedReference finds the first reference the compiler can't resolve, from given files through the files they import, as far as the caller says. A reference fails when the program finds no module, no type package or no file for it, when a module doesn't export a name imported from it, or, in the package's own files, when the compiler can only give a type its error type: a name or value declared nowhere, a property that doesn't exist, or such a type in a JavaScript file's JSDoc, which keyof, a key or an alias would otherwise hide. A reference that doesn't resolve, anywhere the walk goes, is named before such a type, since it names what to install. A side-effect import counts where the compiler checks one (the default from TypeScript 6), and a module found without types resolves.
  • unresolvedGlobals walks the program's global roots: the declaration files the package's own config includes, wherever they sit, its own type packages, and each type package the config names, which fails when it can't be found. For a config from above the package (a monorepo's root), only the package's own declaration files start the walk. A dependency's declarations aren't followed: tsc skips their errors (skipLibCheck), and they reach a component only through the globals they declare.

Current specification

Supports architecture:cli-surface INV33, "Documented component props agree with the source" (AST-039 FR12, which now points to it; DEC-5), with #7663, #7676, #7684 and #7667. Its evidence row names the prop-drift, type-text, doctor-integration and core-prop-docs tests. project-typescript.mjs also serves INV34's doc type check (#7664).

Public delta and caller burden

Design and alternatives

  • TypeScript's own config reading. The config search, parse and project references are the compiler's (findConfigFile, parseJsonConfigFileContent, resolveProjectReferencePath), so the reading follows tsc as it changes, with no second parser.
  • Not an app's config for an installed package. A published package rarely ships its tsconfig.json. Reading the app's config above it would apply the app's paths to the package's own imports, such as remapping @/base to the app's file.
  • Declaration files only, not every file. Loading every file the config lists would make Core's comparison about 80% slower, with no change in what it reads.
  • Rejected: a bundled TypeScript, and reading every config up the tree.

Evidence

  • foundation/discovery/unresolved-references.test.mjs (3 tests):
    • each reference that fails: a module, a name its module doesn't export (imported or re-exported), a missing /// <reference types> or /// <reference path>, a side-effect import under TypeScript 6 but not with noUncheckedSideEffectImports: false, and a type the compiler can only give its error type, reached through keyof, a template literal key or typeof (a name declared nowhere, a value declared nowhere behind a binding, a property that doesn't exist, a name in a JavaScript file's JSDoc, named by its innermost part), only in the files the caller checks, and never from a .ts file's JSDoc; and a value the compiler can't type that a typeof reads, spread into an object, in the same file or another of the package's, or returned by an own function it calls, passes on or constructs (a class field), or held by a class member a type indexes (a field, a getter, a parameter property, or a member of a base built by a call, also through an interface that extends the class), or a class's base built by a call; and what resolves: a module found without types, the JSX runtime import the compiler adds, as const, globalThis, typeof undefined, typeof arguments, a class's value base, an instantiation expression, typeof of a name from a module found without types, a value read with no error tsc reports (under @ts-expect-error, also on a later line of a read, in a @ts-nocheck file, or an index that misses without noImplicitAny), an error in a function whose return type is written, that has overloads, or that is called only for its effect, a value cast to a written type, a broken member of a class that no type indexes (a component's state, or a field of the package's own base component), and names that resolve; a dependency's own missing import found only where the walk follows it;
    • the global roots: a declaration file outside the package that its own config includes, the package's own type package named in types, a type name declared nowhere in the package's own declaration file, and a type package the config names that can't be found; a module declaration file no component imports, not a root, nor one that re-exports a module under one name (export * as), while one that augments a module, makes a side-effect import, imports no names, re-exports everything or declares a UMD global is; and not followed: a dependency's type package, named, under "*", or referenced from the package's own declaration file;
    • for a monorepo's root config, only the package's own declaration files: a broken one elsewhere in the repository is not read, and one in the package is.
  • foundation/discovery/package-tsconfig.test.mjs (6 tests):
    • defaults without a config; the package's own options, with the declaration files, global declarations and scripts it includes (one whose namespace has an export inside included), and not its modules;
    • a solution-style root followed through six levels of references, each level also referring back to the root;
    • a monorepo root's config read for a local package, and an app's config not read for an installed package;
    • a config with errors reported: a files entry that doesn't exist, an extends that isn't installed, a config that doesn't parse, and a solution's reference that is missing or doesn't parse; while a file the include matched that is gone by the time it is read is no error;
    • each files entry read as tsc finds it: ${configDir}, in the package's config and in a shared base it extends, and an entry without its extension are read (an ambient script listed that way is loaded), while a missing file (TS6053) and a folder (TS6231) are errors.
  • feat(cli): compare documented component props with the TypeScript source #7663's tests cover each of these through the comparison, and cover the missing and TypeScript 7 cases of resolveProjectTypeScript.

Since the first automated review

Each fix has a test that failed first:

  • A files entry naming a file that doesn't exist crashed the reading with ENOENT, since the config parses without checking files. It is reported as a config error now, as tsc reports it (TS6053), and an included file that can't be read is never taken as ambient.
  • References were followed only four levels deep, though the description says any depth. The walk now follows every reference, any number of levels down, reading each config once, so a cycle ends.
  • An included script (a .ts file with no import or export), whose declarations are global, wasn't loaded. It is loaded now, like a declaration file. On Core and Lab the files loaded are unchanged.
  • project-typescript.mjs is unchanged, and still byte-identical to feat(cli): check each authored doc against its published authoring type #7664's.

After the next review

  • Any file the config's include matched that was gone by the time it was read, such as one another process wrote and removed, made the config read as having an error. Every prop table was then listed as not compared. A test run showed it: a package created inside a repository read the repository's config while a parallel test removed a file that config's include matched. Only a files entry that doesn't exist is an error now, as tsc reports it (TS6053). A test reproduces the race and failed before the fix.

After the review that came next

  • The files check resolved each entry's text itself, so a config tsc -p compiles cleanly was reported as an error. That happened when an entry used ${configDir}, or left off its extension. TypeScript now reads each entry itself. It substitutes ${configDir} on a version that has it, resolves the entry against the config that writes it, and tries the TypeScript extensions for an entry without one. Only an entry TypeScript can't find is an error, with the message tsc gives: TS6053 for a missing file, TS6231 for a folder. The files it finds are the ones read, so an ambient script listed without its extension is loaded.
  • A test covers ${configDir} in the package's config and in a shared base it extends, entries without their extension, a missing file and a folder; it failed before the fix. On TypeScript 6, 5.0 and 4.8 each case reads as that version's tsc reads it: 5.0 and 4.8 have no ${configDir}, and their tsc reports such an entry as not found, as the check does. Core and Lab are unchanged.

After the most recent review

  • Finding the references the compiler can't resolve moved here from feat(cli): compare documented component props with the TypeScript source #7663, which reached 1,992 lines. It's a reading of the package's TypeScript project, as this PR's other groundwork is. feat(cli): compare documented component props with the TypeScript source #7663 uses it to list a component as not compared when a dependency it reads isn't installed. It moved with two fixes from an independent audit, each with a test that failed before it:
    • A declaration file the package's own config includes from outside the package's folder, or through a symlinked folder, didn't start the walk, because only files inside the folder did. A missing type package it references then left the props a guess. Every declaration file the package's own config includes starts the walk now. Only a config from above the package, such as a monorepo's root config, is limited to the package's own files.
    • The package's own type package, from a typeRoots folder and named in types, wasn't walked, since found type packages were skipped as dependencies. A type package that isn't a dependency's (one below node_modules) is walked now.
  • A type name declared nowhere, reached only through keyof, a Record's keys, an alias or a template literal key, gave the compiler's error type no part to show it: keyof of it is string | number | symbol. The walk now treats such a name in the package's own files as a reference that doesn't resolve, the counterpart of a missing import. as const, which TypeScript writes as a type reference named const, isn't one, and neither are globalThis, undefined and arguments, which the compiler makes itself without a declaration; only the compiler's own stand-in for a missing name counts.
  • A solution-style config whose referenced config is missing or doesn't parse was read with the root's options and no error. When no config includes the sources, that reference's error is the project's error now.
  • A global script that declares a namespace with an export inside it was taken for a module, so its types weren't loaded. TypeScript's own module detection decides now. It parses each included .ts file that writes import or export. On Core's 1,292 files that costs about 1 s on a shared machine, against a comparison that takes about 5 s.
  • Each has a test that failed first. project-typescript.mjs is unchanged, and still byte-identical to feat(cli): check each authored doc against its published authoring type #7664's.

After the review that followed

  • A type the compiler can only give its error type, but whose root isn't a name, got past the walk: a value declared nowhere behind a binding (keyof typeof d after const d = missing), a property that doesn't exist (keyof Theme['sizes']), and a name declared nowhere in a JavaScript component's JSDoc, which tsc doesn't report there. The walk now checks each type reference, type query, indexed access, import type and heritage type in the package's own files for the compiler's error type, and reads a JavaScript file's JSDoc types; the innermost such part is named. A reference that doesn't resolve, anywhere the walk goes, is still named first, since it names the package to install. The reference test adds all three and a .ts file whose JSDoc names something declared nowhere, which TypeScript ignores; each failed before.

  • An independent audit of that rule found two kinds of correct code it would have listed, and two augmentation loaders it missed, each fixed with a test that failed first:

    • A class's base is a value (class X extends Base<P> after const Base = React.Component, or a mixin's class extends B), which has no type of its own name. The extends of a class is skipped.
    • A name from a module found without types is any, not missing, so typeof fmt or typeof import('./format.js').fmt no longer counts, as the plain import already didn't.
    • A module declaration file that loads a package for its global effect through an import that takes no names (import {} from, import type {} from) or through export * acts globally too, so it starts the walk like a side-effect import.
  • The global walk started from every declaration file the config includes, so a module .d.ts that no component imports, but that imports a package that isn't installed, marked the whole package unresolved, though tsc compiles it under skipLibCheck. Only a declaration file that acts globally starts the walk now: a script, declare global, declare module, or a side-effect import or /// <reference>, which load augmentations for every file. Type packages named in types take the same test. A module that does none of these counts only where a component imports it. The global-roots test adds the module, a module that augments one, and a module with a side-effect import; the first failed before.

After the round-14 audit and review

  • A UMD declaration file (export as namespace) declares a global namespace for its module, yet it didn't start the walk, so a component reached through it was listed with a vaguer reason that didn't name the missing package. It counts as acting globally now. The global-roots test adds it, and it failed before.
  • An instantiation expression (export const StringSelect = Select<string>;) is an ExpressionWithTypeArguments too, so it was reported as a type that doesn't resolve, though tsc -p accepts it. Type arguments count now only in an interface's extends or a class's implements; a class's base was already left alone.
  • export * as legacy from 'pkg' binds the module's names under one name, as import * as does, yet it made an unimported module .d.ts a global root. Only a bare export * acts globally now.
  • A value the compiler can't type, spread into an object (const defaults = {...theme.sizes} where theme has no sizes, or {...undeclaredTones}), turns the object into any. So keyof typeof defaults takes any name, and no type node carries the error. Where a typeof in the package's own files reads a value, the walk now also checks what that value's type is inferred from, in any file of the package's own, for a value read the compiler can only give its error type. That means an initializer with no type written, and what a function returns when its return type isn't written and it has no overloads, a parameter's default, and a class's base and members. It follows the package's own declarations those name, so const slots = makeSlots(), lazy(makeSlots) and a class field are read through to the spread. A class member that a type indexes (Store['slots'], also through a subclass or an interface that extends the class) is read the same way, from its initializer or getter, and so is a class's base where a call builds it. A broken member that no type indexes decides no prop name, so it is left alone. A written type, a cast that writes one (not as const), a call made only for its effect, and JSX decide nothing about the type, so they are left alone. A read counts only where tsc reports an error inside it. So a line @ts-expect-error covers, a @ts-nocheck file and an index that misses without noImplicitAny are left alone, as tsc leaves them, and so are names from a module found without types.
    • Checking every expression in every file instead listed all 176 Core components: Core's i18n helper reads an engine API under @ts-expect-error. It also tripled the time on Core. So the check follows what the types read.
  • Each has a test that failed first. Core and Lab are unchanged, and the comparison on Core takes about 7 s, up from about 5.5 s; feat(cli): read two written TypeScript types as one when they differ only in how they are written #7684's new reading adds part of that.
  • Rebased onto main after the change that lists family members under their own names. That change lets a member's source be null, and feat(cli): doctor integration components compares documented props with the code #7676 lists such a member as not compared.
  • Known limitation, for a follow-up after these PRs: a class member that a type reaches through a generic key or an infer (type Get<K> = Store[K], T extends {slots: infer S} ? S : never) isn't read, so a broken member there leaves the component compared. That only happens where tsc --noEmit already fails, and the recommended typecheck script runs tsc first. Reading every member of a class passed as a type argument would catch it, but it would also uncheck a component over an unrelated broken member of a class it only refers to (a RefObject<Widget> prop).
  • This PR is groundwork with no command of its own, so it changes no user docs. The authoring guide for the props check, which explains these reasons, ships with feat(cli): doctor integration components compares documented props with the code #7676.

After the review of the class and spread fixes

  • The walk cached each file's result for every later call, but that result depended on the declarations the current call had already scanned. So a file a call reached after its declaration was cached as clean: CardHeader.ts, whose keyof typeof defaults comes from {...theme.sizes}, was found on a fresh program but missed once a call on Card.ts, which imports it, had run. Now nothing the walk learns outlives the program, and no answer depends on what an earlier call read. The record is kept per program, each declaration's own scan is memoized, and each start walks with its own visited set. Only declarations whose whole reach read clean are shared, which changes no answer.
  • A test calls it on Card.ts and CardHeader.ts in both orders on one program; it failed before. Core and Lab are unchanged.

After the review of the type packages

  • With types unset, TypeScript 5 loads every package in typeRoots, but the walk read type packages only when types was set, so the package's own type package there was never walked. It now asks the project's TypeScript which packages it loads (getAutomaticTypeDirectiveNames), whatever types says; TypeScript 6 loads none when types is unset. The test models TypeScript 5's default with TypeScript 6's types: ['*'], and failed before.

Rebased onto current main

After the review of JSDoc tags

  • In a JavaScript file, the walk read the type in every JSDoc tag, @throws and @exception included, which the compiler never checks. So a component whose file documents @throws {HttpError}, with HttpError declared nowhere, was listed as not compared, though its props are fully typed and tsc -p exits 0 with or without checkJs.
  • It now reads an allow-list: the tags the compiler reads types from. Those are @type, @param, @returns, @typedef, @callback, @template, @property, @this, @enum, @augments/@extends, @implements, @satisfies, @overload and @import, where the project's TypeScript has them. Everything else (@throws, @exception, @see, @example, unknown tags) is passed over, so a new tag can't bring the problem back.
  • A test file with @throws, @exception and two @see forms is now among the references that resolve; it failed before. Every allowed tag still reports a name declared nowhere, on TypeScript 4.8, 5.0 and 6.0.
  • An independent audit of this change found a related false result. A class whose base names nothing (class AcmeX extends NowhereBase<P> in TypeScript or JavaScript, or @augments {NowhereBase}) has no type the compiler can give, so <AcmeX /> doesn't type-check, yet it was compared and its documented props were reported as missing. The walk read a class's base only as a value, for the class it names. A base whose name resolves to nothing now counts as a reference that does not resolve, so the component is listed as not compared. Bases that resolve, call bases such as mixin(), and instantiation expressions read as before. Three cases join the test (two that name nothing, one that names a class); they failed before.

Merge order

The order is required:

  1. feat(cli): read a package's TypeScript project the way tsc reads it #7681 (reading the package's TypeScript project), then feat(cli): compare documented component props with the TypeScript source #7663 (the prop comparison, stacked on feat(cli): read a package's TypeScript project the way tsc reads it #7681), then feat(cli): doctor integration components compares documented props with the code #7676 (props in doctor integration components, stacked on feat(cli): compare documented component props with the TypeScript source #7663), then feat(cli): read two written TypeScript types as one when they differ only in how they are written #7684 (reading two written types as one, stacked on feat(cli): doctor integration components compares documented props with the code #7676), then feat(cli): doctor integration components judges documented prop types against the code #7667 (prop types, stacked on feat(cli): read two written TypeScript types as one when they differ only in how they are written #7684).
  2. feat(cli): check each authored doc against its published authoring type #7664 (the doc type check), after feat(cli): read a package's TypeScript project the way tsc reads it #7681: once feat(cli): read a package's TypeScript project the way tsc reads it #7681 merges, feat(cli): check each authored doc against its published authoring type #7664 is rebased onto main, which takes out the 65 lines the two share and brings it to 1,960, and reviewed again. Then feat(cli): doctor integration validate checks each doc against its published authoring type #7673 (doc types in validate, stacked on feat(cli): check each authored doc against its published authoring type #7664).
  3. fix(cli): doc links to a component, page, or block resolve #7668 (doc links to components and templates) any time before feat(cli): integration check runs every source check for a package's build, with --strict #7665.
  4. feat(cli): integration check runs every source check for a package's build, with --strict #7665 (integration check) last, after a rebase that adds these PRs' fields and codes to its failures list. Merged earlier, it breaks main.

This PR must ship in the same release as integration check, with #7663, #7676, #7684 and #7667.

Overlap with open PRs

Size

1,867 changed lines.

Scope

  • One capability is added.
  • Unrelated fixes, cleanup, and policy changes were removed or split.
  • Consumer docs, focused tests, and migration evidence ship with the feature.
  • Public text and artifacts contain no internal Meta context.

Testing

On main 9b8b723 with this branch: copyright, sync-exports, the published ./api type check, readme:check, the four CLI typechecks (strict, json-api, authoring-contract, template-docs), check:changesets, the lint job's changeset coverage check (run on the merge commit GitHub builds), check:cli-structure, check:repo, eslint on the changed files, prettier on the new files, and the full pnpm vitest run --project node suite: 10,291 tests pass. The 22 failures are in the npm pack, packed real-world package, theme resolution and CSS graph tests, which fail the same way on current main on the same machine (they need a package registry it doesn't reach), plus a build test and a search test that timed out on the busy machine and pass on their own. With all nine PRs merged into main, the four doctor integration subcommands were run in text and JSON on 64 package shapes, against main: templates output is byte-identical everywhere; validate and components only add their new section and JSON field; docs differs only where the doc-links PR fixes a link warning. integration check exits 1 exactly when one of the four would, each of its sections is byte-identical to that subcommand's output, and only integration check's three list tests fail until its planned rebase.

@vercel

vercel Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
astryx Ready Ready Preview Oct 19, 2026 3:44pm UTC

Request Review

@astracat-bot

astracat-bot Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

AI review status for this pull request.

Review status Updated
🟢 Reviewed (for maintainers only) Oct 19, 2026, 4:13 PM UTC

@github-actions

github-actions Bot commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR

🧪 Sandbox Preview

View Sandbox for this PR

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: No accessibility violations detected.

Visual Regression

40 of 482 shot(s) changed.

A change here is a question, not a failure: check whether the after is the
picture you intended. Record that review on the PR. Baseline maintenance is an
explicit dispatch of CI; this report never rewrites the baseline or adds a release gate.

component story theme mode pixels
Selector Indicator Space Bottom Sheet Narrow End RTL neutral light 754,077
Selector Indicator Space Bottom Sheet Wide Start neutral light 740,188
Chat Full AI Chat probe light 230,823
Chat Full AI Chat probe dark 229,805
RichTextEditor Markdown parity: side by side probe dark 94,866
RichTextEditor Markdown parity: side by side probe light 89,762
Collapsible Dividers probe dark 66,445
Collapsible Dividers probe light 66,445
Chat Default neutral dark 36,304
Chat Default neutral light 35,767
Chat Empty State probe light 23,531
Chat Empty State probe dark 22,622
ChatAutoScroll Scroll Behavior Comparison neutral dark 15,378
ChatAutoScroll Scroll Behavior Comparison neutral light 14,107
Chat With Attachments probe light 13,406
Chat With Attachments probe dark 11,232
ProgressBar With Target Mark probe dark 7,895
ProgressBar With Target Mark probe light 7,895
Markdown Default neutral dark 7,573
Markdown Default neutral light 7,566

and 20 more.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is on hold for a maintainer decision: the only authority this groundwork cites, AST-039 FR12 (and FR13 for the shared TypeScript resolver), exists only in the draft spec PR #7658, so a maintainer needs to approve that requirement, or identify the current spec, covering how the CLI picks a project's TypeScript and reads its tsconfig.json. Independently of that decision, package-tsconfig.mjs needs attention:

  • readPackageTsconfig throws ENOENT instead of reporting a config error when a files entry names a missing .ts file, because the ambient-file filter reads every included file outside the try (tsc reports this case as TS6053).
  • Two smaller gaps contradict the file header: references are followed only four levels deep although it says "at any depth", and a global script file (one with no import or export) is not picked up as an ambient file.

[Automated review]

Comment on lines +126 to +130
declarations: (parsed?.fileNames ?? []).filter(
/** @param {string} file */ file =>
/\.d\.[cm]?ts$/.test(file) ||
/\bdeclare\s+(?:global|module)\b/.test(fs.readFileSync(file, 'utf8')),
),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reads every non-declaration file in fileNames, and it runs outside the try above. TypeScript's config parse does not check that a files entry exists, so "files": ["src/Button.tsx", "src/missing.ts"] parses with no error and this line then throws ENOENT. tsc reports the same config as TS6053. The header promises that "A config with errors is reported". Treating a file that cannot be read as non-ambient, or returning it as error, would fix this; please add a test for that case.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 577fdd0. A files entry that doesn't exist is now a config error, tsconfig.json: File 'src/missing.ts' not found., matching tsc's TS6053. The ambient filter also treats a file it can't read as not ambient, so the reading never throws. The test 'reports a config with errors instead of reading defaults' adds the case "files": ["src/Button.tsx", "src/missing.ts"].

const including = (config, depth) =>
includes(config)
? config
: depth < 4

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The header and the test title say references are followed "at any depth", but this stops after four levels. A five-level chain returns the solution root's empty options with no error, so the package is read with TypeScript's built-in defaults. Tracking the config paths already visited, instead of capping depth, would match the docs and still stop cycles. Otherwise, please document the cap and report an error when it is reached.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 577fdd0. The walk now follows references at any depth, tracking the config paths it has visited instead of capping the depth, so a cycle ends. The test now follows a six-level chain whose every level also refers back to the root, and reads the deepest config's options.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is on hold for a maintainer decision on spec authority: the only requirement this groundwork cites, AST-039 FR12 (and FR13 for the shared TypeScript resolver), is still in the draft spec PR #7658, so a maintainer needs to approve it, or identify a current spec, covering how the CLI picks a project's TypeScript (including reporting a 7.x install by its version) and which tsconfig.json governs a package. The three issues raised on the previous revision (the ENOENT throw on a missing files entry, the four-level reference cap, and missed global scripts) are fixed, each with a test that fails without the fix. No further change is needed from the author while that decision is pending.

[Automated review]

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is on hold for a maintainer decision on spec authority: the only requirement this groundwork cites, AST-039 FR12 (and FR13 for the shared TypeScript resolver), is still in the draft spec PR #7658, so a maintainer needs to approve it, or identify a current spec, covering how the CLI picks a project's TypeScript and which tsconfig.json governs a package. Independently of that decision, the new files check in package-tsconfig.mjs needs a fix: it resolves the raw files strings itself, so a config that tsc -p compiles cleanly is reported as a config error when an entry uses ${configDir} (which the previous revision read correctly) or leaves off its extension. Resolving each entry the way TypeScript does, with a test for each case, would fix it; the fix for include matches that vanish mid-read works as described.

[Automated review]

.filter(/** @param {unknown} file */ file => typeof file === 'string')
.map(
/** @param {string} file */ file =>
path.resolve(path.dirname(configPath), file),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This resolves the raw files string itself, so it misses TypeScript's own resolution: "files": ["${configDir}/src/Button.ts"] (also when inherited from a shared base) and "files": ["src/Button"] with src/Button.ts present both compile with tsc -p (exit 0), but this returns File '…' not found. and the whole config reads as an error. The previous check, over the file names TypeScript had already substituted, read the ${configDir} case correctly.

Substituting a leading ${configDir} with the config's directory, accepting a .ts, .tsx or .d.ts file for an entry without an extension, and checking with ts.sys.fileExists (false for a directory, which tsc rejects as TS6231) would match tsc; please add those cases to the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in c950711. TypeScript now reads each files entry itself, so the check can't drift from tsc. The entries are TypeScript's own parse of the config with nothing matched by include. Whether each is found is a program over just those entries (no library, no types, no imports): TS6053 for a missing file, TS6231 for a folder. That substitutes ${configDir} on a version that has it, resolves an entry against the config that writes it, and tries the TypeScript extensions for one without its own. The files it finds are the ones read. Test: 'reads each files entry as tsc finds it' covers ${configDir} in the package's config and in a shared base it extends, entries without their extension (an ambient script among them is loaded), a missing file and a folder. On TypeScript 5.0 and 4.8, which have no ${configDir}, tsc -p reports such an entry as not found, and so does the check.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is on hold for a maintainer decision on spec authority: the only requirement this groundwork cites, AST-039 FR12 (and FR13 for the shared TypeScript resolver), is still in the draft spec PR #7658, so a maintainer needs to approve it, or identify a current spec, covering how the CLI picks a project's TypeScript and which tsconfig.json governs a package. The files fix works as described, but two issues in package-tsconfig.mjs need fixing regardless of that decision:

  • A solution-style tsconfig.json whose referenced config is missing or does not parse is read with the root's options and no error, while tsc reports it (TS6053, TS5083 or TS1136); returning that reference's error when no config includes the sources, with a test, would fix it.
  • A global script that declares a namespace, such as declare namespace Acme { export interface Props {} }, is not loaded, because the module check also matches the indented export; TypeScript's own module detection, or counting only an unindented import or export, would fix it.

[Automated review]

const next = path.resolve(ts.resolveProjectReferencePath(ref));
if (visited.has(next)) continue;
visited.add(next);
const hit = including(parse(next));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a referenced config is missing or does not parse, parse(next) sets its error, but it has no file names, so including passes over it and line 196 falls back to the solution root's own options with no error. For the same project tsc -p reports TS6053 and tsc -b reports TS5083 or TS1136, and the header says a config with errors is reported, never read as defaults. Keeping the first referenced config that carries an error and returning it when no config includes the sources would fix this; please add a test for a missing and an unparsable reference.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 665d0d3. The reference walk keeps the first referenced config that is missing or doesn't parse. When no config includes the sources, that config's error is the project's error, as tsc reports it, and the comparison lists every table with it. The references test adds a reference to a missing config and one to a config that doesn't parse; both failed before.

}
return (
/\bdeclare\s+(?:global|module)\b/.test(text) ||
(/\.tsx?$/.test(file) && !/^\s*(?:import|export)\b/m.test(text))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

^\s* also matches an indented export inside a namespace, so a global script such as declare namespace Acme { export interface Props {} }, which TypeScript treats as a script rather than a module, is not loaded. Deciding with TypeScript's own parse (ts.isExternalModule on the parsed file), or counting only an unindented import or export, would load it; please add the case to the test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 665d0d3. TypeScript's own module detection decides now: a .ts file that writes import or export anywhere is parsed, and it counts as a script when isExternalModule is false. So declare namespace Acme { export interface Props {} } is loaded. The defaults test adds that file. A file that writes neither word skips the parse; on this repository's Core, the parse adds about a second over 1,292 files.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is on hold until a maintainer approves the draft AST-039 FR12 (and FR13 for the shared TypeScript resolver) in #7658, or identifies a current spec, covering how the CLI picks a project's TypeScript, which tsconfig.json governs a package, and which unresolved references make a package's types a guess. Both issues from the previous revision are fixed, each with a test. Independently of that decision, unresolvedGlobals in the new unresolved-references.mjs starts from every declaration file the config includes, so a module .d.ts that no component imports, but that imports a package that isn't installed, marks the whole package unresolved even though tsc compiles it with skipLibCheck. Starting the walk only from declaration files that declare globals or augment a module, with a test, would fix it.

[Automated review]

Comment on lines +222 to +224
const roots = declarations
.filter(file => own || isOwnFile(file))
.map(file => program.getSourceFile(file));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declarations holds every .d.ts the config includes, because isAmbient returns true for any .d.ts. So a module declaration file becomes a global root here. A module .d.ts declares nothing global, yet a missing import in one that no component imports is reported as the package's unresolved reference. For example, take src/legacy/old-api.d.ts containing import type {Old} from '@acme/removed-package'. This returns @acme/removed-package, while tsc -p exits 0 under skipLibCheck: true. Keeping only the declaration files TypeScript reads as no module, or that contain declare global or declare module, would match "whose types every file sees". Please add the case to the unresolvedGlobals test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in b986443. Only a declaration file that acts globally starts the walk now: one TypeScript reads as no module, or with declare global or declare module, or with an import that takes no names (a side-effect import, import {} from, import type {} from), an export *, or a /// <reference>. Those last forms load a package for its augmentations, which is why they stay; an own type package whose only content is import '@missing/css-prop' would otherwise lose its css prop silently. Type packages named in types take the same test. A module like src/legacy/old-api.d.ts counts only where a component imports it. The unresolvedGlobals test adds that case (compared now, and it failed before) and keeps the augmenting and loading forms as roots.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approval is on hold until a maintainer approves AST-039 FR12 (and FR13 for the shared TypeScript resolver), which reached main through #7658 but is still a draft record, or identifies another current spec covering how the CLI picks a project's TypeScript, which tsconfig.json governs a package, and which unresolved references make a package's types a guess. The previous revision's issue is fixed, with a test. Independently of that decision, unresolved-references.mjs needs two fixes:

  • An instantiation expression such as export const StringSelect = Select<string>; is reported as an unresolved type, though tsc -p accepts it, because ExpressionWithTypeArguments is checked outside heritage clauses; checking it only in an extends or implements clause, with a test, would fix it.
  • export * as x from 'pkg' in a module .d.ts that no component imports starts the global walk, unlike import * as x from 'pkg', so a missing package there marks every component unresolved; counting only a bare export * would fix it.

[Automated review]

K.TypeQuery,
K.IndexedAccessType,
K.ImportType,
K.ExpressionWithTypeArguments,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An instantiation expression is also an ExpressionWithTypeArguments, so export const StringSelect = Select<string>; reaches isErrorType: getTypeFromTypeNode resolves Select as a type name, finds only a value, and returns the error type. The walk then reports `Select<string>` as unresolved, while tsc -p exits 0 on the same package; the same happens through a helper a component imports (makeBox<string>). isClassBase only exempts a class's extends. Checking an ExpressionWithTypeArguments only when node.parent is a HeritageClause (an interface's extends, a class's implements) would fix it; please add an instantiation expression to the cases that resolve.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6a3895e. An ExpressionWithTypeArguments is checked only where it names a type: in an interface's extends or a class's implements. An instantiation expression (select<string>), like a class's base, gives type arguments to a value, so it's left alone. The reference test adds export const selectString = select<string>; with a typeof of it to the cases that resolve; it failed before.

(ts.isExportDeclaration(statement) &&
!!statement.moduleSpecifier &&
(!statement.exportClause ||
ts.isNamespaceExport(statement.exportClause))),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

export * as legacy from 'pkg' binds every name under one, like import * as legacy from 'pkg', which importsNames counts as taking names, but this arm makes it a global root. An unimported src/legacy/index.d.ts with export * as legacy from '@acme/removed-package' makes unresolvedGlobals return `@acme/removed-package` for the whole package, while tsc -p exits 0 under skipLibCheck and the import * as form returns null. Dropping the isNamespaceExport arm, so only a bare export * counts, would fix it; please add the case to the unresolvedGlobals test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 6a3895e. Only a bare export * counts as acting globally now; export * as legacy from 'pkg' binds names as import * as does. The unresolvedGlobals test adds an unimported module .d.ts with each of import * as legacy and export * as legacy from a missing package, and both return null; the export * as one failed before.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: one fix in unresolved-references.mjs is needed before approval; the governing claims are now current in the CLI surface record (INV33/INV34), and both issues from the previous revision are fixed with tests. unresolvedReference caches each file's result across calls in the module-level IMPORTS map, but that result depends on the per-call checkedDeclarations set, so a file an earlier call on the same program already walked through is cached as clean: CardHeader.ts, whose keyof typeof defaults comes from {...theme.sizes} (a TS2339 error), returns theme.sizes on a fresh program but null once a call on Card.ts, which imports it, has run. Caching only missing and next across calls (or memoizing each declaration's scan result instead of skipping it), with a test that calls twice on one program in both orders, would fix it.

[Automated review]

};
/** @type {(sf: any) => {missing: UnresolvedReference | null, untyped: UnresolvedReference | null, next: any[]}} */
const read = sf => {
const cache = IMPORTS[mine(sf.fileName) ? 1 : 0];

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This cache outlives the call, but the untyped it stores depends on checkedDeclarations, which is per call: a declaration already scanned in this call is skipped (lines 186–194), so a file reached after it is cached with untyped: null even when its type comes from the same erroneous value. On one program, CardHeader.ts (with keyof typeof defaults, where defaults = {...theme.sizes} is a TS2339 error) returns `theme.sizes` when checked first, but null once a call on Card.ts, which imports it, has run. Caching only missing and next across calls, or memoizing each declaration's scan result instead of skipping it, would fix it; please add a test that calls twice on one program in both orders.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 56eb82a. Nothing the walk learns outlives the program now, and no answer depends on what an earlier call read. The record is kept per program and per mine test, in a WeakMap keyed on the program. Each declaration's own scan is memoized by declaration: the first error read in what its type is inferred from, and the declarations it names. Each start then walks those depth first with a fresh visited set. The only thing shared across calls is the set of declarations whose whole reach read clean, which can't change an answer. The new test calls unresolvedReference on Card.ts and CardHeader.ts in both orders on one program and gets theme.sizes for both each time; it failed before. Core and Lab are unchanged. The props check passes one mine per run, so each file is still read once.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: one small fix in unresolved-references.mjs is needed before approval; the order-dependent cache from the previous revision is fixed and tested, and the governing claims (CLI surface INV33/INV34) are current. unresolvedGlobals walks type packages only when compilerOptions.types is set, but TypeScript 5.x loads every typeRoots package automatically when types is unset, so a package's own type package there is never walked: on 5.9.3, a types/css-prop/index.d.ts that references an uninstalled package returns null, while naming it in types reports the missing package. Iterating ts.getAutomaticTypeDirectiveNames(options, ts.sys) unconditionally (it already returns [] where TypeScript 6 loads nothing), with a test, would fix it.

[Automated review]

Comment on lines +724 to +726
for (const name of options.types
? ts.getAutomaticTypeDirectiveNames(options, ts.sys)
: []) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Before TypeScript 6, an unset types makes the program load every package in typeRoots, so a package's own type package there is loaded but never walked. On 5.9.3, with typeRoots: ['./types', './node_modules/@types'] and types/css-prop/index.d.ts containing /// <reference types="@acme/not-installed-css" />, this returns null, while types: ['css-prop'] returns the types `@acme/not-installed-css` . TypeScript 6 loads nothing automatically, which is why the suite on 6.0.3 can't see it. Iterating ts.getAutomaticTypeDirectiveNames(options, ts.sys) unconditionally would fix it: it returns [] where TypeScript 6 loads nothing, follow still keeps a dependency's type package out, and the existing suite still passes with that change. Please add a test.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 4178089. unresolvedGlobals now asks the project's TypeScript which type packages it loads, ts.getAutomaticTypeDirectiveNames(options, ts.sys), whatever types says: every typeRoots package on TypeScript 5 with types unset, none on TypeScript 6. So a package's own types/css-prop is walked on 5.x and its missing @acme/not-installed-css is named. The test models TypeScript 5's default with TypeScript 6's types: ['*'], since the repository installs 6, and failed before.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommending approval: the fix requested on the previous revision is in, since unresolvedGlobals now walks every type package the project's TypeScript loads, including those TypeScript 5 loads automatically when types is unset, and a check against the compiler on TypeScript 5.9.3 and 6.0.3 confirms it. Every change is internal groundwork for the current CLI-surface INV33/INV34, nothing reaches it until the stacked pull requests wire it in, and the changed tests, lint, the changeset check and CI pass. No further changes are needed; this is ready for a maintainer's approval.

[Automated review]

Groundwork for the documented-props comparison: `resolveProjectTypeScript`
finds the project's own `typescript` (and names an installed version that
has no JavaScript compiler API, such as TypeScript 7), and
`readPackageTsconfig` reads the package's TypeScript project as `tsc`
reads it: the nearest `tsconfig.json`, which is the package's own or a
monorepo root's for a local package but never an app's above an installed
package; a solution-style config's references, at any depth, to the config
that includes the sources; and the declaration files the config includes.
A config with errors is reported, never read as defaults. Nothing reaches
it yet, so the CLI's behavior does not change.
…h, and read global scripts

A `files` entry that does not exist is a config error, as `tsc` reports it
(TS6053), instead of a crash. References are followed at any depth, each
config once, so a cycle ends. An included script (a `.ts` file with no
import or export) declares globals and joins the files to read, like a
declaration file.
…nnot find it

A file the config's `include` matched that is gone by the time it is read,
such as one another process writes and removes, made the package's config
read as having an error, and every prop table was listed as not compared.
Each `files` entry is now read by TypeScript itself, which substitutes
`${configDir}`, resolves the entry against the config that writes it, and
tries the TypeScript extensions for one written without its own. Only an
entry it cannot find is an error, with the message `tsc` gives (TS6053, or
TS6231 for a folder), and the files it finds are the ones read.
…compiler cannot resolve

A foundation module, `unresolved-references.mjs`, finds the first reference
the compiler cannot resolve, so a check that reads a package's types knows
when part of them is a guess. `unresolvedReference` walks given files, the
files they import, and so on, as far as the caller allows: a module the
program cannot find, a name its module does not export, a missing
`/// <reference>` type package or file, a side-effect import where the
compiler checks one fail, and so does, in the package's own files, a type
the compiler can only give its error type (a name or value declared
nowhere, a property that doesn't exist, a JavaScript file's JSDoc type),
or a value read that way, with an error `tsc` reports, in what the
type of a value a `typeof` reads, a member a type indexes, or a class's
base is inferred from;
a module found without types resolves. A missing reference anywhere the
walk goes is named before such a type. What the walk learns is kept per
program, so an answer never depends on what an earlier call read.
`unresolvedGlobals` walks the program's global roots: the declaration
files the package's own config includes, wherever they sit, its own type
packages, and each type package the program loads (the ones the config
names, or with `types` unset, every one in `typeRoots` where the project's
TypeScript loads them all), which fails when it cannot be found. A config from above the package starts only from the
package's own declaration files, and a dependency's declarations are not
followed, as `tsc` skips their errors.

The documented-props comparison, stacked on this, uses it to list a
component as not compared when a dependency it reads is not installed.
…bal script that declares a namespace

A solution-style config whose referenced config is missing or does not
parse was read with the root's own options and no error, while `tsc`
reports it. When no config includes the sources, the first broken
reference is now the project's error. A global script that declares a
namespace with an `export` inside it was taken for a module, so its types
were not loaded; TypeScript's own module detection decides now.
@josephfarina
josephfarina force-pushed the josephfarina/doctor-typescript-project branch from 4178089 to 9a2f678 Compare October 19, 2026 12:42
@josephfarina

Copy link
Copy Markdown
Contributor Author

Rebased onto main 9b8b723, so the PRs stacked on this one carry main's #7691 (a ContextMenu doc fix), which the stacked props PR's Core test needs. The diff is unchanged apart from the base move.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes requested: one small fix in unresolved-references.mjs is needed before approval; every change here serves the current CLI-surface INV33/INV34, and the rebase left the delta unchanged. unresolvedReference checks every JSDoc type in a JavaScript file, including @throws and @exception, which the compiler never checks, so a component whose file documents @throws {HttpError} with HttpError declared nowhere is reported as having an unresolved reference even though its props are fully typed and tsc -p exits 0, with or without checkJs. Skipping that tag when visiting a JavaScript file's JSDoc (for example, returning early from visit for ts.SyntaxKind.JSDocThrowsTag), with a @throws case among the references that resolve in unresolved-references.test.mjs, would fix it.

[Automated review]

/** @type {(node: any) => void} */
const visit = node => {
if (untyped) return;
if (js) for (const doc of node.jsDoc ?? []) visit(doc);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This visits every JSDoc tag of a JavaScript file, including @throws and @exception, whose type the compiler never checks. isErrorType then gives an undeclared name there the error type, so a file documenting @throws {HttpError} with HttpError declared nowhere returns `HttpError` as unresolved, and a component with fully typed props would be listed as not compared, while tsc -p exits 0 with checkJs off and on. Skipping that tag, for example with an early if (node.kind === ts.SyntaxKind.JSDocThrowsTag) return; in visit, and adding a @throws case to the references that resolve in the test, would fix it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 07caa63 with an allow-list rather than one more skip. In a JavaScript file the walk now reads only the JSDoc tags the compiler reads types from: @type, @param, @returns, @typedef, @callback, @template, @property, @this, @enum, @augments/@extends, @implements, @satisfies, @overload and @import, where the project's TypeScript has them. It passes over every other tag (@throws, @exception, @see, @example, unknown tags). A file with @throws {HttpError}, @exception, and @see in two forms is now among the references that resolve in unresolved-references.test.mjs; it failed before. Every allowed tag still reports a name declared nowhere, on TypeScript 4.8, 5.0 and 6.0. An independent audit of this change also found that a class whose base names nothing (class AcmeX extends NowhereBase<P>, or @augments {NowhereBase} in JavaScript) was compared, with its documented props reported as missing, though <AcmeX /> doesn't type-check. The walk read a class's base only as a value; a base whose name resolves to nothing now counts as a reference that does not resolve, with tests that failed before.

…d a class base that names nothing

In a JavaScript file, the walk for references that do not resolve read the
type in every JSDoc tag, `@throws` and `@exception` included, which the
compiler never checks. So a component whose file documents
`@throws {HttpError}`, with `HttpError` declared nowhere, was listed as not
compared, though its props are fully typed and `tsc -p` exits 0. It now
reads only the tags the compiler reads types from (`@type`, `@param`,
`@returns`, `@typedef`, `@callback`, `@template`, `@property`, `@this`,
`@enum`, `@augments`, `@implements`, `@satisfies`, `@overload` and
`@import`, where the project's TypeScript has them) and passes over the
rest (`@throws`, `@see`, `@example` …).

A class whose base names nothing (`class AcmeX extends NowhereBase<P>`, in
TypeScript or JavaScript, or `@augments {NowhereBase}`) has no type the
compiler can give, so `<AcmeX />` does not type-check, yet it was compared
and its documented props reported as missing. The walk read a class's base
only as a value, for the class it names; a base that resolves to nothing
now counts as a reference that does not resolve, so the component is
listed as not compared.

@astracat-bot astracat-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Recommending approval: both changes in this revision work as described (a JavaScript file's JSDoc is now read only in the tags the compiler takes types from, and a class whose base names nothing is reported), and each agrees with tsc -p on TypeScript 5.9.3 and 6.0.3. Everything here remains internal groundwork for the CLI-surface INV33/INV34 checks, nothing reaches it until the stacked pull requests wire it in, and the changed suites, lint and the changeset check pass. One optional follow-up: a class whose base is read off an any value (declare const lib: any; class X extends lib.Component<P> {}) is reported as a base that does not resolve although tsc accepts it, so also requiring the compiler's error type in baseless would keep that reason accurate.

[Automated review]

(ts.isHeritageClause(node.parent) ||
ts.isJSDocAugmentsTag(node.parent)) &&
ts.isEntityNameExpression(node.expression) &&
!checker.getSymbolAtLocation(node.expression);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-blocking: getSymbolAtLocation also returns no symbol for a property read off an any value, so declare const lib: any; export class X extends lib.Component<{label?: string}> {} returns `lib.Component` here as a base that names nothing, while tsc -p exits 0 on TypeScript 5.9.3 and 6.0.3. The class's props are any, so listing it as not compared is still right, but the reason would send the author after a name that resolves. Also requiring isErrorValue(checker, node.expression) here leaves that case unreported and still reports NowhereBase, in TypeScript and in @augments.

@josephfarina
josephfarina merged commit 1c69283 into main Oct 19, 2026
52 of 53 checks passed
@github-actions
github-actions Bot deleted the josephfarina/doctor-typescript-project branch October 20, 2026 06:22

This branch was successfully deployed

1 active deployment
Preview — 07caa63e Deployed Oct 19, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant