diff --git a/packages/compiler/src/frontend/program.ts b/packages/compiler/src/frontend/program.ts index 25caecb64..2f1b2f729 100644 --- a/packages/compiler/src/frontend/program.ts +++ b/packages/compiler/src/frontend/program.ts @@ -488,43 +488,33 @@ function visitNpmStaticDeclarationClosure( visit(entryPath); } -function npmStaticDeclarationOverloadsOf( +/** The overloads and properties of the classes a package's declaration + * closure declares, read from one parse of each declaration file. */ +function npmStaticDeclarationMetadataOf( services: FrontendServices, entryPath: string, -): NpmStaticDeclarationOverloads { - const classes = new Map>(); +): { overloads: NpmStaticDeclarationOverloads; properties: NpmStaticDeclarationProperties } { + const overloads = new Map>(); + const properties = new Map>(); visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => { - for (const [className, methods] of parseNpmStaticDeclarationOverloads( - services.parse(path, source, "ts"), - )) { + const sourceFile = services.parse(path, source, "ts"); + for (const [className, methods] of parseNpmStaticDeclarationOverloads(sourceFile)) { const target = - classes.get(className) ?? new Map(); - classes.set(className, target); + overloads.get(className) ?? new Map(); + overloads.set(className, target); for (const [methodName, signatures] of methods) { if (!target.has(methodName)) target.set(methodName, signatures); } } - }); - return classes; -} - -function npmStaticDeclarationPropertiesOf( - services: FrontendServices, - entryPath: string, -): NpmStaticDeclarationProperties { - const classes = new Map>(); - visitNpmStaticDeclarationClosure(services, entryPath, (path, source) => { - for (const [className, properties] of parseNpmStaticDeclarationProperties( - services.parse(path, source, "ts"), - )) { - const target = classes.get(className) ?? new Map(); - classes.set(className, target); - for (const [propertyName, type] of properties) { + for (const [className, fields] of parseNpmStaticDeclarationProperties(sourceFile)) { + const target = properties.get(className) ?? new Map(); + properties.set(className, target); + for (const [propertyName, type] of fields) { if (!target.has(propertyName)) target.set(propertyName, type); } } }); - return classes; + return { overloads, properties }; } /** See LoadResult.startupCrash: Node's exact error message, the IR error @@ -1096,8 +1086,7 @@ export function loadProgram( ) { continue; } - const overloads = npmStaticDeclarationOverloadsOf(services, resolved.typesFile); - const properties = npmStaticDeclarationPropertiesOf(services, resolved.typesFile); + const { overloads, properties } = npmStaticDeclarationMetadataOf(services, resolved.typesFile); const classNames = new Set([...overloads.keys(), ...properties.keys()]); if (classNames.size === 0) continue; const runtime = resolveBareModule(entryPath, pkg, "js-only"); diff --git a/packages/compiler/src/frontend/services.ts b/packages/compiler/src/frontend/services.ts index 14b366bfa..a0acb5818 100644 --- a/packages/compiler/src/frontend/services.ts +++ b/packages/compiler/src/frontend/services.ts @@ -20,6 +20,12 @@ import * as syntax from "./npm-static-declaration-syntax.js"; import { normalizeCjsExportsVar } from "./npm-static-cjs-exports-var.js"; import type { SourceFile } from "./ts7/ast-types.js"; +/** Whether the source can contain `word` as a keyword or identifier: it + * holds the word itself or a Unicode escape that might spell it. */ +function spells(source: string, word: string): boolean { + return source.includes(word) || source.includes("\\u"); +} + /** Evaluate an isolated, self-contained TypeScript callback under a finite * budget. Hosts supply execution; shared lowering owns capture checks and * verifies the returned value before baking it into the program. */ @@ -150,13 +156,18 @@ export class FrontendServices { return normalizeCjsExportsVar(this.parse(path, source, "js")); } nullableClassFields(path: string, source: string): syntax.NpmStaticOverloadRewrite | null { + // Rewrites `null`-initialized fields of class declarations. + if (!spells(source, "class") || !spells(source, "null")) return null; return syntax.applyNpmStaticNullableClassFields(this.parse(path, source, "js"), source); } jsDocNamepaths(path: string, source: string): string | null { - if (!source.includes("~") && !source.includes("#")) return null; + // A namepath segment joined by `#` or `~` (applyNpmStaticJsDocNamepaths). + if (!/[$\w][#~][$A-Z_a-z]/.test(source)) return null; return syntax.applyNpmStaticJsDocNamepaths(this.parse(path, source, "js"), source); } findReturnWidening(path: string, source: string): syntax.NpmStaticOverloadRewrite | null { + // Widens `find` calls returned by methods of class declarations. + if (!spells(source, "class") || !spells(source, "find")) return null; return syntax.applyNpmStaticFindReturnWidening(this.parse(path, source, "js"), source); } declarationProperties(