From 16539a629b9d42181c53eb87f3439e2d73e88706 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Wed, 30 Sep 2026 00:29:42 -0700 Subject: [PATCH] parser: a bare import of a package the repository declares binds to its source A monorepo imports a sibling package by its name, and that package's main / types / exports name build output that is not in the source tree. tsc then resolved nothing, or a dist file no program walks, so every call, type and const across the package boundary was matched by name only. The package-entry rows already map a dist target back to the source it is built from; import resolution now uses the same mapping (subpaths and patterns included) for any bare specifier whose package.json lives under the walked tree, in the TypeScript and JavaScript front ends. A package tsc resolves inside node_modules is left alone. impact: asked of an interface method, the implementations the engine dispatches to are listed under "must change with it". Without an override table they had no row at all, and one only surfaced through a wrong by-name edge that the resolved import removed. The hook's SQL summary gets the same leg and now drops shape-only pairs as the rules do. Tests: new TypeScript case 85-workspace-package-import (scoped package with exports, subpath, pattern, unscoped deep import, a field-typed receiver, and a third-party control); the three JavaScript alias cases fold into 70-bare-specifier-to-project-source with a workspace-package table row; the dispatch-base CLI case checks the base side. Co-authored-by: axiomcode-bot[bot] <334110751+axiomcode-bot[bot]@users.noreply.github.com> --- .../src/bundler-alias}/legacy/app.js | 0 .../src/bundler-alias}/legacy/lib/extra.js | 0 .../src/bundler-alias}/legacy/lib/index.js | 0 .../bundler-alias}/legacy/lib/util/trim.js | 0 .../bundler-alias}/legacy/webpack.config.cjs | 0 .../src/bundler-alias}/mapped/jsconfig.json | 0 .../src/bundler-alias}/mapped/own/date.js | 0 .../src/bundler-alias}/mapped/probe.js | 0 .../src/bundler-alias}/package.json | 0 .../src/bundler-alias}/shared/fmt.js | 0 .../src/bundler-alias}/src/pages/Home.js | 0 .../src/bundler-alias}/src/utils/date.js | 0 .../src/bundler-alias}/vite.config.js | 0 .../src/framework-alias}/kit/jsconfig.json | 0 .../src/framework-alias}/kit/src/lib/api.js | 0 .../framework-alias}/kit/src/routes/page.js | 0 .../framework-alias}/nuxt/server/api/price.js | 0 .../src/framework-alias}/nuxt/tsconfig.json | 0 .../src/framework-alias}/nuxt/utils/price.js | 0 .../framework-alias}/nuxt4/app/pages/index.js | 0 .../framework-alias}/nuxt4/app/utils/date.js | 0 .../src/framework-alias}/nuxt4/tsconfig.json | 0 .../src/framework-alias}/other/jsconfig.json | 0 .../src/framework-alias}/other/probe.js | 0 .../framework-alias}/other/src/lib/helper.js | 0 .../src/framework-alias}/own/jsconfig.json | 0 .../src/framework-alias}/own/probe.js | 0 .../src/framework-alias}/own/shared/util.js | 0 .../src/framework-alias}/own/src/lib/util.js | 0 .../src/framework-alias}/package.json | 0 .../src/jsconfig-paths}/app.js | 0 .../src/jsconfig-paths}/jsconfig.json | 0 .../src/jsconfig-paths}/lib/check.js | 0 .../src/jsconfig-paths}/noalias/jsconfig.json | 0 .../src/jsconfig-paths}/noalias/probe.js | 0 .../src/jsconfig-paths}/package.json | 0 .../remix/app/models/note.server.js | 0 .../src/jsconfig-paths}/remix/app/route.js | 0 .../src/jsconfig-paths}/remix/tsconfig.json | 0 .../workspace-package/apps/app/package.json | 1 + .../workspace-package/apps/app/src/main.js | 13 ++ .../packages/lib/package.json | 8 ++ .../packages/lib/src/index.js | 9 ++ .../packages/lib/src/sub/index.js | 3 + .../packages/tools/package.json | 1 + .../packages/tools/src/deep.js | 3 + .../70-bare-specifier-to-project-source.diag | 35 ++++++ .../70-bare-specifier-to-project-source.edges | 35 ++++++ ...70-bare-specifier-to-project-source.oracle | 9 ++ .../expected/70-jsconfig-path-alias.diag | 3 - .../expected/70-jsconfig-path-alias.edges | 4 - .../expected/70-jsconfig-path-alias.oracle | 2 - .../expected/71-bundler-config-alias.diag | 21 ---- .../expected/71-bundler-config-alias.edges | 18 --- .../expected/71-bundler-config-alias.oracle | 8 -- .../71-framework-generated-config-alias.diag | 3 - .../71-framework-generated-config-alias.edges | 7 -- ...71-framework-generated-config-alias.oracle | 1 - .../src/apps/app/package.json | 1 + .../src/apps/app/src/local.ts | 1 + .../src/apps/app/src/main.ts | 25 ++++ .../src/apps/app/types/zod.ts | 4 + .../src/package.json | 1 + .../src/packages/lib/package.json | 10 ++ .../src/packages/lib/src/feature/flags.ts | 3 + .../src/packages/lib/src/index.ts | 7 ++ .../src/packages/lib/src/sub/index.ts | 1 + .../src/packages/tools/package.json | 1 + .../src/packages/tools/src/deep.ts | 1 + .../src/packages/tools/src/index.ts | 1 + .../src/pnpm-workspace.yaml | 3 + .../85-workspace-package-import.edges | 8 ++ .../85-workspace-package-import.entries | 3 + .../85-workspace-package-import.fields | 1 + .../85-workspace-package-import.fields-oracle | 7 ++ .../85-workspace-package-import.oracle | 1 + .../85-workspace-package-import.type-use | 1 + .../85-workspace-package-import.types-oracle | 7 ++ .../typescript/ground-truth/tsc-program.mjs | 26 ++++ .../imports/TsImportResolutionKind.ts | 5 + .../extractors/js-fact-extractor.ts | 6 + .../extractors/js-module-edge-extractor.ts | 22 +++- .../extractors/ts-fact-extractor.ts | 22 ++-- .../extractors/ts-import-extractor.ts | 35 +++++- .../typescript/ts-package-entry-extractor.ts | 64 +++++++++- .../parsers/typescript/workspace-packages.ts | 111 ++++++++++++++++++ .../javascript/javascript-project-analyzer.ts | 19 +++ .../typescript/typescript-project-analyzer.ts | 43 +++++++ .../skills/axiomcode/scripts/dl/impact.dl | 6 + .../skills/axiomcode/scripts/graph_sql.py | 18 +++ .../dispatch-base-is-a-contract/case.json | 6 +- 91 files changed, 567 insertions(+), 87 deletions(-) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/legacy/app.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/legacy/lib/extra.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/legacy/lib/index.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/legacy/lib/util/trim.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/legacy/webpack.config.cjs (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/mapped/jsconfig.json (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/mapped/own/date.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/mapped/probe.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/package.json (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/shared/fmt.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/src/pages/Home.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/src/utils/date.js (100%) rename graph/test/javascript/cases/{71-bundler-config-alias/src => 70-bare-specifier-to-project-source/src/bundler-alias}/vite.config.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/kit/jsconfig.json (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/kit/src/lib/api.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/kit/src/routes/page.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/nuxt/server/api/price.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/nuxt/tsconfig.json (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/nuxt/utils/price.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/nuxt4/app/pages/index.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/nuxt4/app/utils/date.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/nuxt4/tsconfig.json (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/other/jsconfig.json (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/other/probe.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/other/src/lib/helper.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/own/jsconfig.json (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/own/probe.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/own/shared/util.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/own/src/lib/util.js (100%) rename graph/test/javascript/cases/{71-framework-generated-config-alias/src => 70-bare-specifier-to-project-source/src/framework-alias}/package.json (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/app.js (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/jsconfig.json (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/lib/check.js (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/noalias/jsconfig.json (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/noalias/probe.js (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/package.json (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/remix/app/models/note.server.js (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/remix/app/route.js (100%) rename graph/test/javascript/cases/{70-jsconfig-path-alias/src => 70-bare-specifier-to-project-source/src/jsconfig-paths}/remix/tsconfig.json (100%) create mode 100644 graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/apps/app/package.json create mode 100644 graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/apps/app/src/main.js create mode 100644 graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/package.json create mode 100644 graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/src/index.js create mode 100644 graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/src/sub/index.js create mode 100644 graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/tools/package.json create mode 100644 graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/tools/src/deep.js create mode 100644 graph/test/javascript/expected/70-bare-specifier-to-project-source.diag create mode 100644 graph/test/javascript/expected/70-bare-specifier-to-project-source.edges create mode 100644 graph/test/javascript/expected/70-bare-specifier-to-project-source.oracle delete mode 100644 graph/test/javascript/expected/70-jsconfig-path-alias.diag delete mode 100644 graph/test/javascript/expected/70-jsconfig-path-alias.edges delete mode 100644 graph/test/javascript/expected/70-jsconfig-path-alias.oracle delete mode 100644 graph/test/javascript/expected/71-bundler-config-alias.diag delete mode 100644 graph/test/javascript/expected/71-bundler-config-alias.edges delete mode 100644 graph/test/javascript/expected/71-bundler-config-alias.oracle delete mode 100644 graph/test/javascript/expected/71-framework-generated-config-alias.diag delete mode 100644 graph/test/javascript/expected/71-framework-generated-config-alias.edges delete mode 100644 graph/test/javascript/expected/71-framework-generated-config-alias.oracle create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/apps/app/package.json create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/local.ts create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/main.ts create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/apps/app/types/zod.ts create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/package.json create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/package.json create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/feature/flags.ts create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/index.ts create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/sub/index.ts create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/package.json create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/deep.ts create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/index.ts create mode 100644 graph/test/typescript/cases/85-workspace-package-import/src/pnpm-workspace.yaml create mode 100644 graph/test/typescript/expected/85-workspace-package-import.edges create mode 100644 graph/test/typescript/expected/85-workspace-package-import.entries create mode 100644 graph/test/typescript/expected/85-workspace-package-import.fields create mode 100644 graph/test/typescript/expected/85-workspace-package-import.fields-oracle create mode 100644 graph/test/typescript/expected/85-workspace-package-import.oracle create mode 100644 graph/test/typescript/expected/85-workspace-package-import.type-use create mode 100644 graph/test/typescript/expected/85-workspace-package-import.types-oracle create mode 100644 parser/src/parsers/typescript/workspace-packages.ts diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/legacy/app.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/app.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/legacy/app.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/app.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/legacy/lib/extra.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/lib/extra.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/legacy/lib/extra.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/lib/extra.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/legacy/lib/index.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/lib/index.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/legacy/lib/index.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/lib/index.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/legacy/lib/util/trim.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/lib/util/trim.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/legacy/lib/util/trim.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/lib/util/trim.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/legacy/webpack.config.cjs b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/webpack.config.cjs similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/legacy/webpack.config.cjs rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/legacy/webpack.config.cjs diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/mapped/jsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/mapped/jsconfig.json similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/mapped/jsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/mapped/jsconfig.json diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/mapped/own/date.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/mapped/own/date.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/mapped/own/date.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/mapped/own/date.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/mapped/probe.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/mapped/probe.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/mapped/probe.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/mapped/probe.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/package.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/package.json similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/package.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/package.json diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/shared/fmt.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/shared/fmt.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/shared/fmt.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/shared/fmt.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/src/pages/Home.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/src/pages/Home.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/src/pages/Home.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/src/pages/Home.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/src/utils/date.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/src/utils/date.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/src/utils/date.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/src/utils/date.js diff --git a/graph/test/javascript/cases/71-bundler-config-alias/src/vite.config.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/vite.config.js similarity index 100% rename from graph/test/javascript/cases/71-bundler-config-alias/src/vite.config.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/bundler-alias/vite.config.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/jsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/kit/jsconfig.json similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/jsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/kit/jsconfig.json diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/lib/api.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/kit/src/lib/api.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/lib/api.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/kit/src/lib/api.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/routes/page.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/kit/src/routes/page.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/kit/src/routes/page.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/kit/src/routes/page.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/server/api/price.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt/server/api/price.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/server/api/price.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt/server/api/price.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/tsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt/tsconfig.json similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/tsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt/tsconfig.json diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/utils/price.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt/utils/price.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt/utils/price.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt/utils/price.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/pages/index.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt4/app/pages/index.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/pages/index.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt4/app/pages/index.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/utils/date.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt4/app/utils/date.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/app/utils/date.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt4/app/utils/date.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/tsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt4/tsconfig.json similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/nuxt4/tsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/nuxt4/tsconfig.json diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/jsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/other/jsconfig.json similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/other/jsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/other/jsconfig.json diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/probe.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/other/probe.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/other/probe.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/other/probe.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/other/src/lib/helper.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/other/src/lib/helper.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/other/src/lib/helper.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/other/src/lib/helper.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/jsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/own/jsconfig.json similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/own/jsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/own/jsconfig.json diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/probe.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/own/probe.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/own/probe.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/own/probe.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/shared/util.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/own/shared/util.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/own/shared/util.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/own/shared/util.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/own/src/lib/util.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/own/src/lib/util.js similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/own/src/lib/util.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/own/src/lib/util.js diff --git a/graph/test/javascript/cases/71-framework-generated-config-alias/src/package.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/package.json similarity index 100% rename from graph/test/javascript/cases/71-framework-generated-config-alias/src/package.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/framework-alias/package.json diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/app.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/app.js similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/app.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/app.js diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/jsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/jsconfig.json similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/jsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/jsconfig.json diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/lib/check.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/lib/check.js similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/lib/check.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/lib/check.js diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/noalias/jsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/noalias/jsconfig.json similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/noalias/jsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/noalias/jsconfig.json diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/noalias/probe.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/noalias/probe.js similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/noalias/probe.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/noalias/probe.js diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/package.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/package.json similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/package.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/package.json diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/remix/app/models/note.server.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/remix/app/models/note.server.js similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/remix/app/models/note.server.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/remix/app/models/note.server.js diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/remix/app/route.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/remix/app/route.js similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/remix/app/route.js rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/remix/app/route.js diff --git a/graph/test/javascript/cases/70-jsconfig-path-alias/src/remix/tsconfig.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/remix/tsconfig.json similarity index 100% rename from graph/test/javascript/cases/70-jsconfig-path-alias/src/remix/tsconfig.json rename to graph/test/javascript/cases/70-bare-specifier-to-project-source/src/jsconfig-paths/remix/tsconfig.json diff --git a/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/apps/app/package.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/apps/app/package.json new file mode 100644 index 00000000..6eba3270 --- /dev/null +++ b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/apps/app/package.json @@ -0,0 +1 @@ +{ "name": "@ws/app", "type": "module" } diff --git a/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/apps/app/src/main.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/apps/app/src/main.js new file mode 100644 index 00000000..dbef4e0f --- /dev/null +++ b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/apps/app/src/main.js @@ -0,0 +1,13 @@ +import { f, Bus } from '@ws/lib'; +import { g } from '@ws/lib/sub'; +import { deep } from 'ws-tools/deep'; +// Control: a package this repository does not declare stays unresolved. +import { outside } from 'not-in-this-repo'; + +export function run() { + f(); + g(); + deep(); + new Bus().publish('k'); + return outside(); +} diff --git a/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/package.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/package.json new file mode 100644 index 00000000..4388cd84 --- /dev/null +++ b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/package.json @@ -0,0 +1,8 @@ +{ + "name": "@ws/lib", + "main": "./dist/index.js", + "exports": { + ".": { "import": "./dist/index.mjs", "require": "./dist/index.js" }, + "./sub": "./dist/sub/index.js" + } +} diff --git a/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/src/index.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/src/index.js new file mode 100644 index 00000000..b4154a53 --- /dev/null +++ b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/src/index.js @@ -0,0 +1,9 @@ +export function f() { + return 1; +} + +export class Bus { + publish(key) { + return key; + } +} diff --git a/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/src/sub/index.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/src/sub/index.js new file mode 100644 index 00000000..ac3e27ac --- /dev/null +++ b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/lib/src/sub/index.js @@ -0,0 +1,3 @@ +export function g() { + return 2; +} diff --git a/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/tools/package.json b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/tools/package.json new file mode 100644 index 00000000..c1cd873a --- /dev/null +++ b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/tools/package.json @@ -0,0 +1 @@ +{ "name": "ws-tools", "main": "dist/index.js" } diff --git a/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/tools/src/deep.js b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/tools/src/deep.js new file mode 100644 index 00000000..8539777a --- /dev/null +++ b/graph/test/javascript/cases/70-bare-specifier-to-project-source/src/workspace-package/packages/tools/src/deep.js @@ -0,0 +1,3 @@ +export function deep() { + return 3; +} diff --git a/graph/test/javascript/expected/70-bare-specifier-to-project-source.diag b/graph/test/javascript/expected/70-bare-specifier-to-project-source.diag new file mode 100644 index 00000000..fe9810d3 --- /dev/null +++ b/graph/test/javascript/expected/70-bare-specifier-to-project-source.diag @@ -0,0 +1,35 @@ +import_cause bundler-alias/legacy/app.js:4:10 Lib/extra not_staged +import_cause bundler-alias/legacy/app.js:5:10 Loose/extra not_staged +import_cause bundler-alias/legacy/webpack.config.cjs:1:14 path builtin +import_cause bundler-alias/src/pages/Home.js:6:10 @utils/date not_staged +import_cause bundler-alias/vite.config.js:1:10 vite not_staged +import_cause bundler-alias/vite.config.js:2:1 path builtin +import_cause bundler-alias/vite.config.js:3:10 node:url builtin +import_cause bundler-alias/vite.config.js:3:25 node:url builtin +import_cause framework-alias/other/probe.js:2:10 $lib/helper not_staged +import_cause jsconfig-paths/noalias/probe.js:2:10 @/check not_staged +import_cause workspace-package/apps/app/src/main.js:5:10 not-in-this-repo not_staged +package_entry @ws/app . [] DEFAULT_INDEX index.js MISSING_FILE -> - +package_entry @ws/lib . [] MAIN dist/index.js MISSING_FILE -> - +package_entry @ws/lib . [import] EXPORTS dist/index.mjs MISSING_FILE -> - +package_entry @ws/lib . [require] EXPORTS dist/index.js MISSING_FILE -> - +package_entry @ws/lib ./sub [] EXPORTS dist/sub/index.js MISSING_FILE -> - +package_entry bundler-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +package_entry framework-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +package_entry path-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - +package_entry ws-tools . [] MAIN dist/index.js MISSING_FILE -> - +unresolved bundler-alias/legacy/app.js:10:3 FUNCTION_CALL loose callee_untyped +unresolved bundler-alias/legacy/app.js:9:3 FUNCTION_CALL extra callee_untyped +unresolved bundler-alias/legacy/lib/util/trim.js:2:10 METHOD_CALL trim receiver_untyped +unresolved bundler-alias/legacy/webpack.config.cjs:7:13 METHOD_CALL resolve no_target +unresolved bundler-alias/legacy/webpack.config.cjs:8:13 METHOD_CALL join no_target +unresolved bundler-alias/shared/fmt.js:2:10 METHOD_CALL toFixed receiver_untyped +unresolved bundler-alias/src/pages/Home.js:10:3 FUNCTION_CALL scoped callee_untyped +unresolved bundler-alias/src/utils/date.js:2:10 FUNCTION_CALL String no_target +unresolved bundler-alias/vite.config.js:10:18 FUNCTION_CALL fileURLToPath no_target +unresolved bundler-alias/vite.config.js:10:32 CONSTRUCTOR_CALL URL no_target +unresolved bundler-alias/vite.config.js:6:16 FUNCTION_CALL defineConfig callee_untyped +unresolved bundler-alias/vite.config.js:9:12 METHOD_CALL resolve receiver_untyped +unresolved framework-alias/other/probe.js:5:10 FUNCTION_CALL helper callee_untyped +unresolved jsconfig-paths/noalias/probe.js:5:10 FUNCTION_CALL checkA callee_untyped +unresolved workspace-package/apps/app/src/main.js:12:10 FUNCTION_CALL outside callee_untyped diff --git a/graph/test/javascript/expected/70-bare-specifier-to-project-source.edges b/graph/test/javascript/expected/70-bare-specifier-to-project-source.edges new file mode 100644 index 00000000..6dc90ef1 --- /dev/null +++ b/graph/test/javascript/expected/70-bare-specifier-to-project-source.edges @@ -0,0 +1,35 @@ +bundler-alias/legacy/app.js:10:3 FUNCTION_CALL loose -> ambiguous_unknown - +bundler-alias/legacy/app.js:11:10 FUNCTION_CALL trim -> known_edge bundler-alias/legacy/lib/util/trim.js:1:1 trim +bundler-alias/legacy/app.js:8:3 FUNCTION_CALL boot -> known_edge bundler-alias/legacy/lib/index.js:1:1 boot +bundler-alias/legacy/app.js:9:3 FUNCTION_CALL extra -> ambiguous_unknown - +bundler-alias/legacy/lib/util/trim.js:2:10 METHOD_CALL s.trim -> ambiguous_unknown - +bundler-alias/legacy/webpack.config.cjs:7:13 METHOD_CALL path.resolve -> ambient_terminal - +bundler-alias/legacy/webpack.config.cjs:8:13 METHOD_CALL path.join -> ambient_terminal - +bundler-alias/mapped/probe.js:5:10 FUNCTION_CALL fmtDate -> known_edge bundler-alias/mapped/own/date.js:1:1 fmtDate +bundler-alias/shared/fmt.js:2:10 METHOD_CALL n.toFixed -> ambiguous_unknown - +bundler-alias/src/pages/Home.js:10:3 FUNCTION_CALL scoped -> ambiguous_unknown - +bundler-alias/src/pages/Home.js:11:10 FUNCTION_CALL fmtDate -> known_edge bundler-alias/src/utils/date.js:1:1 fmtDate +bundler-alias/src/pages/Home.js:9:3 FUNCTION_CALL fmtMoney -> known_edge bundler-alias/shared/fmt.js:1:1 fmtMoney +bundler-alias/src/utils/date.js:2:10 FUNCTION_CALL String -> ambient_terminal - +bundler-alias/vite.config.js:10:18 FUNCTION_CALL fileURLToPath -> ambient_terminal - +bundler-alias/vite.config.js:10:32 CONSTRUCTOR_CALL URL -> ambient_terminal - +bundler-alias/vite.config.js:6:16 FUNCTION_CALL defineConfig -> ambiguous_unknown - +bundler-alias/vite.config.js:6:16 FUNCTION_CALL defineConfig -> callback_registered bundler-alias/vite.config.js:6:29 +bundler-alias/vite.config.js:9:12 METHOD_CALL path.resolve -> ambient_terminal - +framework-alias/kit/src/routes/page.js:10:10 FUNCTION_CALL post -> known_edge framework-alias/kit/src/lib/api.js:5:1 post +framework-alias/kit/src/routes/page.js:6:10 METHOD_CALL api.get -> known_edge framework-alias/kit/src/lib/api.js:1:1 get +framework-alias/nuxt/server/api/price.js:6:10 FUNCTION_CALL formatPrice -> known_edge framework-alias/nuxt/utils/price.js:1:1 formatPrice +framework-alias/nuxt/server/api/price.js:6:27 FUNCTION_CALL viaAt -> known_edge framework-alias/nuxt/utils/price.js:1:1 formatPrice +framework-alias/nuxt4/app/pages/index.js:5:10 FUNCTION_CALL formatDate -> known_edge framework-alias/nuxt4/app/utils/date.js:1:1 formatDate +framework-alias/other/probe.js:5:10 FUNCTION_CALL helper -> ambiguous_unknown - +framework-alias/own/probe.js:5:10 FUNCTION_CALL pick -> known_edge framework-alias/own/shared/util.js:1:1 pick +jsconfig-paths/app.js:11:10 FUNCTION_CALL checkB -> known_edge jsconfig-paths/lib/check.js:5:1 checkB +jsconfig-paths/app.js:7:10 FUNCTION_CALL checkA -> known_edge jsconfig-paths/lib/check.js:1:1 checkA +jsconfig-paths/noalias/probe.js:5:10 FUNCTION_CALL checkA -> ambiguous_unknown - +jsconfig-paths/remix/app/route.js:5:10 FUNCTION_CALL getNote -> known_edge jsconfig-paths/remix/app/models/note.server.js:1:1 getNote +workspace-package/apps/app/src/main.js:10:3 FUNCTION_CALL deep -> known_edge workspace-package/packages/tools/src/deep.js:1:1 deep +workspace-package/apps/app/src/main.js:11:3 CONSTRUCTOR_CALL Bus -> implicit_constructor - +workspace-package/apps/app/src/main.js:11:3 METHOD_CALL new Bus().publish -> known_edge workspace-package/packages/lib/src/index.js:6:3 publish +workspace-package/apps/app/src/main.js:12:10 FUNCTION_CALL outside -> ambiguous_unknown - +workspace-package/apps/app/src/main.js:8:3 FUNCTION_CALL f -> known_edge workspace-package/packages/lib/src/index.js:1:1 f +workspace-package/apps/app/src/main.js:9:3 FUNCTION_CALL g -> known_edge workspace-package/packages/lib/src/sub/index.js:1:1 g diff --git a/graph/test/javascript/expected/70-bare-specifier-to-project-source.oracle b/graph/test/javascript/expected/70-bare-specifier-to-project-source.oracle new file mode 100644 index 00000000..c5403e62 --- /dev/null +++ b/graph/test/javascript/expected/70-bare-specifier-to-project-source.oracle @@ -0,0 +1,9 @@ +bundler-alias/legacy/webpack.config.cjs:7:13 METHOD_CALL resolve LIB_AMBIENT_OK +bundler-alias/legacy/webpack.config.cjs:8:13 METHOD_CALL join LIB_AMBIENT_OK +bundler-alias/src/utils/date.js:2:10 FUNCTION_CALL String LIB_AMBIENT_OK +bundler-alias/vite.config.js:10:18 FUNCTION_CALL fileURLToPath LIB_AMBIENT_OK +bundler-alias/vite.config.js:10:32 CONSTRUCTOR_CALL URL LIB_AMBIENT_OK +bundler-alias/vite.config.js:6:16 FUNCTION_CALL defineConfig LIB_MISSED +bundler-alias/vite.config.js:9:12 METHOD_CALL resolve LIB_AMBIENT_OK +jsconfig-paths/app.js:11:10 FUNCTION_CALL checkB EXACT jsconfig-paths/lib/check.js:5:1 +# defects: 0 diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.diag b/graph/test/javascript/expected/70-jsconfig-path-alias.diag deleted file mode 100644 index 74d4aa41..00000000 --- a/graph/test/javascript/expected/70-jsconfig-path-alias.diag +++ /dev/null @@ -1,3 +0,0 @@ -import_cause noalias/probe.js:2:10 @/check not_staged -package_entry path-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - -unresolved noalias/probe.js:5:10 FUNCTION_CALL checkA callee_untyped diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.edges b/graph/test/javascript/expected/70-jsconfig-path-alias.edges deleted file mode 100644 index 09a8c77c..00000000 --- a/graph/test/javascript/expected/70-jsconfig-path-alias.edges +++ /dev/null @@ -1,4 +0,0 @@ -app.js:11:10 FUNCTION_CALL checkB -> known_edge lib/check.js:5:1 checkB -app.js:7:10 FUNCTION_CALL checkA -> known_edge lib/check.js:1:1 checkA -noalias/probe.js:5:10 FUNCTION_CALL checkA -> ambiguous_unknown - -remix/app/route.js:5:10 FUNCTION_CALL getNote -> known_edge remix/app/models/note.server.js:1:1 getNote diff --git a/graph/test/javascript/expected/70-jsconfig-path-alias.oracle b/graph/test/javascript/expected/70-jsconfig-path-alias.oracle deleted file mode 100644 index 16df3e53..00000000 --- a/graph/test/javascript/expected/70-jsconfig-path-alias.oracle +++ /dev/null @@ -1,2 +0,0 @@ -app.js:11:10 FUNCTION_CALL checkB EXACT lib/check.js:5:1 -# defects: 0 diff --git a/graph/test/javascript/expected/71-bundler-config-alias.diag b/graph/test/javascript/expected/71-bundler-config-alias.diag deleted file mode 100644 index d46236f8..00000000 --- a/graph/test/javascript/expected/71-bundler-config-alias.diag +++ /dev/null @@ -1,21 +0,0 @@ -import_cause legacy/app.js:4:10 Lib/extra not_staged -import_cause legacy/app.js:5:10 Loose/extra not_staged -import_cause legacy/webpack.config.cjs:1:14 path builtin -import_cause src/pages/Home.js:6:10 @utils/date not_staged -import_cause vite.config.js:1:10 vite not_staged -import_cause vite.config.js:2:1 path builtin -import_cause vite.config.js:3:10 node:url builtin -import_cause vite.config.js:3:25 node:url builtin -package_entry bundler-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - -unresolved legacy/app.js:10:3 FUNCTION_CALL loose callee_untyped -unresolved legacy/app.js:9:3 FUNCTION_CALL extra callee_untyped -unresolved legacy/lib/util/trim.js:2:10 METHOD_CALL trim receiver_untyped -unresolved legacy/webpack.config.cjs:7:13 METHOD_CALL resolve no_target -unresolved legacy/webpack.config.cjs:8:13 METHOD_CALL join no_target -unresolved shared/fmt.js:2:10 METHOD_CALL toFixed receiver_untyped -unresolved src/pages/Home.js:10:3 FUNCTION_CALL scoped callee_untyped -unresolved src/utils/date.js:2:10 FUNCTION_CALL String no_target -unresolved vite.config.js:10:18 FUNCTION_CALL fileURLToPath no_target -unresolved vite.config.js:10:32 CONSTRUCTOR_CALL URL no_target -unresolved vite.config.js:6:16 FUNCTION_CALL defineConfig callee_untyped -unresolved vite.config.js:9:12 METHOD_CALL resolve receiver_untyped diff --git a/graph/test/javascript/expected/71-bundler-config-alias.edges b/graph/test/javascript/expected/71-bundler-config-alias.edges deleted file mode 100644 index db2535ff..00000000 --- a/graph/test/javascript/expected/71-bundler-config-alias.edges +++ /dev/null @@ -1,18 +0,0 @@ -legacy/app.js:10:3 FUNCTION_CALL loose -> ambiguous_unknown - -legacy/app.js:11:10 FUNCTION_CALL trim -> known_edge legacy/lib/util/trim.js:1:1 trim -legacy/app.js:8:3 FUNCTION_CALL boot -> known_edge legacy/lib/index.js:1:1 boot -legacy/app.js:9:3 FUNCTION_CALL extra -> ambiguous_unknown - -legacy/lib/util/trim.js:2:10 METHOD_CALL s.trim -> ambiguous_unknown - -legacy/webpack.config.cjs:7:13 METHOD_CALL path.resolve -> ambient_terminal - -legacy/webpack.config.cjs:8:13 METHOD_CALL path.join -> ambient_terminal - -mapped/probe.js:5:10 FUNCTION_CALL fmtDate -> known_edge mapped/own/date.js:1:1 fmtDate -shared/fmt.js:2:10 METHOD_CALL n.toFixed -> ambiguous_unknown - -src/pages/Home.js:10:3 FUNCTION_CALL scoped -> ambiguous_unknown - -src/pages/Home.js:11:10 FUNCTION_CALL fmtDate -> known_edge src/utils/date.js:1:1 fmtDate -src/pages/Home.js:9:3 FUNCTION_CALL fmtMoney -> known_edge shared/fmt.js:1:1 fmtMoney -src/utils/date.js:2:10 FUNCTION_CALL String -> ambient_terminal - -vite.config.js:10:18 FUNCTION_CALL fileURLToPath -> ambient_terminal - -vite.config.js:10:32 CONSTRUCTOR_CALL URL -> ambient_terminal - -vite.config.js:6:16 FUNCTION_CALL defineConfig -> ambiguous_unknown - -vite.config.js:6:16 FUNCTION_CALL defineConfig -> callback_registered vite.config.js:6:29 -vite.config.js:9:12 METHOD_CALL path.resolve -> ambient_terminal - diff --git a/graph/test/javascript/expected/71-bundler-config-alias.oracle b/graph/test/javascript/expected/71-bundler-config-alias.oracle deleted file mode 100644 index 2d722071..00000000 --- a/graph/test/javascript/expected/71-bundler-config-alias.oracle +++ /dev/null @@ -1,8 +0,0 @@ -legacy/webpack.config.cjs:7:13 METHOD_CALL resolve LIB_AMBIENT_OK -legacy/webpack.config.cjs:8:13 METHOD_CALL join LIB_AMBIENT_OK -src/utils/date.js:2:10 FUNCTION_CALL String LIB_AMBIENT_OK -vite.config.js:10:18 FUNCTION_CALL fileURLToPath LIB_AMBIENT_OK -vite.config.js:10:32 CONSTRUCTOR_CALL URL LIB_AMBIENT_OK -vite.config.js:6:16 FUNCTION_CALL defineConfig LIB_MISSED -vite.config.js:9:12 METHOD_CALL resolve LIB_AMBIENT_OK -# defects: 0 diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.diag b/graph/test/javascript/expected/71-framework-generated-config-alias.diag deleted file mode 100644 index 008e2a73..00000000 --- a/graph/test/javascript/expected/71-framework-generated-config-alias.diag +++ /dev/null @@ -1,3 +0,0 @@ -import_cause other/probe.js:2:10 $lib/helper not_staged -package_entry framework-alias-fixture . [] DEFAULT_INDEX index.js MISSING_FILE -> - -unresolved other/probe.js:5:10 FUNCTION_CALL helper callee_untyped diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.edges b/graph/test/javascript/expected/71-framework-generated-config-alias.edges deleted file mode 100644 index 916bc000..00000000 --- a/graph/test/javascript/expected/71-framework-generated-config-alias.edges +++ /dev/null @@ -1,7 +0,0 @@ -kit/src/routes/page.js:10:10 FUNCTION_CALL post -> known_edge kit/src/lib/api.js:5:1 post -kit/src/routes/page.js:6:10 METHOD_CALL api.get -> known_edge kit/src/lib/api.js:1:1 get -nuxt/server/api/price.js:6:10 FUNCTION_CALL formatPrice -> known_edge nuxt/utils/price.js:1:1 formatPrice -nuxt/server/api/price.js:6:27 FUNCTION_CALL viaAt -> known_edge nuxt/utils/price.js:1:1 formatPrice -nuxt4/app/pages/index.js:5:10 FUNCTION_CALL formatDate -> known_edge nuxt4/app/utils/date.js:1:1 formatDate -other/probe.js:5:10 FUNCTION_CALL helper -> ambiguous_unknown - -own/probe.js:5:10 FUNCTION_CALL pick -> known_edge own/shared/util.js:1:1 pick diff --git a/graph/test/javascript/expected/71-framework-generated-config-alias.oracle b/graph/test/javascript/expected/71-framework-generated-config-alias.oracle deleted file mode 100644 index 8675b52d..00000000 --- a/graph/test/javascript/expected/71-framework-generated-config-alias.oracle +++ /dev/null @@ -1 +0,0 @@ -# defects: 0 diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/package.json b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/package.json new file mode 100644 index 00000000..d23a4a6c --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/package.json @@ -0,0 +1 @@ +{ "name": "@x/app", "private": true, "dependencies": { "@x/lib": "workspace:*", "tools": "workspace:*", "zod": "^3.0.0" } } diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/local.ts b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/local.ts new file mode 100644 index 00000000..e21076ca --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/local.ts @@ -0,0 +1 @@ +export function local(): void {} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/main.ts b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/main.ts new file mode 100644 index 00000000..74be7308 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/src/main.ts @@ -0,0 +1,25 @@ +import { f, Bus, K } from '@x/lib'; +import { g } from '@x/lib/sub'; +import { isOn } from '@x/lib/feature/flags'; +import { tool } from 'tools'; +import { deep } from 'tools/deep'; +// Control: a third-party package no workspace declares stays a library import. +import { z } from 'zod'; + +import { local } from './local'; + +export class S { + constructor(private bus: Bus) {} + + run(): void { + f(); + g(); + this.bus.publish(K); + if (isOn('x')) { + tool(); + } + deep(); + local(); + z.string(); + } +} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/types/zod.ts b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/types/zod.ts new file mode 100644 index 00000000..0e1757b6 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/apps/app/types/zod.ts @@ -0,0 +1,4 @@ +// A third-party package, declared the way its own types would declare it. +declare module 'zod' { + export const z: { string(): unknown }; +} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/package.json b/graph/test/typescript/cases/85-workspace-package-import/src/package.json new file mode 100644 index 00000000..43c63464 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/package.json @@ -0,0 +1 @@ +{ "name": "ws-root", "private": true } diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/package.json b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/package.json new file mode 100644 index 00000000..aa2d2aff --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/package.json @@ -0,0 +1,10 @@ +{ + "name": "@x/lib", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { "types": "./dist/index.d.ts", "default": "./dist/index.js" }, + "./sub": { "types": "./dist/sub/index.d.ts", "default": "./dist/sub/index.js" }, + "./feature/*": { "types": "./dist/feature/*.d.ts", "default": "./dist/feature/*.js" } + } +} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/feature/flags.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/feature/flags.ts new file mode 100644 index 00000000..817e02d5 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/feature/flags.ts @@ -0,0 +1,3 @@ +export function isOn(name: string): boolean { + return name !== ''; +} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/index.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/index.ts new file mode 100644 index 00000000..87c7fa1a --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/index.ts @@ -0,0 +1,7 @@ +export function f(): void {} + +export class Bus { + publish(key: string): void {} +} + +export const K = 'k'; diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/sub/index.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/sub/index.ts new file mode 100644 index 00000000..41f7f92b --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/lib/src/sub/index.ts @@ -0,0 +1 @@ +export function g(): void {} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/package.json b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/package.json new file mode 100644 index 00000000..92a5cc83 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/package.json @@ -0,0 +1 @@ +{ "name": "tools", "main": "dist/index.js" } diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/deep.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/deep.ts new file mode 100644 index 00000000..50bbcc3f --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/deep.ts @@ -0,0 +1 @@ +export function deep(): void {} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/index.ts b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/index.ts new file mode 100644 index 00000000..d53b1d0a --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/packages/tools/src/index.ts @@ -0,0 +1 @@ +export function tool(): void {} diff --git a/graph/test/typescript/cases/85-workspace-package-import/src/pnpm-workspace.yaml b/graph/test/typescript/cases/85-workspace-package-import/src/pnpm-workspace.yaml new file mode 100644 index 00000000..4e708bd3 --- /dev/null +++ b/graph/test/typescript/cases/85-workspace-package-import/src/pnpm-workspace.yaml @@ -0,0 +1,3 @@ +packages: + - 'packages/*' + - 'apps/*' diff --git a/graph/test/typescript/expected/85-workspace-package-import.edges b/graph/test/typescript/expected/85-workspace-package-import.edges new file mode 100644 index 00000000..d8ac1ca7 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.edges @@ -0,0 +1,8 @@ +known_edge FUNCTION_CALL S#run() @L15 -> packages/lib/src/index#f() +known_edge FUNCTION_CALL S#run() @L16 -> packages/lib/src/sub/index#g() +known_edge FUNCTION_CALL S#run() @L18 -> packages/lib/src/feature/flags#isOn(string) +known_edge FUNCTION_CALL S#run() @L19 -> packages/tools/src/index#tool() +known_edge FUNCTION_CALL S#run() @L21 -> packages/tools/src/deep#deep() +known_edge FUNCTION_CALL S#run() @L22 -> apps/app/src/local#local() +known_edge METHOD_CALL S#run() @L17 -> Bus#publish(string) +known_edge METHOD_CALL S#run() @L23 -> apps/app/types/zod#string() diff --git a/graph/test/typescript/expected/85-workspace-package-import.entries b/graph/test/typescript/expected/85-workspace-package-import.entries new file mode 100644 index 00000000..79185187 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.entries @@ -0,0 +1,3 @@ +── entry_point (2) ── + unimported_module apps/app/src/main# main.ts:1 + unimported_module apps/app/types/zod# zod.ts:1 diff --git a/graph/test/typescript/expected/85-workspace-package-import.fields b/graph/test/typescript/expected/85-workspace-package-import.fields new file mode 100644 index 00000000..565a7ee6 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.fields @@ -0,0 +1 @@ +known_edge read S#run() -> S#bus diff --git a/graph/test/typescript/expected/85-workspace-package-import.fields-oracle b/graph/test/typescript/expected/85-workspace-package-import.fields-oracle new file mode 100644 index 00000000..44b1f6d7 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.fields-oracle @@ -0,0 +1,7 @@ +85-workspace-package-import [fields] + precision 1.0000 (1 correct, 0 wrong) + recall 1.0000 (1 of 1 the compiler resolved) + sites 1 resolved 1 (100.0%) + tiers known_edge=1 + access read=1 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/expected/85-workspace-package-import.oracle b/graph/test/typescript/expected/85-workspace-package-import.oracle new file mode 100644 index 00000000..00f772ed --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.oracle @@ -0,0 +1 @@ +oracle=8 engine=8 agree=8 missing=0 (known 0, NEW 0) extra=0 diff --git a/graph/test/typescript/expected/85-workspace-package-import.type-use b/graph/test/typescript/expected/85-workspace-package-import.type-use new file mode 100644 index 00000000..28bd1b65 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.type-use @@ -0,0 +1 @@ +known_edge METHOD_PARAM 0 S [METHOD_PARAM] -> Bus diff --git a/graph/test/typescript/expected/85-workspace-package-import.types-oracle b/graph/test/typescript/expected/85-workspace-package-import.types-oracle new file mode 100644 index 00000000..ef6d2236 --- /dev/null +++ b/graph/test/typescript/expected/85-workspace-package-import.types-oracle @@ -0,0 +1,7 @@ +85-workspace-package-import [types] + precision 1.0000 (1 correct, 0 wrong) + recall 1.0000 (1 of 1 the compiler resolved) + sites 1 resolved 1 (100.0%) + tiers known_edge=1 + contexts METHOD_PARAM=1 + not scored: 0 rows whose target is not a client declaration diff --git a/graph/test/typescript/ground-truth/tsc-program.mjs b/graph/test/typescript/ground-truth/tsc-program.mjs index 907c664c..020f6614 100644 --- a/graph/test/typescript/ground-truth/tsc-program.mjs +++ b/graph/test/typescript/ground-truth/tsc-program.mjs @@ -105,6 +105,32 @@ export function loadProgram(srcDir, libDir, toolName, programDir) { } } + // ── a WORKSPACE PACKAGE below the case root, imported by its name ─────────── + // In a real monorepo the compiler reaches a sibling package through a node_modules + // link and the declarations its build wrote. A case holds neither, so the program is + // told where each named package's source is by the plainest convention there is: the + // package is `/src/index.ts`, and `/x` is `/src/x`. Deliberately not + // the parser's rule (which reads main / types / exports): the two must agree on the + // answer without sharing the reasoning. A case with no nested package.json is unchanged. + const workspacePaths = {}; + (function walk(d) { + for (const e of fs.readdirSync(d, { withFileTypes: true })) { + const p = path.join(d, e.name); + if (e.isDirectory() && e.name !== 'node_modules') { + walk(p); + } else if (e.name === 'package.json' && d !== root) { + const name = JSON.parse(fs.readFileSync(p, 'utf-8')).name; + if (typeof name === 'string' && name !== '') { + workspacePaths[name] = [path.join(d, 'src', 'index.ts')]; + workspacePaths[`${name}/*`] = [path.join(d, 'src', '*'), path.join(d, 'src', '*', 'index.ts')]; + } + } + } + })(root); + if (Object.keys(workspacePaths).length > 0) { + options.paths = { ...workspacePaths, ...(options.paths ?? {}) }; + } + const program = ts.createProgram(files, options); const checker = program.getTypeChecker(); // CALLERS come only from the client. TARGETS may be either, which is what makes the diff --git a/parser/src/enums/typescript/imports/TsImportResolutionKind.ts b/parser/src/enums/typescript/imports/TsImportResolutionKind.ts index d2d32d53..bc36e385 100644 --- a/parser/src/enums/typescript/imports/TsImportResolutionKind.ts +++ b/parser/src/enums/typescript/imports/TsImportResolutionKind.ts @@ -41,6 +41,11 @@ export enum TsImportResolutionKind { NODE_MODULES_SOURCE = 'NODE_MODULES_SOURCE', /** Resolved through a package's `exports` map. */ PACKAGE_EXPORTS = 'PACKAGE_EXPORTS', + /** + * A package this repository declares, imported by its name: bound to the walked + * source its `main` / `types` / `exports` entry is built from. + */ + WORKSPACE_PACKAGE = 'WORKSPACE_PACKAGE', /** Matched a `declare module "x"` in this analysis. No file, and none needed. */ AMBIENT_MODULE = 'AMBIENT_MODULE', /** A Node builtin, with or without the `node:` prefix. */ diff --git a/parser/src/parsers/javascript/extractors/js-fact-extractor.ts b/parser/src/parsers/javascript/extractors/js-fact-extractor.ts index 63933436..b2a41e37 100644 --- a/parser/src/parsers/javascript/extractors/js-fact-extractor.ts +++ b/parser/src/parsers/javascript/extractors/js-fact-extractor.ts @@ -111,6 +111,11 @@ export interface JsFileExtractionOptions { readonly projectModuleHashes: ReadonlyMap; /** Absolute path -> project-relative path, extension stripped. */ readonly toProjectRelative: (absolutePath: string) => string; + /** + * A bare specifier naming a package this repository declares -> the absolute path of + * the walked source module its entry is built from (`WorkspacePackages`). + */ + readonly resolveWorkspaceModule?: (specifier: string) => string | undefined; /** * How `sourceText` parses, when the file's extension cannot say: a `.vue` * component's virtual script is JS or JSX by its `lang`, not by its name. @@ -384,6 +389,7 @@ export function extractJavaScriptFile(options: JsFileExtractionOptions): JsFileF typeHashByNode: declarations.typeHashByNode, moduleInitMethodHash: declarations.moduleInitMethodHash, toProjectRelative: options.toProjectRelative, + resolveWorkspaceModule: options.resolveWorkspaceModule, projectModuleHashes: options.projectModuleHashes, declarationTargetByName, }); diff --git a/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts b/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts index de97820e..50bd20c4 100644 --- a/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts +++ b/parser/src/parsers/javascript/extractors/js-module-edge-extractor.ts @@ -88,6 +88,8 @@ export interface ModuleEdgeExtractionOptions { readonly toProjectRelative: (absolutePath: string) => string; /** Absolute paths the analysis covers, for `RESOLVED_PROJECT`. */ readonly projectModuleHashes: ReadonlyMap; + /** A bare specifier naming a package this repository declares -> its walked source module. */ + readonly resolveWorkspaceModule?: (specifier: string) => string | undefined; /** Declarations by name, so an export can point at what it exports. */ /** * Every declaration under a name, in source order, with its offset. @@ -1214,21 +1216,33 @@ class JsModuleEdgeExtractor { // component in this program is looked up by path. const resolved = resolveIn(mode) ?? (mode === ts.ModuleKind.ESNext ? resolveIn(ts.ModuleKind.CommonJS) : undefined) ?? resolveVueSpecifier(specifier, this.options.absoluteFilePath, this.options.compilerOptions); - if (resolved === undefined) { - return { filePath: '', outcome: JsImportResolutionOutcome.UNRESOLVED_MISSING }; - } // BOTH SIDES CANONICAL. `projectModuleHashes` is keyed by the files the analyzer // walked from a root it has already resolved through its symlinks; the resolver // answers with the real path for a package found under `node_modules` but does NOT // realpath a relative specifier, so the two sides are compared as real paths and the // spelling of the root cannot decide the outcome any more (#795). - const absolute = realPathOfResolved(path.normalize(resolved)); + const absolute = resolved === undefined ? '' : realPathOfResolved(path.normalize(resolved)); if (this.options.projectModuleHashes.has(absolute)) { return { filePath: this.options.toProjectRelative(absolute), outcome: JsImportResolutionOutcome.RESOLVED_PROJECT, }; } + // A package this repository declares, imported by its name, whose entry names build + // output that was not walked (or not built): the walked source it is built from is + // what the import means. Never for a package installed under `node_modules`. + if (!absolute.includes(`${path.sep}node_modules${path.sep}`)) { + const workspace = this.options.resolveWorkspaceModule?.(specifier); + if (workspace !== undefined) { + return { + filePath: this.options.toProjectRelative(workspace), + outcome: JsImportResolutionOutcome.RESOLVED_PROJECT, + }; + } + } + if (resolved === undefined) { + return { filePath: '', outcome: JsImportResolutionOutcome.UNRESOLVED_MISSING }; + } return { filePath: absolute.split(path.sep).join('/'), outcome: JsImportResolutionOutcome.RESOLVED_EXTERNAL, diff --git a/parser/src/parsers/typescript/extractors/ts-fact-extractor.ts b/parser/src/parsers/typescript/extractors/ts-fact-extractor.ts index da67a6b4..9f312e3c 100644 --- a/parser/src/parsers/typescript/extractors/ts-fact-extractor.ts +++ b/parser/src/parsers/typescript/extractors/ts-fact-extractor.ts @@ -36,7 +36,7 @@ import { extractExports } from '@/parsers/typescript/extractors/ts-export-extrac import { TsExpressionExtractor } from '@/parsers/typescript/extractors/ts-expression-extractor'; import { TsExpressionWalker } from '@/parsers/typescript/extractors/ts-expression-walker'; -import { TsImportExtractor } from '@/parsers/typescript/extractors/ts-import-extractor'; +import { TsImportExtractor, WorkspaceModule } from '@/parsers/typescript/extractors/ts-import-extractor'; import { extractModules } from '@/parsers/typescript/extractors/ts-module-extractor'; import { EngineHandoff, @@ -92,6 +92,8 @@ export interface TsFileExtractionOptions { readonly projectModuleHashes: ReadonlyMap; /** Absolute path -> project-relative path, extension stripped. */ readonly toProjectRelative: (absolutePath: string) => string; + /** A bare specifier naming a package this repository declares -> its walked source module. */ + readonly resolveWorkspaceModule?: (specifier: string) => WorkspaceModule | undefined; /** * How `sourceText` parses, when the file's extension cannot say: a `.vue` * component's virtual script is TS or TSX by its `lang`, not by its name. @@ -181,15 +183,18 @@ export function extractTypeScriptFile(options: TsFileExtractionOptions): TsFileF ); const fileName = resolved.resolvedModule?.resolvedFileName ?? resolveVueSpecifier(specifier, options.absoluteFilePath); - if (!fileName) { - return undefined; - } - const absolute = path.normalize(fileName); + const absolute = fileName ? path.normalize(fileName) : ''; const moduleHash = options.projectModuleHashes.get(absolute); - if (!moduleHash) { - return undefined; + if (moduleHash) { + return { moduleHash, relativePath: options.toProjectRelative(absolute) }; } - return { moduleHash, relativePath: options.toProjectRelative(absolute) }; + // `declare module '@scope/lib'` augmenting a package this repository declares. + const workspace = absolute.includes(`${path.sep}node_modules${path.sep}`) + ? undefined + : options.resolveWorkspaceModule?.(specifier); + return workspace === undefined + ? undefined + : { moduleHash: workspace.moduleHash, relativePath: options.toProjectRelative(workspace.absolutePath) }; }, ambientModuleHashes: modules.ambientModuleHashes, }); @@ -203,6 +208,7 @@ export function extractTypeScriptFile(options: TsFileExtractionOptions): TsFileF serviceVersionLinkHash: options.serviceVersionLinkHash, projectModuleHashes: options.projectModuleHashes, toProjectRelative: options.toProjectRelative, + resolveWorkspaceModule: options.resolveWorkspaceModule, }); const importResult = importExtractor.run(); diff --git a/parser/src/parsers/typescript/extractors/ts-import-extractor.ts b/parser/src/parsers/typescript/extractors/ts-import-extractor.ts index 8c7e8952..5536a84f 100644 --- a/parser/src/parsers/typescript/extractors/ts-import-extractor.ts +++ b/parser/src/parsers/typescript/extractors/ts-import-extractor.ts @@ -39,6 +39,17 @@ export interface ImportExtractorOptions { /** Absolute resolved path -> `ts_module` hash, for project-internal targets. */ readonly projectModuleHashes: ReadonlyMap; readonly toProjectRelative: (absolutePath: string) => string; + /** + * The walked source module a bare specifier names when its package is one this + * repository declares (`TsWorkspacePackages`), or `undefined`. + */ + readonly resolveWorkspaceModule?: (specifier: string) => WorkspaceModule | undefined; +} + +export interface WorkspaceModule { + readonly absolutePath: string; + readonly moduleHash: string; + readonly packageName: string; } export interface ImportExtractionResult { @@ -455,6 +466,27 @@ export class TsImportExtractor { packageName: '', }; } + const absolute = module ? path.normalize(module.resolvedFileName) : ''; + const moduleHash = this.options.projectModuleHashes.get(absolute) ?? ''; + const isNodeModules = absolute.includes(`${path.sep}node_modules${path.sep}`); + // A package this repository declares, imported by its name. Its entry names + // build output, so tsc found nothing, or a `dist` file no program walks; the + // source that output is built from is walked, and is what the import means. + // Asked only when tsc landed on no walked module and not inside `node_modules`: + // a real installed package keeps its own resolution. + if (moduleHash === '' && !isNodeModules) { + const workspace = this.options.resolveWorkspaceModule?.(specifier); + if (workspace !== undefined) { + return { + absolutePath: workspace.absolutePath, + relativePath: this.options.toProjectRelative(workspace.absolutePath), + moduleHash: workspace.moduleHash, + kind: TsImportResolutionKind.WORKSPACE_PACKAGE, + extension: path.extname(workspace.absolutePath), + packageName: workspace.packageName, + }; + } + } if (!module) { // `undefined` is an honest answer, not a failure to try. It is also the // right answer for a wildcard ambient specifier like `"*.svg"`, which @@ -468,9 +500,6 @@ export class TsImportExtractor { // is filled here whether resolution succeeded or not. return { ...UNRESOLVED, packageName: packageNameOf(specifier) }; } - const absolute = path.normalize(module.resolvedFileName); - const moduleHash = this.options.projectModuleHashes.get(absolute) ?? ''; - const isNodeModules = absolute.includes(`${path.sep}node_modules${path.sep}`); return { absolutePath: absolute, relativePath: this.options.toProjectRelative(absolute), diff --git a/parser/src/parsers/typescript/ts-package-entry-extractor.ts b/parser/src/parsers/typescript/ts-package-entry-extractor.ts index bda21609..dfa851f0 100644 --- a/parser/src/parsers/typescript/ts-package-entry-extractor.ts +++ b/parser/src/parsers/typescript/ts-package-entry-extractor.ts @@ -70,18 +70,19 @@ function sourceStemsFor(targetPath: string): string[] { * * @returns `[moduleHash, exact]` — `exact` when the target itself is the walked file */ -function sourceModuleFor( +function sourceModuleFor( packageDir: string, targetPath: string, - moduleHashOf: (absolutePath: string) => string | undefined -): [string, boolean] | undefined { + moduleHashOf: (absolutePath: string) => T | undefined, + extensions: readonly string[] = SOURCE_EXTENSIONS +): [T, boolean] | undefined { const exact = moduleHashOf(path.normalize(path.resolve(packageDir, targetPath))); if (exact !== undefined) { return [exact, true]; } for (const stem of sourceStemsFor(targetPath)) { for (const candidate of [stem, `${stem}/index`]) { - for (const extension of SOURCE_EXTENSIONS) { + for (const extension of extensions) { const hash = moduleHashOf(path.normalize(path.resolve(packageDir, candidate + extension))); if (hash !== undefined) { return [hash, false]; @@ -138,6 +139,61 @@ function sourceModulesForPattern( return []; } +/** + * The walked source module a consumer's import of `subpath` (`"."`, `"./sub"`) binds + * to, by the same convention as the entry rows: the package's own entry for that + * subpath, mapped from build output back to source. + * + * `source` and `types` count for `"."` as they do for the rows. A subpath pattern + * substitutes its `*` into the target, as Node does. A package with no `exports` + * publishes every file, so a deep subpath (`"./sub"`, `"./dist/sub"`) is its own + * target. With `exports` present, a subpath it does not list is blocked for Node and + * binds to nothing here either. `extensions` are the source extensions looked for, in + * order: TypeScript's by default, JavaScript's for a JavaScript workspace. + */ +export function sourceModuleForSubpath( + facts: PackageJsonFacts, + subpath: string, + moduleHashOf: (absolutePath: string) => T | undefined, + extensions: readonly string[] = SOURCE_EXTENSIONS +): T | undefined { + const packageDir = path.dirname(facts.path); + const strip = (target: string): string => target.replace(/^\.\//, ''); + const entries = packageEntriesOf(facts); + const targets: string[] = []; + if (subpath === '.' && facts.source !== undefined && facts.source !== '') { + targets.push(strip(facts.source)); + } + for (const entry of entries) { + if (entry.subpath === subpath && entry.targetPath !== '' && !entry.targetPath.includes('*')) { + targets.push(entry.targetPath); + } + } + for (const entry of entries) { + const [before, after, ...more] = entry.subpath.split('*'); + if (after === undefined || more.length > 0 || entry.targetPath === '' + || !subpath.startsWith(before!) || !subpath.endsWith(after) + || subpath.length < before!.length + after.length) { + continue; + } + const match = subpath.slice(before!.length, subpath.length - after.length); + targets.push(entry.targetPath.split('*').join(match)); + } + if (subpath === '.' && facts.types !== undefined && facts.types !== '') { + targets.push(strip(facts.types)); + } + if (subpath !== '.' && facts.exports === undefined) { + targets.push(strip(subpath)); + } + for (const target of targets) { + const found = sourceModuleFor(packageDir, target, moduleHashOf, extensions); + if (found !== undefined) { + return found[0]; + } + } + return undefined; +} + const SOURCE_OF: Record = { [JsPackageEntrySource.MAIN]: TsPackageEntrySource.MAIN, [JsPackageEntrySource.MODULE]: TsPackageEntrySource.MODULE, diff --git a/parser/src/parsers/typescript/workspace-packages.ts b/parser/src/parsers/typescript/workspace-packages.ts new file mode 100644 index 00000000..449f2e4f --- /dev/null +++ b/parser/src/parsers/typescript/workspace-packages.ts @@ -0,0 +1,111 @@ +import * as fs from 'fs'; +import * as path from 'path'; + +import { PackageJsonFacts, PackageJsonResolver } from '@/parsers/javascript/package-json-resolver'; +import { sourceModuleForSubpath } from '@/parsers/typescript/ts-package-entry-extractor'; + +/** + * The packages a repository declares itself, by name: every named `package.json` + * under the walked tree. Shared by the TypeScript and JavaScript front ends. + * + * ## Why import resolution needs this + * + * In a monorepo, app code imports a sibling package by its NAME (`'@scope/lib'`, + * `'@scope/lib/sub'`), and that package's `main` / `types` / `exports` name build + * output (`./dist/index.d.ts`) that is not in the source tree. `ts.resolveModuleName` + * then finds nothing (no `node_modules`), or a `dist` file no program walks (a + * `node_modules` symlink into the repository after a build). Either way the import + * bound to no module, and every function, class and const used across the package + * boundary was matched by name only. + * + * The package is in the repository, though, and its entry names the source it is + * built from by the same convention the entry rows already use + * (`sourceModuleForSubpath`). So a bare specifier whose package is one of these binds + * to that walked source module. + * + * ## What it does not claim + * + * A name two `package.json` files share is ambiguous and binds nothing. A specifier + * tsc resolved into `node_modules` is a real installed package and is never asked + * here, so a dependency that happens to share a name with a fixture stays external. + */ +export class WorkspacePackages { + private constructor(private readonly byName: ReadonlyMap) {} + + /** Every named package at or under `rootDir`, skipping `skipDirectories` and dot directories. */ + static discover(rootDir: string, skipDirectories: ReadonlySet): WorkspacePackages { + const reader = new PackageJsonResolver(); + const byName = new Map(); + const visit = (directory: string): void => { + let entries: fs.Dirent[]; + try { + entries = fs.readdirSync(directory, { withFileTypes: true }); + } catch { + return; + } + for (const entry of entries) { + if (entry.isFile() && entry.name === 'package.json') { + const facts = reader.packageAt(directory); + if (facts !== undefined && facts.name !== '') { + byName.set(facts.name, byName.has(facts.name) ? null : facts); + } + } else if (entry.isDirectory() && !entry.name.startsWith('.') + && !skipDirectories.has(entry.name)) { + visit(path.join(directory, entry.name)); + } + } + }; + visit(rootDir); + const unique = new Map(); + for (const [name, facts] of byName) { + if (facts !== null) { + unique.set(name, facts); + } + } + return new WorkspacePackages(unique); + } + + get size(): number { + return this.byName.size; + } + + /** + * The walked source module a bare specifier names, or `undefined` when its package + * is not one of these or its entry maps to no walked source. + * + * @param moduleHashOf the module hash of an absolute, normalised source path, or + * `undefined` when no program walks it + */ + resolve( + specifier: string, + moduleHashOf: (absolutePath: string) => string | undefined, + extensions?: readonly string[] + ): { readonly absolutePath: string; readonly moduleHash: string; readonly packageName: string } | undefined { + const name = bareSpecifierPackageName(specifier); + const facts = name === '' ? undefined : this.byName.get(name); + if (facts === undefined) { + return undefined; + } + const rest = specifier.slice(name.length); + const subpath = rest === '' ? '.' : `.${rest}`; + const found = sourceModuleForSubpath(facts, subpath, + (absolutePath) => { + const moduleHash = moduleHashOf(absolutePath); + return moduleHash === undefined ? undefined : { absolutePath, moduleHash }; + }, extensions); + return found === undefined ? undefined : { ...found, packageName: name }; + } +} + +/** `@scope/name` of `@scope/name/deep`, `name` of `name/deep`; `''` for anything not a bare package specifier. */ +function bareSpecifierPackageName(specifier: string): string { + if (specifier === '' || specifier.startsWith('.') || specifier.startsWith('/') + || specifier.includes('*') || specifier.includes(':')) { + return ''; + } + const segments = specifier.split('/'); + if (specifier.startsWith('@')) { + return segments.length >= 2 && segments[1] !== '' ? `${segments[0]}/${segments[1]}` : ''; + } + return segments[0] ?? ''; +} diff --git a/parser/src/workflows/javascript/javascript-project-analyzer.ts b/parser/src/workflows/javascript/javascript-project-analyzer.ts index 18e3e6ae..47a90e4a 100644 --- a/parser/src/workflows/javascript/javascript-project-analyzer.ts +++ b/parser/src/workflows/javascript/javascript-project-analyzer.ts @@ -23,6 +23,7 @@ import { import { moduleHashFor } from '@/parsers/javascript/extractors/js-module-extractor'; import { BUNDLER_CONFIG_NAMES, readBundlerAliases } from '@/parsers/javascript/bundler-alias-reader'; import { PackageJsonResolver } from '@/parsers/javascript/package-json-resolver'; +import { WorkspacePackages } from '@/parsers/typescript/workspace-packages'; import { buildOutputDirectoriesNamedBy, extractPackageEntries, @@ -58,6 +59,9 @@ import { scriptTextOf } from '@/utils/vue-sfc'; * its columns. `getCsvHeader` reads no instance state — the header is a * constant list — which is why the prototype can answer without a row. */ +/** Source extensions a workspace package's entry is mapped back to, in the order they are looked for. */ +const JS_SOURCE_EXTENSIONS = ['.js', '.jsx', '.mjs', '.cjs'] as const; + const HEADER_BY_FILE: Readonly> = { [JAVASCRIPT_CSV_FILES.MODULES]: JsModuleRegistry.prototype.getCsvHeader(), [JAVASCRIPT_CSV_FILES.SCOPES]: JsScopeRegistry.prototype.getCsvHeader(), @@ -348,6 +352,20 @@ export class JavaScriptProjectAnalyzer { } const toProjectRelative = (absolutePath: string): string => stripExtension(toRelative(pathAnchor, absolutePath)); + // A sibling package imported by its name binds to the walked source its entry is + // built from, by the same convention as TypeScript's (`WorkspacePackages`). + const workspacePackages = WorkspacePackages.discover(pathAnchor, excludes); + const workspaceResolutions = new Map(); + const resolveWorkspaceModule = (specifier: string): string | undefined => { + if (workspacePackages.size === 0) { + return undefined; + } + if (!workspaceResolutions.has(specifier)) { + workspaceResolutions.set(specifier, workspacePackages.resolve(specifier, + (absolutePath) => projectModuleHashes.get(absolutePath), JS_SOURCE_EXTENSIONS)?.absolutePath); + } + return workspaceResolutions.get(specifier); + }; // Every package this parse touched: each walk root's own `package.json`, and // the governing config of every file. What each exposes is a fact of the @@ -462,6 +480,7 @@ export class JavaScriptProjectAnalyzer { compilerOptions: compilerOptionsFor(governing.moduleSystem, pathAliases.aliasesFor(file)), projectModuleHashes, toProjectRelative, + resolveWorkspaceModule, }); } catch (error) { // An extraction error is a DEFECT, never a decision. Counted apart diff --git a/parser/src/workflows/typescript/typescript-project-analyzer.ts b/parser/src/workflows/typescript/typescript-project-analyzer.ts index ea62a178..447c6fcd 100644 --- a/parser/src/workflows/typescript/typescript-project-analyzer.ts +++ b/parser/src/workflows/typescript/typescript-project-analyzer.ts @@ -27,6 +27,8 @@ import { import { moduleHashFor } from '@/parsers/typescript/extractors/ts-module-extractor'; import { PackageJsonResolver } from '@/parsers/javascript/package-json-resolver'; import { extractTsPackageEntries } from '@/parsers/typescript/ts-package-entry-extractor'; +import { WorkspacePackages } from '@/parsers/typescript/workspace-packages'; +import { WorkspaceModule } from '@/parsers/typescript/extractors/ts-import-extractor'; import { TsConfigResolver } from '@/parsers/typescript/tsconfig-resolver'; import { TsRelationWriter } from './ts-relation-writer'; import { EntityUtils } from '@/utils/entity-utils'; @@ -265,6 +267,34 @@ export class TypeScriptProjectAnalyzer { } const toProjectRelative = (absolutePath: string): string => stripExtension(toRelative(pathAnchor, absolutePath)); + // A sibling package imported by its name binds to the source its entry is built + // from. That source may belong to another program under the same anchor, whose + // module hash is the same pure function of its path; a source file outside the + // anchor or under a skipped directory is walked by no program and binds nothing. + const workspacePackages = this.workspacePackagesAt(pathAnchor); + const sourceModuleHashOf = (absolutePath: string): string | undefined => { + const inProgram = projectModuleHashes.get(absolutePath); + if (inProgram !== undefined) { + return inProgram; + } + const relative = path.relative(pathAnchor, absolutePath); + if (relative.startsWith('..') || path.isAbsolute(relative) || /\.d\.(m|c)?ts$/.test(relative) + || relative.split(path.sep).some((segment) => excludes.has(segment)) + || !fs.existsSync(absolutePath)) { + return undefined; + } + return moduleHashFor(toRelative(pathAnchor, absolutePath), options.baseMservPath, serviceVersionLinkHash); + }; + const workspaceResolutions = new Map(); + const resolveWorkspaceModule = (specifier: string): WorkspaceModule | undefined => { + if (workspacePackages.size === 0) { + return undefined; + } + if (!workspaceResolutions.has(specifier)) { + workspaceResolutions.set(specifier, workspacePackages.resolve(specifier, sourceModuleHashOf)); + } + return workspaceResolutions.get(specifier); + }; // Skips accumulate across the programs one analyzePrograms call drives; a // standalone analyze starts its own list. @@ -332,6 +362,7 @@ export class TypeScriptProjectAnalyzer { packageName: '', projectModuleHashes, toProjectRelative, + resolveWorkspaceModule, }); } catch (error) { // An extraction error is a DEFECT, never a decision. Counted apart from @@ -537,6 +568,18 @@ export class TypeScriptProjectAnalyzer { /** Distinguishes concurrent writes within one process; the pid does the rest. */ private writeSequence = 0; + /** The packages declared under each path anchor, found once for every program under it. */ + private readonly workspacePackages = new Map(); + + private workspacePackagesAt(anchor: string): WorkspacePackages { + let found = this.workspacePackages.get(anchor); + if (found === undefined) { + found = WorkspacePackages.discover(anchor, new Set(TS_SKIP_DIRECTORIES)); + this.workspacePackages.set(anchor, found); + } + return found; + } + private async exportSkippedFilesCsv(outputDir: string): Promise { const header = ['filePath', 'baseMservPath', 'serviceVersionLinkHash', 'reason', 'detail'] .join('\t'); diff --git a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl index 600cf0fe..282818b6 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl +++ b/plugins/axiomcode/skills/axiomcode/scripts/dl/impact.dl @@ -251,6 +251,12 @@ contract(q, b, "it overrides this") :- target(q, "method", m, _), override(b, m) .decl implements_pair(b:symbol, m:symbol) .input implements_pair contract(q, b, "it implements this — the engine records a dispatch candidate here and no override row") :- target(q, "method", m, _), implements_pair(b, m), !value_pair(b, m), !override(b, m), !override(m, b), !target(q, _, b, _). +// …and the other direction, asked of the BASE. Java's override row puts an implementation under "overrides it"; +// a structurally typed language has no such row, so an interface method's own implementations were missing from +// its answer entirely: nothing calls them through it (the dispatch edge runs candidate -> base) and no contract +// named them, though a change to the interface method's signature breaks every one of them. +contract(q, m, "implements it — the engine records a dispatch candidate here and no override row") :- + target(q, "method", b, _), implements_pair(b, m), !value_pair(b, m), !override(b, m), !override(m, b), !target(q, _, m, _). // A FUNCTION STORED IN A FIELD (#1206). `value_pair(b, m)`: m flows into a field whose declared type is the // function type b — `this.getPath = options.getPath ?? getPath`, `fetch: T = (req) => …`. A call through the field // is typed to b, so its caller is a DIRECT user of m: without this row the answer said "directly touches it: diff --git a/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py b/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py index 549f6ebd..f8b33678 100644 --- a/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py +++ b/plugins/axiomcode/skills/axiomcode/scripts/graph_sql.py @@ -171,6 +171,15 @@ def _at(f_, l_): ({r[0] for r in q( f"""SELECT DISTINCT s.display FROM dispatch_candidates dc JOIN symbols s ON s.method_id = dc.base_method_id WHERE dc.candidate_method_id IN ({ph}) AND dc.base_method_id <> dc.candidate_method_id AND s.id NOT IN ({ph}) + AND dc.basis <> 'structural' + AND NOT EXISTS (SELECT 1 FROM overrides o WHERE (o.method_id = dc.base_method_id AND o.overriding_method_id = dc.candidate_method_id) + OR (o.overriding_method_id = dc.base_method_id AND o.method_id = dc.candidate_method_id))""", + ids + ids)} if 'dispatch_candidates' in _tables(con) else set()) | + # …and, asked of the base, the implementations it dispatches to ("implements it") + ({r[0] for r in q( + f"""SELECT DISTINCT s.display FROM dispatch_candidates dc JOIN symbols s ON s.method_id = dc.candidate_method_id + WHERE dc.base_method_id IN ({ph}) AND dc.base_method_id <> dc.candidate_method_id AND s.id NOT IN ({ph}) + AND dc.basis NOT IN ('structural', 'value') AND NOT EXISTS (SELECT 1 FROM overrides o WHERE (o.method_id = dc.base_method_id AND o.overriding_method_id = dc.candidate_method_id) OR (o.overriding_method_id = dc.base_method_id AND o.method_id = dc.candidate_method_id))""", ids + ids)} if 'dispatch_candidates' in _tables(con) else set())) @@ -278,6 +287,7 @@ def _at(f_, l_): if dispatch: contract_ids |= {r[0] for r in q(f"""SELECT DISTINCT dc.base_method_id FROM dispatch_candidates dc WHERE dc.candidate_method_id IN ({ph}) AND dc.base_method_id <> dc.candidate_method_id + AND dc.basis <> 'structural' AND NOT EXISTS (SELECT 1 FROM overrides o WHERE (o.method_id = dc.base_method_id AND o.overriding_method_id = dc.candidate_method_id) OR (o.overriding_method_id = dc.base_method_id AND o.method_id = dc.candidate_method_id))""", ids)} seen_ids -= contract_ids @@ -1755,6 +1765,14 @@ def contract_for_method(q, ids): AND NOT EXISTS (SELECT 1 FROM overrides o WHERE (o.method_id = dc.base_method_id AND o.overriding_method_id = dc.candidate_method_id) OR (o.overriding_method_id = dc.base_method_id AND o.method_id = dc.candidate_method_id))""", *ids): if b not in ids: out.append((b, 'it implements this — the engine records a dispatch candidate here and no override row')) + # …and asked of the BASE, the implementations it dispatches to: an interface method's own implementers had + # no row at all where the engine keeps no override table. `value` pairs are excluded as the rules exclude them. + for (m,) in q(f"""SELECT DISTINCT dc.candidate_method_id FROM dispatch_candidates dc + WHERE dc.base_method_id IN ({ph}) AND dc.base_method_id <> dc.candidate_method_id + AND dc.basis NOT IN ('structural', 'value') + AND NOT EXISTS (SELECT 1 FROM overrides o WHERE (o.method_id = dc.base_method_id AND o.overriding_method_id = dc.candidate_method_id) + OR (o.overriding_method_id = dc.base_method_id AND o.method_id = dc.candidate_method_id))""", *ids): + if m not in ids: out.append((m, 'implements it — the engine records a dispatch candidate here and no override row')) return sorted(set(out)) # a set, for the same reason diff --git a/tests/cases/typescript/dispatch-base-is-a-contract/case.json b/tests/cases/typescript/dispatch-base-is-a-contract/case.json index 0afd462a..4133344c 100644 --- a/tests/cases/typescript/dispatch-base-is-a-contract/case.json +++ b/tests/cases/typescript/dispatch-base-is-a-contract/case.json @@ -15,5 +15,9 @@ {"why": "a class that only matches the interface's SHAPE is a dispatch candidate, not a declared contract: its callers are still reached through the base, but the base is never 'must change - it implements this'", "run": ["impact", "DuckRouter.add"], "want": ["App.mount"], - "avoid": ["it implements this", "must change with it"]} + "avoid": ["it implements this", "must change with it"]}, + {"why": "asked of the BASE, the same contract names its implementations: with no override row an interface method's implementers had no row at all, though a change to its signature breaks each of them. The shape-only DuckRouter stays out, as above", + "run": ["impact", "Router.add"], + "want": ["must change with it (2: bound by a contract", "LinearRouter.add", "TrieRouter.add", "implements it"], + "avoid": ["must change with it (3"]} ]}