@rigour-labs/core 6.2.5 → 6.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/discovery-signals.d.ts +27 -0
- package/dist/discovery-signals.js +96 -0
- package/dist/discovery.d.ts +1 -2
- package/dist/discovery.js +36 -49
- package/dist/discovery.test.js +84 -71
- package/dist/gates/deep-analysis.d.ts +2 -0
- package/dist/gates/deep-analysis.js +5 -1
- package/dist/gates/deep-intent.d.ts +3 -0
- package/dist/gates/deep-intent.js +57 -0
- package/dist/gates/deep-intent.test.d.ts +1 -0
- package/dist/gates/deep-intent.test.js +51 -0
- package/dist/gates/deep-runner.js +1 -0
- package/dist/gates/deprecated-apis-ast.d.ts +8 -0
- package/dist/gates/deprecated-apis-ast.js +25 -0
- package/dist/gates/deprecated-apis-rules-node.js +2 -8
- package/dist/gates/deprecated-apis.js +5 -1
- package/dist/gates/deprecated-apis.test.js +30 -0
- package/dist/gates/frontend-secret-exposure.js +5 -1
- package/dist/gates/frontend-secret-exposure.test.js +20 -0
- package/dist/gates/hallucinated-imports/framework-modules.d.ts +14 -0
- package/dist/gates/hallucinated-imports/framework-modules.js +75 -0
- package/dist/gates/hallucinated-imports/index.js +14 -6
- package/dist/gates/hallucinated-imports/js-resolver.d.ts +16 -12
- package/dist/gates/hallucinated-imports/js-resolver.js +65 -153
- package/dist/gates/hallucinated-imports/ts-path-target.js +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.d.ts +16 -0
- package/dist/gates/hallucinated-imports/tsconfig-paths.js +120 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.d.ts +1 -0
- package/dist/gates/hallucinated-imports-sveltekit.test.js +108 -0
- package/dist/gates/runner.js +5 -0
- package/dist/gates/semantic-bugs.d.ts +23 -0
- package/dist/gates/semantic-bugs.js +48 -0
- package/dist/gates/semantic-bugs.test.d.ts +1 -0
- package/dist/gates/semantic-bugs.test.js +70 -0
- package/dist/gates/side-effect-analysis/index.js +11 -1
- package/dist/gates/side-effect-analysis.test.d.ts +1 -0
- package/dist/gates/side-effect-analysis.test.js +162 -0
- package/dist/gates/side-effect-helpers/pattern-detection.js +35 -7
- package/dist/gates/side-effect-helpers/resource-tracking.js +17 -10
- package/dist/gates/side-effect-helpers/scope-analysis.d.ts +6 -0
- package/dist/gates/side-effect-helpers/scope-analysis.js +20 -0
- package/dist/index.d.ts +5 -1
- package/dist/index.js +4 -1
- package/dist/inference/model-download.test.js +54 -2
- package/dist/inference/model-manager.d.ts +6 -0
- package/dist/inference/model-manager.js +58 -18
- package/dist/inference/sidecar-provider.js +12 -1
- package/dist/inference/types.d.ts +8 -2
- package/dist/inference/types.js +7 -3
- package/dist/semantic/ast.d.ts +29 -0
- package/dist/semantic/ast.js +122 -0
- package/dist/semantic/benchmark.d.ts +16 -0
- package/dist/semantic/benchmark.js +31 -0
- package/dist/semantic/benchmark.test.d.ts +1 -0
- package/dist/semantic/benchmark.test.js +18 -0
- package/dist/semantic/engine.d.ts +8 -0
- package/dist/semantic/engine.js +45 -0
- package/dist/semantic/intent/benchmark.d.ts +31 -0
- package/dist/semantic/intent/benchmark.js +74 -0
- package/dist/semantic/intent/intent-check.d.ts +43 -0
- package/dist/semantic/intent/intent-check.js +72 -0
- package/dist/semantic/intent/intent.test.d.ts +1 -0
- package/dist/semantic/intent/intent.test.js +46 -0
- package/dist/semantic/intent/parallel-reads.d.ts +23 -0
- package/dist/semantic/intent/parallel-reads.js +59 -0
- package/dist/semantic/learn/benchmark.d.ts +20 -0
- package/dist/semantic/learn/benchmark.js +71 -0
- package/dist/semantic/learn/compile.d.ts +4 -0
- package/dist/semantic/learn/compile.js +56 -0
- package/dist/semantic/learn/conditions.d.ts +22 -0
- package/dist/semantic/learn/conditions.js +93 -0
- package/dist/semantic/learn/extract.d.ts +30 -0
- package/dist/semantic/learn/extract.js +122 -0
- package/dist/semantic/learn/git-trees.d.ts +8 -0
- package/dist/semantic/learn/git-trees.js +32 -0
- package/dist/semantic/learn/identity.d.ts +18 -0
- package/dist/semantic/learn/identity.js +97 -0
- package/dist/semantic/learn/learn.d.ts +25 -0
- package/dist/semantic/learn/learn.js +109 -0
- package/dist/semantic/learn/learn.test.d.ts +1 -0
- package/dist/semantic/learn/learn.test.js +67 -0
- package/dist/semantic/learn/store.d.ts +4 -0
- package/dist/semantic/learn/store.js +39 -0
- package/dist/semantic/learn/types.d.ts +468 -0
- package/dist/semantic/learn/types.js +55 -0
- package/dist/semantic/origins.d.ts +42 -0
- package/dist/semantic/origins.js +149 -0
- package/dist/semantic/origins.test.d.ts +1 -0
- package/dist/semantic/origins.test.js +77 -0
- package/dist/semantic/program.d.ts +13 -0
- package/dist/semantic/program.js +62 -0
- package/dist/semantic/rules/credential-redirect.d.ts +2 -0
- package/dist/semantic/rules/credential-redirect.js +209 -0
- package/dist/semantic/rules/degraded-response-cached.d.ts +2 -0
- package/dist/semantic/rules/degraded-response-cached.js +271 -0
- package/dist/semantic/rules/in-memory-aggregation.d.ts +2 -0
- package/dist/semantic/rules/in-memory-aggregation.js +179 -0
- package/dist/semantic/summaries.d.ts +18 -0
- package/dist/semantic/summaries.js +33 -0
- package/dist/semantic/types.d.ts +32 -0
- package/dist/semantic/types.js +1 -0
- package/dist/storage/index.d.ts +1 -0
- package/dist/storage/index.js +1 -0
- package/dist/storage/team-diagnostics.d.ts +22 -0
- package/dist/storage/team-diagnostics.js +58 -0
- package/dist/storage/team-diagnostics.test.d.ts +1 -0
- package/dist/storage/team-diagnostics.test.js +51 -0
- package/dist/storage/team-schema.d.ts +6 -0
- package/dist/storage/team-schema.js +82 -0
- package/dist/storage/team-store.js +25 -72
- package/dist/templates/universal-config.js +4 -0
- package/dist/types/index.d.ts +56 -0
- package/dist/types/index.js +7 -0
- package/package.json +6 -6
|
@@ -22,6 +22,7 @@ import { Gate } from './base.js';
|
|
|
22
22
|
import { FileScanner } from '../utils/scanner.js';
|
|
23
23
|
import { Logger } from '../utils/logger.js';
|
|
24
24
|
import { languageAdapters } from './language-adapters/index.js';
|
|
25
|
+
import { findWithStatements } from './deprecated-apis-ast.js';
|
|
25
26
|
import fs from 'fs-extra';
|
|
26
27
|
import path from 'path';
|
|
27
28
|
import { NODE_DEPRECATED_RULES, WEB_DEPRECATED_RULES, PYTHON_DEPRECATED_RULES, GO_DEPRECATED_RULES, CSHARP_DEPRECATED_RULES, JAVA_DEPRECATED_RULES } from './deprecated-apis-rules.js';
|
|
@@ -81,8 +82,11 @@ export class DeprecatedApisGate extends Gate {
|
|
|
81
82
|
case 'js':
|
|
82
83
|
if (this.config.check_node)
|
|
83
84
|
this.checkNodeDeprecated(content, file, deprecated);
|
|
84
|
-
if (this.config.check_web)
|
|
85
|
+
if (this.config.check_web) {
|
|
85
86
|
this.checkWebDeprecated(content, file, deprecated);
|
|
87
|
+
for (const w of findWithStatements(content, file))
|
|
88
|
+
deprecated.push({ file, ...w });
|
|
89
|
+
}
|
|
86
90
|
break;
|
|
87
91
|
case 'python':
|
|
88
92
|
this.checkPythonDeprecated(content, file, deprecated);
|
|
@@ -286,3 +286,33 @@ class Resource {
|
|
|
286
286
|
expect(failures[0].details).toContain('finalize');
|
|
287
287
|
});
|
|
288
288
|
});
|
|
289
|
+
describe('DeprecatedApisGate — word fragments and non-code text', () => {
|
|
290
|
+
let gate;
|
|
291
|
+
beforeEach(() => {
|
|
292
|
+
gate = new DeprecatedApisGate();
|
|
293
|
+
vi.clearAllMocks();
|
|
294
|
+
});
|
|
295
|
+
async function apisFor(file, source) {
|
|
296
|
+
mockFindFiles.mockResolvedValue([file]);
|
|
297
|
+
mockReadFile.mockResolvedValue(source);
|
|
298
|
+
const failures = await gate.run({ cwd: '/project' });
|
|
299
|
+
return failures.map(f => f.details).join('\n');
|
|
300
|
+
}
|
|
301
|
+
it('does not flag ArrayBuffer or SharedArrayBuffer as the Buffer() constructor', async () => {
|
|
302
|
+
const details = await apisFor('src/lib/ingestion-proxy.ts', 'const empty = new ArrayBuffer(0);\nconst shared = new SharedArrayBuffer(8);\n');
|
|
303
|
+
expect(details).not.toContain('Buffer() constructor');
|
|
304
|
+
});
|
|
305
|
+
it('still flags a bare Buffer() constructor call', async () => {
|
|
306
|
+
expect(await apisFor('src/handler.js', "const b = Buffer(10);\n")).toContain('Buffer() constructor');
|
|
307
|
+
});
|
|
308
|
+
it('does not flag "with (" inside a trailing comment or a string', async () => {
|
|
309
|
+
const details = await apisFor('src/lib/aliases.ts', [
|
|
310
|
+
"const pattern = compile(source); // match with (optional) prefix",
|
|
311
|
+
"const label = 'works with (legacy) clients';",
|
|
312
|
+
].join('\n'));
|
|
313
|
+
expect(details).not.toContain('with statement');
|
|
314
|
+
});
|
|
315
|
+
it('still flags a real with statement', async () => {
|
|
316
|
+
expect(await apisFor('src/legacy.js', 'with (obj) {\n x = 1;\n}\n')).toContain('with statement');
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -62,8 +62,12 @@ export class FrontendSecretExposureGate extends Gate {
|
|
|
62
62
|
'(^|/)e2e/',
|
|
63
63
|
'(^|/)tests/',
|
|
64
64
|
'(^|/)server/',
|
|
65
|
-
'(^|/)(?:playwright|vitest|vite|jest|next|webpack|eslint|prettier|tailwind|postcss|babel|tsup|rollup)\\.config\\.[cm]?[jt]s$',
|
|
65
|
+
'(^|/)(?:playwright|vitest|vite|jest|next|webpack|eslint|prettier|tailwind|postcss|babel|tsup|rollup|drizzle|svelte|astro|nuxt|remix|wrangler|knip|commitlint|esbuild|turbo)\\.config\\.[cm]?[jt]s$',
|
|
66
|
+
// Any tool config at the repository root runs in Node, never in a browser bundle.
|
|
67
|
+
'^[^/]+\\.config\\.[cm]?[jt]s$',
|
|
66
68
|
'\\.server\\.(?:ts|tsx|js|jsx|mjs|cjs)$',
|
|
69
|
+
// SvelteKit endpoints (`+server.ts`) are server-only.
|
|
70
|
+
'(^|/)\\+server\\.[cm]?[jt]s$',
|
|
67
71
|
...(config.server_path_patterns ?? []),
|
|
68
72
|
],
|
|
69
73
|
allowlist_env_names: config.allowlist_env_names ?? [],
|
|
@@ -108,4 +108,24 @@ describe('FrontendSecretExposureGate', () => {
|
|
|
108
108
|
const failures = await gate.run({ cwd: testDir });
|
|
109
109
|
expect(failures).toHaveLength(0);
|
|
110
110
|
});
|
|
111
|
+
it('treats SvelteKit endpoints and root tool configs as server code', async () => {
|
|
112
|
+
const files = {
|
|
113
|
+
'src/routes/api/sync/+server.ts': 'export const GET = () => fetch(url, { headers: { key: process.env.STRIPE_SECRET_KEY } });\n',
|
|
114
|
+
'drizzle.config.ts': 'export default { dbCredentials: { url: process.env.DATABASE_SECRET_URL } };\n',
|
|
115
|
+
'apps/web/drizzle.config.ts': 'export default { dbCredentials: { url: process.env.DATABASE_SECRET_URL } };\n',
|
|
116
|
+
};
|
|
117
|
+
for (const [rel, body] of Object.entries(files)) {
|
|
118
|
+
fs.mkdirSync(path.dirname(path.join(testDir, rel)), { recursive: true });
|
|
119
|
+
fs.writeFileSync(path.join(testDir, rel), body);
|
|
120
|
+
}
|
|
121
|
+
const failures = await new FrontendSecretExposureGate().run({ cwd: testDir });
|
|
122
|
+
expect(failures).toHaveLength(0);
|
|
123
|
+
});
|
|
124
|
+
it('still flags a secret in a config module inside the client source tree', async () => {
|
|
125
|
+
const filePath = path.join(testDir, 'src/components/site.config.ts');
|
|
126
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
127
|
+
fs.writeFileSync(filePath, 'export const key = process.env.STRIPE_SECRET_KEY;\n');
|
|
128
|
+
const failures = await new FrontendSecretExposureGate().run({ cwd: testDir });
|
|
129
|
+
expect(failures.length).toBeGreaterThan(0);
|
|
130
|
+
});
|
|
111
131
|
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** SvelteKit project root for a file (nearest package.json depending on the kit), cached per directory. */
|
|
2
|
+
export declare class SvelteKitRoots {
|
|
3
|
+
private readonly cwd;
|
|
4
|
+
private cache;
|
|
5
|
+
constructor(cwd: string);
|
|
6
|
+
rootFor(file: string): Promise<string | null>;
|
|
7
|
+
}
|
|
8
|
+
/** `$app/*`, `$env/*`, `$service-worker`, or a route's generated `./$types`. */
|
|
9
|
+
export declare function isSvelteKitProvided(importPath: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* Resolve `$lib` / `$lib/...` to the kit's default `src/lib` folder.
|
|
12
|
+
* Returns null when the import is not a `$lib` import.
|
|
13
|
+
*/
|
|
14
|
+
export declare function resolveSvelteKitLib(importPath: string, kitRoot: string, cwd: string, projectFiles: Set<string>): Promise<boolean | null>;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Framework-provided modules that no package.json or tsconfig declares.
|
|
3
|
+
*
|
|
4
|
+
* SvelteKit supplies `$app/*`, `$env/*` and `$service-worker` as virtual
|
|
5
|
+
* modules and generates each route's `./$types`. Its `$lib` alias lives in
|
|
6
|
+
* `.svelte-kit/tsconfig.json`, which `svelte-kit sync` generates and git
|
|
7
|
+
* ignores, so on a fresh clone or in CI the tsconfig mapping is missing and
|
|
8
|
+
* `$lib/...` would be read as an uninstalled package.
|
|
9
|
+
*/
|
|
10
|
+
import fs from 'fs-extra';
|
|
11
|
+
import path from 'path';
|
|
12
|
+
import { resolveTsPathTarget } from './ts-path-target.js';
|
|
13
|
+
const SVELTEKIT_PACKAGE = '@sveltejs/kit';
|
|
14
|
+
const SVELTEKIT_VIRTUAL = /^(?:\$app|\$env)\/|^\$service-worker$/;
|
|
15
|
+
const GENERATED_TYPES = /^\.{1,2}\/(?:.*\/)?\$types(?:\.d\.ts|\.js)?$/;
|
|
16
|
+
const LIB_ALIAS = /^\$lib(?:\/(.*))?$/;
|
|
17
|
+
/** SvelteKit project root for a file (nearest package.json depending on the kit), cached per directory. */
|
|
18
|
+
export class SvelteKitRoots {
|
|
19
|
+
cwd;
|
|
20
|
+
cache = new Map();
|
|
21
|
+
constructor(cwd) {
|
|
22
|
+
this.cwd = cwd;
|
|
23
|
+
}
|
|
24
|
+
async rootFor(file) {
|
|
25
|
+
const top = path.resolve(this.cwd);
|
|
26
|
+
const visited = [];
|
|
27
|
+
let dir = path.dirname(path.resolve(this.cwd, file));
|
|
28
|
+
let found = null;
|
|
29
|
+
while (dir.startsWith(top)) {
|
|
30
|
+
if (this.cache.has(dir)) {
|
|
31
|
+
found = this.cache.get(dir) ?? null;
|
|
32
|
+
break;
|
|
33
|
+
}
|
|
34
|
+
visited.push(dir);
|
|
35
|
+
if (await declaresKit(path.join(dir, 'package.json'))) {
|
|
36
|
+
found = dir;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
const parent = path.dirname(dir);
|
|
40
|
+
if (parent === dir)
|
|
41
|
+
break;
|
|
42
|
+
dir = parent;
|
|
43
|
+
}
|
|
44
|
+
for (const d of visited)
|
|
45
|
+
this.cache.set(d, found);
|
|
46
|
+
return found;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function declaresKit(packageJsonPath) {
|
|
50
|
+
if (!(await fs.pathExists(packageJsonPath)))
|
|
51
|
+
return false;
|
|
52
|
+
try {
|
|
53
|
+
const pkg = await fs.readJson(packageJsonPath);
|
|
54
|
+
return [pkg?.dependencies, pkg?.devDependencies, pkg?.peerDependencies]
|
|
55
|
+
.some(deps => !!deps && Object.prototype.hasOwnProperty.call(deps, SVELTEKIT_PACKAGE));
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** `$app/*`, `$env/*`, `$service-worker`, or a route's generated `./$types`. */
|
|
62
|
+
export function isSvelteKitProvided(importPath) {
|
|
63
|
+
return SVELTEKIT_VIRTUAL.test(importPath) || GENERATED_TYPES.test(importPath);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Resolve `$lib` / `$lib/...` to the kit's default `src/lib` folder.
|
|
67
|
+
* Returns null when the import is not a `$lib` import.
|
|
68
|
+
*/
|
|
69
|
+
export async function resolveSvelteKitLib(importPath, kitRoot, cwd, projectFiles) {
|
|
70
|
+
const match = importPath.match(LIB_ALIAS);
|
|
71
|
+
if (!match)
|
|
72
|
+
return null;
|
|
73
|
+
const target = match[1] ? path.join('src/lib', match[1]) : 'src/lib';
|
|
74
|
+
return resolveTsPathTarget(kitRoot, target, cwd, projectFiles);
|
|
75
|
+
}
|
|
@@ -24,6 +24,7 @@ import fs from 'fs-extra';
|
|
|
24
24
|
import path from 'path';
|
|
25
25
|
import { checkGoImports, checkRubyImports, checkCSharpImports, checkRustImports, checkJavaKotlinImports } from '../hallucinated-imports-lang.js';
|
|
26
26
|
import { checkJSImports } from './js-resolver.js';
|
|
27
|
+
import { SvelteKitRoots } from './framework-modules.js';
|
|
27
28
|
import { checkPyImports } from './python-resolver.js';
|
|
28
29
|
import { discoverWorkspacePackages, loadPackageJson } from './manifest-discovery.js';
|
|
29
30
|
export class HallucinatedImportsGate extends Gate {
|
|
@@ -47,6 +48,8 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
47
48
|
const failures = [];
|
|
48
49
|
const hallucinated = [];
|
|
49
50
|
const defaultPatterns = ['**/*.{ts,js,tsx,jsx,py,go,rb,cs,rs,java,kt}'];
|
|
51
|
+
// Imports can target component files that are not scanned themselves.
|
|
52
|
+
const resolutionPatterns = ['**/*.{ts,js,tsx,jsx,mjs,cjs,py,go,rb,cs,rs,java,kt,svelte,vue,astro}'];
|
|
50
53
|
const scanPatterns = context.patterns || defaultPatterns;
|
|
51
54
|
const ignore = [...(context.ignore || []), '**/node_modules/**', '**/dist/**', '**/build/**',
|
|
52
55
|
'**/examples/**', '**/studio-dist/**', '**/.next/**', '**/coverage/**',
|
|
@@ -60,11 +63,9 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
60
63
|
});
|
|
61
64
|
const analyzableFiles = files.filter(file => !this.shouldSkipFile(file));
|
|
62
65
|
Logger.info(`Hallucinated Imports: Scanning ${analyzableFiles.length} files`);
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
const resolutionFiles = context.patterns
|
|
66
|
-
? await FileScanner.findFiles({ cwd: context.cwd, patterns: defaultPatterns, ignore })
|
|
67
|
-
: files;
|
|
66
|
+
// Resolve against the whole project, not the scanned set: a scoped
|
|
67
|
+
// review scans changed files only, and imports may target components.
|
|
68
|
+
const resolutionFiles = await FileScanner.findFiles({ cwd: context.cwd, patterns: resolutionPatterns, ignore });
|
|
68
69
|
const projectFiles = new Set(resolutionFiles.map(f => f.replace(/\\/g, '/')));
|
|
69
70
|
const allProjectFiles = projectFiles;
|
|
70
71
|
const packageJson = await loadPackageJson(context.cwd);
|
|
@@ -80,6 +81,13 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
80
81
|
const depCacheByDir = new Map();
|
|
81
82
|
const tsPathCacheByDir = new Map();
|
|
82
83
|
const hasNodeModules = await fs.pathExists(path.join(context.cwd, 'node_modules'));
|
|
84
|
+
const jsContext = {
|
|
85
|
+
cwd: context.cwd, projectFiles, rootDeps, depCacheByDir, hasNodeModules, hallucinated, tsPathCacheByDir,
|
|
86
|
+
kitRoots: new SvelteKitRoots(context.cwd),
|
|
87
|
+
shouldIgnore: (importPath) => this.shouldIgnore(importPath),
|
|
88
|
+
resolveRelativeImport: (fromFile, importPath, files) => this.resolveRelativeImport(fromFile, importPath, files),
|
|
89
|
+
extractPackageName: (importPath) => this.extractPackageName(importPath),
|
|
90
|
+
};
|
|
83
91
|
for (const file of analyzableFiles) {
|
|
84
92
|
try {
|
|
85
93
|
const fullPath = path.join(context.cwd, file);
|
|
@@ -90,7 +98,7 @@ export class HallucinatedImportsGate extends Gate {
|
|
|
90
98
|
continue;
|
|
91
99
|
switch (adapter.id) {
|
|
92
100
|
case 'js':
|
|
93
|
-
await checkJSImports(content, file,
|
|
101
|
+
await checkJSImports(content, file, jsContext);
|
|
94
102
|
break;
|
|
95
103
|
case 'python':
|
|
96
104
|
await checkPyImports(content, file, context.cwd, projectFiles, hallucinated);
|
|
@@ -9,21 +9,26 @@
|
|
|
9
9
|
* - node_modules dependency verification
|
|
10
10
|
*/
|
|
11
11
|
import { HallucinatedImport } from './index.js';
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
12
|
+
import { type SvelteKitRoots } from './framework-modules.js';
|
|
13
|
+
import { type TsPathConfig } from './tsconfig-paths.js';
|
|
14
|
+
/** Everything a JS/TS import check needs besides the file itself. */
|
|
15
|
+
export interface JsImportContext {
|
|
16
|
+
cwd: string;
|
|
17
|
+
projectFiles: Set<string>;
|
|
18
|
+
rootDeps: Set<string>;
|
|
19
|
+
depCacheByDir: Map<string, Set<string>>;
|
|
20
|
+
hasNodeModules: boolean;
|
|
21
|
+
hallucinated: HallucinatedImport[];
|
|
22
|
+
tsPathCacheByDir: Map<string, TsPathConfig | null>;
|
|
23
|
+
kitRoots: SvelteKitRoots;
|
|
24
|
+
shouldIgnore: (importPath: string) => boolean;
|
|
25
|
+
resolveRelativeImport: (fromFile: string, importPath: string, projectFiles: Set<string>) => boolean;
|
|
26
|
+
extractPackageName: (importPath: string) => string;
|
|
22
27
|
}
|
|
23
28
|
/**
|
|
24
29
|
* Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
|
|
25
30
|
*/
|
|
26
|
-
export declare function checkJSImports(content: string, file: string,
|
|
31
|
+
export declare function checkJSImports(content: string, file: string, ctx: JsImportContext): Promise<void>;
|
|
27
32
|
/**
|
|
28
33
|
* Collect all import statements from JavaScript/TypeScript file content.
|
|
29
34
|
* Handles import, export, require(), and dynamic import() statements.
|
|
@@ -42,4 +47,3 @@ export declare function resolveJSDepsForFile(file: string, cwd: string, rootDeps
|
|
|
42
47
|
* Returns true if resolved, false if alias exists but doesn't resolve, null if no alias found.
|
|
43
48
|
*/
|
|
44
49
|
export declare function resolveTsPathAlias(file: string, importPath: string, cwd: string, projectFiles: Set<string>, tsPathCacheByDir: Map<string, TsPathConfig | null>): Promise<boolean | null>;
|
|
45
|
-
export {};
|
|
@@ -13,51 +13,78 @@ import path from 'path';
|
|
|
13
13
|
import ts from 'typescript';
|
|
14
14
|
import { isNodeBuiltin } from '../hallucinated-imports-stdlib.js';
|
|
15
15
|
import { resolveTsPathTarget } from './ts-path-target.js';
|
|
16
|
+
import { isSvelteKitProvided, resolveSvelteKitLib } from './framework-modules.js';
|
|
17
|
+
import { loadTsPathConfig } from './tsconfig-paths.js';
|
|
16
18
|
/**
|
|
17
19
|
* Check JavaScript/TypeScript imports in a source file and add hallucinated findings.
|
|
18
20
|
*/
|
|
19
|
-
export async function checkJSImports(content, file,
|
|
20
|
-
const depsForFile = await resolveJSDepsForFile(file, cwd, rootDeps, depCacheByDir);
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
if (!importPath || shouldIgnore(importPath))
|
|
21
|
+
export async function checkJSImports(content, file, ctx) {
|
|
22
|
+
const depsForFile = await resolveJSDepsForFile(file, ctx.cwd, ctx.rootDeps, ctx.depCacheByDir);
|
|
23
|
+
const kitRoot = await ctx.kitRoots.rootFor(file);
|
|
24
|
+
for (const { importPath, line } of collectJSImportSpecs(content, file)) {
|
|
25
|
+
if (!importPath || ctx.shouldIgnore(importPath))
|
|
24
26
|
continue;
|
|
25
|
-
if (importPath
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
if (kitRoot && isSvelteKitProvided(importPath))
|
|
28
|
+
continue;
|
|
29
|
+
const reason = importPath.startsWith('.')
|
|
30
|
+
? await checkRelativeImport(file, importPath, ctx)
|
|
31
|
+
: await checkBareImport(file, importPath, depsForFile, kitRoot, ctx);
|
|
32
|
+
if (reason) {
|
|
33
|
+
ctx.hallucinated.push({
|
|
34
|
+
file, line, importPath,
|
|
35
|
+
type: importPath.startsWith('.') ? 'relative' : 'package',
|
|
36
|
+
reason,
|
|
37
|
+
});
|
|
33
38
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
async function checkRelativeImport(file, importPath, ctx) {
|
|
42
|
+
if (ctx.resolveRelativeImport(file, importPath, ctx.projectFiles))
|
|
43
|
+
return null;
|
|
44
|
+
const config = await resolveTsPathConfigForFile(file, ctx.cwd, ctx.tsPathCacheByDir);
|
|
45
|
+
if (config?.rootDirs.length && await resolveThroughRootDirs(file, importPath, config.rootDirs, ctx))
|
|
46
|
+
return null;
|
|
47
|
+
return `File not found: ${importPath}`;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* tsconfig `rootDirs`: directories merged into one virtual root, so
|
|
51
|
+
* `./$types` next to `src/routes/+page.ts` can live in
|
|
52
|
+
* `.svelte-kit/types/src/routes/$types.d.ts`. Generated folders are often
|
|
53
|
+
* git-ignored and absent from the file list, so resolution checks the disk.
|
|
54
|
+
*/
|
|
55
|
+
async function resolveThroughRootDirs(file, importPath, rootDirs, ctx) {
|
|
56
|
+
const target = path.resolve(path.dirname(path.resolve(ctx.cwd, file)), importPath);
|
|
57
|
+
for (const root of rootDirs) {
|
|
58
|
+
const relative = path.relative(root, target);
|
|
59
|
+
if (relative.startsWith('..') || path.isAbsolute(relative))
|
|
60
|
+
continue;
|
|
61
|
+
for (const other of rootDirs) {
|
|
62
|
+
if (other !== root && await resolveTsPathTarget(other, relative, ctx.cwd, ctx.projectFiles))
|
|
63
|
+
return true;
|
|
59
64
|
}
|
|
60
65
|
}
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
/** Reason the bare import is hallucinated, or null when it resolves. */
|
|
69
|
+
async function checkBareImport(file, importPath, depsForFile, kitRoot, ctx) {
|
|
70
|
+
const aliasResolution = await resolveTsPathAlias(file, importPath, ctx.cwd, ctx.projectFiles, ctx.tsPathCacheByDir);
|
|
71
|
+
if (aliasResolution === true)
|
|
72
|
+
return null;
|
|
73
|
+
if (aliasResolution === false)
|
|
74
|
+
return `Path alias '${importPath}' does not resolve to a project file`;
|
|
75
|
+
if (kitRoot) {
|
|
76
|
+
const lib = await resolveSvelteKitLib(importPath, kitRoot, ctx.cwd, ctx.projectFiles);
|
|
77
|
+
if (lib === true)
|
|
78
|
+
return null;
|
|
79
|
+
if (lib === false)
|
|
80
|
+
return `Path alias '${importPath}' does not resolve to a file under src/lib`;
|
|
81
|
+
}
|
|
82
|
+
const pkgName = ctx.extractPackageName(importPath);
|
|
83
|
+
if (isNodeBuiltin(pkgName) || depsForFile.has(pkgName))
|
|
84
|
+
return null;
|
|
85
|
+
if (ctx.hasNodeModules && await fs.pathExists(path.join(ctx.cwd, 'node_modules', pkgName)))
|
|
86
|
+
return null;
|
|
87
|
+
return `Package '${pkgName}' not in package.json dependencies`;
|
|
61
88
|
}
|
|
62
89
|
/**
|
|
63
90
|
* Collect all import statements from JavaScript/TypeScript file content.
|
|
@@ -196,118 +223,3 @@ async function resolveTsPathConfigForFile(file, cwd, tsPathCacheByDir) {
|
|
|
196
223
|
}
|
|
197
224
|
return null;
|
|
198
225
|
}
|
|
199
|
-
/**
|
|
200
|
-
* Load TypeScript path aliases from tsconfig.json, jsconfig.json, or tsconfig.base.json.
|
|
201
|
-
* Follows the extends chain to merge path mappings.
|
|
202
|
-
*/
|
|
203
|
-
async function loadTsPathConfig(searchDir) {
|
|
204
|
-
const candidates = ['tsconfig.json', 'jsconfig.json', 'tsconfig.base.json'];
|
|
205
|
-
for (const configName of candidates) {
|
|
206
|
-
const configPath = path.join(searchDir, configName);
|
|
207
|
-
if (!(await fs.pathExists(configPath)))
|
|
208
|
-
continue;
|
|
209
|
-
const mergedPaths = {};
|
|
210
|
-
let baseUrl = '.';
|
|
211
|
-
let resolvedBaseDir = searchDir;
|
|
212
|
-
await resolveExtendsChain(configPath, mergedPaths, (bu) => { baseUrl = bu; }, new Set());
|
|
213
|
-
const parsed = await readLooseJson(configPath);
|
|
214
|
-
const compilerOptions = parsed?.compilerOptions || {};
|
|
215
|
-
if (compilerOptions.paths) {
|
|
216
|
-
for (const [key, value] of Object.entries(compilerOptions.paths)) {
|
|
217
|
-
if (typeof key === 'string' && Array.isArray(value)) {
|
|
218
|
-
mergedPaths[key] = value;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
}
|
|
222
|
-
if (compilerOptions.baseUrl)
|
|
223
|
-
baseUrl = compilerOptions.baseUrl;
|
|
224
|
-
if (Object.keys(mergedPaths).length === 0)
|
|
225
|
-
continue;
|
|
226
|
-
resolvedBaseDir = path.resolve(searchDir, baseUrl);
|
|
227
|
-
const rules = [];
|
|
228
|
-
for (const [key, value] of Object.entries(mergedPaths)) {
|
|
229
|
-
if (typeof key !== 'string' || !Array.isArray(value) || value.length === 0)
|
|
230
|
-
continue;
|
|
231
|
-
const hasWildcard = key.includes('*');
|
|
232
|
-
const [prefix, suffix = ''] = key.split('*');
|
|
233
|
-
const targets = value.filter(v => typeof v === 'string');
|
|
234
|
-
if (targets.length === 0)
|
|
235
|
-
continue;
|
|
236
|
-
rules.push({ key, hasWildcard, prefix, suffix, targets });
|
|
237
|
-
}
|
|
238
|
-
rules.sort((left, right) => {
|
|
239
|
-
if (left.hasWildcard !== right.hasWildcard)
|
|
240
|
-
return left.hasWildcard ? 1 : -1;
|
|
241
|
-
return (right.prefix.length + right.suffix.length)
|
|
242
|
-
- (left.prefix.length + left.suffix.length);
|
|
243
|
-
});
|
|
244
|
-
if (rules.length === 0)
|
|
245
|
-
continue;
|
|
246
|
-
return { baseDir: resolvedBaseDir, rules };
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
/**
|
|
251
|
-
* Recursively follow tsconfig `extends` chain to collect path mappings.
|
|
252
|
-
*/
|
|
253
|
-
async function resolveExtendsChain(configPath, mergedPaths, setBaseUrl, visited) {
|
|
254
|
-
const resolved = path.resolve(configPath);
|
|
255
|
-
if (visited.has(resolved))
|
|
256
|
-
return;
|
|
257
|
-
visited.add(resolved);
|
|
258
|
-
const parsed = await readLooseJson(configPath);
|
|
259
|
-
if (!parsed)
|
|
260
|
-
return;
|
|
261
|
-
if (parsed.extends) {
|
|
262
|
-
const extendsPath = typeof parsed.extends === 'string' ? parsed.extends : null;
|
|
263
|
-
if (extendsPath) {
|
|
264
|
-
let resolvedExtends;
|
|
265
|
-
if (extendsPath.startsWith('.')) {
|
|
266
|
-
resolvedExtends = path.resolve(path.dirname(configPath), extendsPath);
|
|
267
|
-
}
|
|
268
|
-
else {
|
|
269
|
-
resolvedExtends = path.resolve(path.dirname(configPath), 'node_modules', extendsPath);
|
|
270
|
-
}
|
|
271
|
-
if (!resolvedExtends.endsWith('.json')) {
|
|
272
|
-
const withJson = resolvedExtends + '.json';
|
|
273
|
-
if (await fs.pathExists(withJson)) {
|
|
274
|
-
resolvedExtends = withJson;
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
if (await fs.pathExists(resolvedExtends)) {
|
|
278
|
-
await resolveExtendsChain(resolvedExtends, mergedPaths, setBaseUrl, visited);
|
|
279
|
-
}
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
const compilerOptions = parsed.compilerOptions || {};
|
|
283
|
-
if (compilerOptions.baseUrl) {
|
|
284
|
-
setBaseUrl(compilerOptions.baseUrl);
|
|
285
|
-
}
|
|
286
|
-
if (compilerOptions.paths) {
|
|
287
|
-
for (const [key, value] of Object.entries(compilerOptions.paths)) {
|
|
288
|
-
if (typeof key === 'string' && Array.isArray(value)) {
|
|
289
|
-
mergedPaths[key] = value;
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
}
|
|
293
|
-
}
|
|
294
|
-
/**
|
|
295
|
-
* Read JSON file with support for comments and trailing commas.
|
|
296
|
-
*/
|
|
297
|
-
async function readLooseJson(filePath) {
|
|
298
|
-
try {
|
|
299
|
-
const text = await fs.readFile(filePath, 'utf-8');
|
|
300
|
-
try {
|
|
301
|
-
return JSON.parse(text);
|
|
302
|
-
}
|
|
303
|
-
catch {
|
|
304
|
-
const noBlockComments = text.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
305
|
-
const noLineComments = noBlockComments.replace(/(^|\s)\/\/.*$/gm, '$1');
|
|
306
|
-
const noTrailingCommas = noLineComments.replace(/,\s*([}\]])/g, '$1');
|
|
307
|
-
return JSON.parse(noTrailingCommas);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
catch {
|
|
311
|
-
return null;
|
|
312
|
-
}
|
|
313
|
-
}
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
import fs from 'fs-extra';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
const sourceExtensions = ['', '.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.d.ts'];
|
|
4
|
+
/** TypeScript ESM imports name the emitted file: `utils.js` for `utils.ts`. */
|
|
5
|
+
const RUNTIME_TO_SOURCE = {
|
|
6
|
+
'.js': ['.ts', '.tsx', '.d.ts'],
|
|
7
|
+
'.jsx': ['.tsx'],
|
|
8
|
+
'.mjs': ['.mts', '.d.mts'],
|
|
9
|
+
'.cjs': ['.cts', '.d.cts'],
|
|
10
|
+
};
|
|
11
|
+
function sourceSiblings(target) {
|
|
12
|
+
const extension = path.extname(target);
|
|
13
|
+
const sources = RUNTIME_TO_SOURCE[extension];
|
|
14
|
+
if (!sources)
|
|
15
|
+
return [];
|
|
16
|
+
const base = target.slice(0, -extension.length);
|
|
17
|
+
return sources.map(source => base + source);
|
|
18
|
+
}
|
|
4
19
|
export async function resolveTsPathTarget(baseDir, candidatePattern, cwd, projectFiles) {
|
|
5
20
|
const absolute = path.resolve(baseDir, candidatePattern);
|
|
6
21
|
const relative = path.relative(cwd, absolute).replace(/\\/g, '/');
|
|
@@ -10,6 +25,7 @@ export async function resolveTsPathTarget(baseDir, candidatePattern, cwd, projec
|
|
|
10
25
|
const candidates = [
|
|
11
26
|
...sourceExtensions.map(extension => normalized + extension),
|
|
12
27
|
...sourceExtensions.map(extension => `${normalized}/index${extension}`),
|
|
28
|
+
...sourceSiblings(normalized),
|
|
13
29
|
];
|
|
14
30
|
if (candidates.some(candidate => projectFiles.has(candidate)))
|
|
15
31
|
return true;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TsPathRule {
|
|
2
|
+
key: string;
|
|
3
|
+
hasWildcard: boolean;
|
|
4
|
+
prefix: string;
|
|
5
|
+
suffix: string;
|
|
6
|
+
targets: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface TsPathConfig {
|
|
9
|
+
/** Directory `paths` targets are relative to: the effective baseUrl, else the declaring config's folder. */
|
|
10
|
+
baseDir: string;
|
|
11
|
+
rules: TsPathRule[];
|
|
12
|
+
/** Absolute `rootDirs`, each resolved against its declaring config. */
|
|
13
|
+
rootDirs: string[];
|
|
14
|
+
}
|
|
15
|
+
/** The first config in `searchDir` that defines `paths` or `rootDirs` (directly or inherited). */
|
|
16
|
+
export declare function loadTsPathConfig(searchDir: string): Promise<TsPathConfig | null>;
|