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"]} ]}