@getmikk/core 2.0.13 → 2.0.15
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 +4 -4
- package/package.json +2 -1
- package/src/analysis/index.ts +9 -0
- package/src/analysis/taint-analysis.ts +419 -0
- package/src/analysis/type-flow.ts +247 -0
- package/src/cache/incremental-cache.ts +278 -0
- package/src/cache/index.ts +1 -0
- package/src/contract/contract-generator.ts +31 -3
- package/src/contract/contract-reader.ts +1 -0
- package/src/contract/lock-compiler.ts +125 -12
- package/src/contract/schema.ts +4 -0
- package/src/error-handler.ts +2 -1
- package/src/graph/cluster-detector.ts +2 -4
- package/src/graph/dead-code-detector.ts +303 -117
- package/src/graph/graph-builder.ts +21 -161
- package/src/graph/impact-analyzer.ts +1 -0
- package/src/graph/index.ts +2 -0
- package/src/graph/rich-function-index.ts +1080 -0
- package/src/graph/symbol-table.ts +252 -0
- package/src/hash/hash-store.ts +1 -0
- package/src/index.ts +4 -0
- package/src/parser/base-extractor.ts +19 -0
- package/src/parser/boundary-checker.ts +31 -12
- package/src/parser/error-recovery.ts +647 -0
- package/src/parser/function-body-extractor.ts +248 -0
- package/src/parser/go/go-extractor.ts +249 -676
- package/src/parser/index.ts +138 -295
- package/src/parser/language-registry.ts +57 -0
- package/src/parser/oxc-parser.ts +166 -28
- package/src/parser/oxc-resolver.ts +179 -11
- package/src/parser/parser-constants.ts +1 -0
- package/src/parser/rust/rust-extractor.ts +109 -0
- package/src/parser/tree-sitter/parser.ts +400 -66
- package/src/parser/tree-sitter/queries.ts +106 -10
- package/src/parser/types.ts +20 -1
- package/src/search/bm25.ts +21 -8
- package/src/search/direct-search.ts +472 -0
- package/src/search/embedding-provider.ts +249 -0
- package/src/search/index.ts +12 -0
- package/src/search/semantic-search.ts +435 -0
- package/src/security/index.ts +1 -0
- package/src/security/scanner.ts +342 -0
- package/src/utils/artifact-transaction.ts +1 -0
- package/src/utils/atomic-write.ts +1 -0
- package/src/utils/errors.ts +89 -4
- package/src/utils/fs.ts +150 -65
- package/src/utils/json.ts +1 -0
- package/src/utils/language-registry.ts +96 -5
- package/src/utils/minimatch.ts +49 -6
- package/src/utils/path.ts +26 -0
- package/tests/dead-code.test.ts +3 -2
- package/tests/direct-search.test.ts +435 -0
- package/tests/error-recovery.test.ts +143 -0
- package/tests/fixtures/simple-api/src/index.ts +1 -1
- package/tests/go-parser.test.ts +19 -335
- package/tests/js-parser.test.ts +18 -1089
- package/tests/language-registry-all.test.ts +276 -0
- package/tests/language-registry.test.ts +6 -4
- package/tests/parse-diagnostics.test.ts +9 -96
- package/tests/parser.test.ts +42 -771
- package/tests/polyglot-parser.test.ts +117 -0
- package/tests/rich-function-index.test.ts +703 -0
- package/tests/tree-sitter-parser.test.ts +108 -80
- package/tests/ts-parser.test.ts +8 -8
- package/tests/verification.test.ts +175 -0
- package/src/parser/base-parser.ts +0 -16
- package/src/parser/go/go-parser.ts +0 -43
- package/src/parser/javascript/js-extractor.ts +0 -278
- package/src/parser/javascript/js-parser.ts +0 -101
- package/src/parser/typescript/ts-extractor.ts +0 -447
- package/src/parser/typescript/ts-parser.ts +0 -36
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { describe, it, expect, afterEach, beforeEach } from 'bun:test';
|
|
2
|
+
import { GraphBuilder } from '../src/graph/graph-builder.js';
|
|
3
|
+
import { parseFiles } from '../src/parser/index.js';
|
|
4
|
+
import { OxcResolver } from '../src/parser/oxc-resolver.js';
|
|
5
|
+
import '../src/parser/oxc-parser.js';
|
|
6
|
+
import '../src/parser/go/go-extractor.js';
|
|
7
|
+
import * as fs from 'node:fs';
|
|
8
|
+
import * as path from 'node:path';
|
|
9
|
+
import * as os from 'node:os';
|
|
10
|
+
|
|
11
|
+
async function resolveImports(files: any[], projectRoot: string) {
|
|
12
|
+
const resolver = new OxcResolver(projectRoot);
|
|
13
|
+
return resolver.resolveBatch(files);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
describe('🚀 Mikk Strategy Restoration Verification', () => {
|
|
17
|
+
let tmpDir: string;
|
|
18
|
+
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
tmpDir = path.join(os.tmpdir(), 'mikk-verification-' + Math.random().toString(36).slice(2));
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
afterEach(() => {
|
|
24
|
+
try {
|
|
25
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
26
|
+
} catch {
|
|
27
|
+
// ignore cleanup errors
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
async function setupFiles(files: Record<string, string>) {
|
|
32
|
+
for (const [name, content] of Object.entries(files)) {
|
|
33
|
+
const filePath = path.join(tmpDir, name);
|
|
34
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
35
|
+
fs.writeFileSync(filePath, content);
|
|
36
|
+
}
|
|
37
|
+
const filePaths = Object.keys(files).map(f => path.join(tmpDir, f));
|
|
38
|
+
const readFile = async (p: string) => fs.readFileSync(p, 'utf-8');
|
|
39
|
+
return { filePaths, readFile };
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
it('should resolve aliased imports correctly', async () => {
|
|
43
|
+
const { filePaths, readFile } = await setupFiles({
|
|
44
|
+
'utils.ts': `
|
|
45
|
+
export function validateInput(data) {
|
|
46
|
+
return data !== null && data !== undefined;
|
|
47
|
+
}
|
|
48
|
+
export function sanitize(data) {
|
|
49
|
+
return String(data).trim();
|
|
50
|
+
}
|
|
51
|
+
`,
|
|
52
|
+
'handler.ts': `
|
|
53
|
+
import { validateInput as check, sanitize as clean } from "./utils";
|
|
54
|
+
|
|
55
|
+
export function process(data) {
|
|
56
|
+
if (check(data)) {
|
|
57
|
+
return clean(data);
|
|
58
|
+
}
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
const parsedFiles = await parseFiles(filePaths, tmpDir, readFile);
|
|
65
|
+
const resolvedFiles = await resolveImports(parsedFiles, tmpDir);
|
|
66
|
+
|
|
67
|
+
const builder = new GraphBuilder();
|
|
68
|
+
const graph = builder.build(resolvedFiles);
|
|
69
|
+
|
|
70
|
+
const processNode = Array.from(graph.nodes.values()).find(n => n.name === 'process');
|
|
71
|
+
const checkNode = Array.from(graph.nodes.values()).find(n => n.name === 'validateInput');
|
|
72
|
+
const cleanNode = Array.from(graph.nodes.values()).find(n => n.name === 'sanitize');
|
|
73
|
+
|
|
74
|
+
expect(processNode).toBeDefined();
|
|
75
|
+
expect(checkNode).toBeDefined();
|
|
76
|
+
expect(cleanNode).toBeDefined();
|
|
77
|
+
|
|
78
|
+
const checkEdge = graph.edges.find(e => e.from === processNode!.id && e.to === checkNode!.id);
|
|
79
|
+
const cleanEdge = graph.edges.find(e => e.from === processNode!.id && e.to === cleanNode!.id);
|
|
80
|
+
|
|
81
|
+
expect(checkEdge).toBeDefined();
|
|
82
|
+
expect(cleanEdge).toBeDefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should resolve default import with method call', async () => {
|
|
86
|
+
const { filePaths, readFile } = await setupFiles({
|
|
87
|
+
'jwt.ts': `
|
|
88
|
+
export function verifyToken(token) {
|
|
89
|
+
return token.startsWith('eyJ');
|
|
90
|
+
}
|
|
91
|
+
export function decodeToken(token) {
|
|
92
|
+
return JSON.parse(atob(token.split('.')[1]));
|
|
93
|
+
}
|
|
94
|
+
`,
|
|
95
|
+
'auth.ts': `
|
|
96
|
+
import jwt from "./jwt";
|
|
97
|
+
|
|
98
|
+
export function validate(req) {
|
|
99
|
+
return jwt.verifyToken(req.token);
|
|
100
|
+
}
|
|
101
|
+
`
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
const parsedFiles = await parseFiles(filePaths, tmpDir, readFile);
|
|
105
|
+
const resolvedFiles = await resolveImports(parsedFiles, tmpDir);
|
|
106
|
+
|
|
107
|
+
const builder = new GraphBuilder();
|
|
108
|
+
const graph = builder.build(resolvedFiles);
|
|
109
|
+
|
|
110
|
+
const validateNode = Array.from(graph.nodes.values()).find(n => n.name === 'validate');
|
|
111
|
+
const verifyNode = Array.from(graph.nodes.values()).find(n => n.name === 'verifyToken');
|
|
112
|
+
|
|
113
|
+
expect(validateNode).toBeDefined();
|
|
114
|
+
expect(verifyNode).toBeDefined();
|
|
115
|
+
|
|
116
|
+
const edge = graph.edges.find(e => e.from === validateNode!.id && e.to === verifyNode!.id);
|
|
117
|
+
expect(edge).toBeDefined();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('should resolve cross-file calls using the GlobalSymbolTable', async () => {
|
|
121
|
+
const { filePaths, readFile } = await setupFiles({
|
|
122
|
+
'auth.ts': `
|
|
123
|
+
import { verify } from "./crypto";
|
|
124
|
+
export function login(u) { return verify(u); }
|
|
125
|
+
`,
|
|
126
|
+
'crypto.ts': `
|
|
127
|
+
export function verify(t) { return "ok"; }
|
|
128
|
+
`
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const parsedFiles = await parseFiles(filePaths, tmpDir, readFile);
|
|
132
|
+
const resolvedFiles = await resolveImports(parsedFiles, tmpDir);
|
|
133
|
+
|
|
134
|
+
const builder = new GraphBuilder();
|
|
135
|
+
const graph = builder.build(resolvedFiles);
|
|
136
|
+
|
|
137
|
+
const loginNode = Array.from(graph.nodes.values()).find(n => n.name === 'login');
|
|
138
|
+
const verifyNode = Array.from(graph.nodes.values()).find(n => n.name === 'verify');
|
|
139
|
+
|
|
140
|
+
expect(loginNode).toBeDefined();
|
|
141
|
+
expect(verifyNode).toBeDefined();
|
|
142
|
+
|
|
143
|
+
const edges = graph.edges.filter(e => e.from === loginNode!.id && e.to === verifyNode!.id);
|
|
144
|
+
expect(edges.length).toBeGreaterThan(0);
|
|
145
|
+
expect(edges[0].type).toBe('calls');
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
it('should handle taint flow between files', async () => {
|
|
149
|
+
const { filePaths, readFile } = await setupFiles({
|
|
150
|
+
'app.ts': `
|
|
151
|
+
import { DB } from "./db";
|
|
152
|
+
const db = new DB();
|
|
153
|
+
function handle(req) { db.query(req.input); }
|
|
154
|
+
`,
|
|
155
|
+
'db.ts': `
|
|
156
|
+
export class DB { query(q) {} }
|
|
157
|
+
`
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
const parsedFiles = await parseFiles(filePaths, tmpDir, readFile);
|
|
161
|
+
const resolvedFiles = await resolveImports(parsedFiles, tmpDir);
|
|
162
|
+
|
|
163
|
+
const builder = new GraphBuilder();
|
|
164
|
+
const graph = builder.build(resolvedFiles);
|
|
165
|
+
|
|
166
|
+
const handleNode = Array.from(graph.nodes.values()).find(n => n.name === 'handle');
|
|
167
|
+
const queryNode = Array.from(graph.nodes.values()).find(n => n.name === 'DB.query');
|
|
168
|
+
|
|
169
|
+
expect(handleNode).toBeDefined();
|
|
170
|
+
expect(queryNode).toBeDefined();
|
|
171
|
+
|
|
172
|
+
const edge = graph.edges.find(e => e.from === handleNode!.id && e.to === queryNode!.id);
|
|
173
|
+
expect(edge).toBeDefined();
|
|
174
|
+
});
|
|
175
|
+
});
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { ParsedFile, ParsedImport } from './types.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Abstract base class all language parsers extend.
|
|
5
|
-
* Forces consistency — every parser implements the same interface.
|
|
6
|
-
*/
|
|
7
|
-
export abstract class BaseParser {
|
|
8
|
-
/** Given raw file content as a string, return ParsedFile */
|
|
9
|
-
abstract parse(filePath: string, content: string): Promise<ParsedFile>
|
|
10
|
-
|
|
11
|
-
/** Given a list of parsed files, resolve all import paths to absolute project paths */
|
|
12
|
-
abstract resolveImports(files: ParsedFile[], projectRoot: string): Promise<ParsedFile[]>
|
|
13
|
-
|
|
14
|
-
/** Returns which file extensions this parser handles */
|
|
15
|
-
abstract getSupportedExtensions(): string[]
|
|
16
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { BaseParser } from '../base-parser.js'
|
|
2
|
-
import { GoExtractor } from './go-extractor.js'
|
|
3
|
-
import { GoResolver } from './go-resolver.js'
|
|
4
|
-
import { hashContent } from '../../hash/file-hasher.js'
|
|
5
|
-
import type { ParsedFile } from '../types.js'
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* GoParser -- implements BaseParser for .go files.
|
|
9
|
-
* Uses GoExtractor (regex-based) to pull structured data from Go source
|
|
10
|
-
* without requiring the Go toolchain.
|
|
11
|
-
*/
|
|
12
|
-
export class GoParser extends BaseParser {
|
|
13
|
-
async parse(filePath: string, content: string): Promise<ParsedFile> {
|
|
14
|
-
const extractor = new GoExtractor(filePath, content)
|
|
15
|
-
|
|
16
|
-
return {
|
|
17
|
-
path: filePath,
|
|
18
|
-
language: 'go',
|
|
19
|
-
functions: extractor.extractFunctions(),
|
|
20
|
-
classes: extractor.extractClasses(),
|
|
21
|
-
generics: [], // Go type aliases handled as classes/exports
|
|
22
|
-
imports: extractor.extractImports(),
|
|
23
|
-
exports: extractor.extractExports(),
|
|
24
|
-
routes: extractor.extractRoutes(),
|
|
25
|
-
variables: [],
|
|
26
|
-
calls: [],
|
|
27
|
-
hash: hashContent(content),
|
|
28
|
-
parsedAt: Date.now(),
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
async resolveImports(files: ParsedFile[], projectRoot: string): Promise<ParsedFile[]> {
|
|
33
|
-
const resolver = new GoResolver(projectRoot)
|
|
34
|
-
return files.map(file => ({
|
|
35
|
-
...file,
|
|
36
|
-
imports: resolver.resolveAll(file.imports),
|
|
37
|
-
}))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
getSupportedExtensions(): string[] {
|
|
41
|
-
return ['.go']
|
|
42
|
-
}
|
|
43
|
-
}
|
|
@@ -1,278 +0,0 @@
|
|
|
1
|
-
import ts from 'typescript'
|
|
2
|
-
import { TypeScriptExtractor } from '../typescript/ts-extractor.js'
|
|
3
|
-
import { hashContent } from '../../hash/file-hasher.js'
|
|
4
|
-
import type { ParsedFunction, ParsedImport, ParsedExport } from '../types.js'
|
|
5
|
-
|
|
6
|
-
/**
|
|
7
|
-
* JavaScriptExtractor -- extends TypeScriptExtractor to add CommonJS support on top of
|
|
8
|
-
* the TypeScript Compiler API's native JS/JSX parsing.
|
|
9
|
-
*
|
|
10
|
-
* Extra patterns handled:
|
|
11
|
-
* - require() imports: const x = require('./m') / const { a } = require('./m')
|
|
12
|
-
* - module.exports = { foo, bar } / module.exports = function() {}
|
|
13
|
-
* - exports.foo = function() {}
|
|
14
|
-
*
|
|
15
|
-
* All ESM patterns (import/export, arrow functions, classes) are inherited from the
|
|
16
|
-
* TypeScriptExtractor which already handles ScriptKind.JS and ScriptKind.JSX.
|
|
17
|
-
*/
|
|
18
|
-
export class JavaScriptExtractor extends TypeScriptExtractor {
|
|
19
|
-
|
|
20
|
-
// --- Public overrides ------------------------------------------------------
|
|
21
|
-
|
|
22
|
-
/** ESM functions + module.exports-assigned functions */
|
|
23
|
-
override extractFunctions(): ParsedFunction[] {
|
|
24
|
-
const fns = super.extractFunctions()
|
|
25
|
-
// Use id (which includes start line) to avoid false deduplication
|
|
26
|
-
const seen = new Set(fns.map(f => f.id))
|
|
27
|
-
for (const fn of this.extractCommonJsFunctions()) {
|
|
28
|
-
if (!seen.has(fn.id)) { fns.push(fn); seen.add(fn.id) }
|
|
29
|
-
}
|
|
30
|
-
return fns
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
/** ESM imports + CommonJS require() calls */
|
|
34
|
-
override extractImports(): ParsedImport[] {
|
|
35
|
-
const esm = super.extractImports()
|
|
36
|
-
const seen = new Set(esm.map(i => i.source))
|
|
37
|
-
for (const imp of this.extractRequireImports()) {
|
|
38
|
-
if (!seen.has(imp.source)) { esm.push(imp); seen.add(imp.source) }
|
|
39
|
-
}
|
|
40
|
-
return esm
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/** ESM exports + CommonJS module.exports / exports.x */
|
|
44
|
-
override extractExports(): ParsedExport[] {
|
|
45
|
-
const esm = super.extractExports()
|
|
46
|
-
// Index by name; for default exports use type as secondary key to avoid
|
|
47
|
-
// a local function named 'default' from being incorrectly matched.
|
|
48
|
-
const seen = new Map(esm.map(e => [`${e.name}:${e.type}`, true]))
|
|
49
|
-
for (const exp of this.extractCommonJsExports()) {
|
|
50
|
-
const key = `${exp.name}:${exp.type}`
|
|
51
|
-
if (!seen.has(key)) {
|
|
52
|
-
esm.push(exp)
|
|
53
|
-
seen.set(key, true)
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return esm
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
// --- CommonJS: require() ---------------------------------------------------
|
|
60
|
-
|
|
61
|
-
private extractRequireImports(): ParsedImport[] {
|
|
62
|
-
const imports: ParsedImport[] = []
|
|
63
|
-
const walk = (node: ts.Node) => {
|
|
64
|
-
if (
|
|
65
|
-
ts.isCallExpression(node) &&
|
|
66
|
-
ts.isIdentifier(node.expression) &&
|
|
67
|
-
node.expression.text === 'require' &&
|
|
68
|
-
node.arguments.length === 1 &&
|
|
69
|
-
ts.isStringLiteral(node.arguments[0])
|
|
70
|
-
) {
|
|
71
|
-
// Ignore require.resolve(), require.cache etc. — those are property accesses
|
|
72
|
-
// on the result, not on `require` itself: require.resolve() has a
|
|
73
|
-
// PropertyAccessExpression as node.expression, not an Identifier.
|
|
74
|
-
const source = (node.arguments[0] as ts.StringLiteral).text
|
|
75
|
-
const names = this.getRequireBindingNames(node)
|
|
76
|
-
// isDefault = true when binding is a plain identifier (const x = require(...))
|
|
77
|
-
// or when there's no binding at all (require(...) used for side effects).
|
|
78
|
-
// Only destructured object bindings (const { a } = require(...)) are named imports.
|
|
79
|
-
const parent = node.parent
|
|
80
|
-
const isDestructured = parent &&
|
|
81
|
-
ts.isVariableDeclaration(parent) &&
|
|
82
|
-
ts.isObjectBindingPattern(parent.name)
|
|
83
|
-
imports.push({
|
|
84
|
-
source,
|
|
85
|
-
resolvedPath: '',
|
|
86
|
-
names,
|
|
87
|
-
isDefault: !isDestructured,
|
|
88
|
-
isDynamic: false,
|
|
89
|
-
})
|
|
90
|
-
}
|
|
91
|
-
ts.forEachChild(node, walk)
|
|
92
|
-
}
|
|
93
|
-
ts.forEachChild(this.sourceFile, walk)
|
|
94
|
-
return imports
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/** Names extracted from the variable declaration that receives the require() call. */
|
|
98
|
-
private getRequireBindingNames(call: ts.CallExpression): string[] {
|
|
99
|
-
const parent = call.parent
|
|
100
|
-
if (!parent || !ts.isVariableDeclaration(parent)) return []
|
|
101
|
-
// const { a: myA, b } = require('...') → ['a', 'b'] (use the SOURCE name, not the alias)
|
|
102
|
-
// The source name (propertyName) is what the module exports.
|
|
103
|
-
// The local alias (element.name) is only visible in this file.
|
|
104
|
-
if (ts.isObjectBindingPattern(parent.name)) {
|
|
105
|
-
return parent.name.elements
|
|
106
|
-
.filter(e => ts.isIdentifier(e.name))
|
|
107
|
-
.map(e => {
|
|
108
|
-
// If there is a property name (the "a" in "a: myA"), use it.
|
|
109
|
-
// Otherwise the binding uses the same name for both sides.
|
|
110
|
-
if (e.propertyName && ts.isIdentifier(e.propertyName)) {
|
|
111
|
-
return e.propertyName.text
|
|
112
|
-
}
|
|
113
|
-
return (e.name as ts.Identifier).text
|
|
114
|
-
})
|
|
115
|
-
}
|
|
116
|
-
// const x = require('...') → ['x']
|
|
117
|
-
if (ts.isIdentifier(parent.name)) return [parent.name.text]
|
|
118
|
-
return []
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
// --- CommonJS: module.exports / exports.x exports -------------------------
|
|
122
|
-
|
|
123
|
-
private extractCommonJsExports(): ParsedExport[] {
|
|
124
|
-
const result: ParsedExport[] = []
|
|
125
|
-
const fp = this.filePath
|
|
126
|
-
|
|
127
|
-
const walk = (node: ts.Node) => {
|
|
128
|
-
if (
|
|
129
|
-
ts.isExpressionStatement(node) &&
|
|
130
|
-
ts.isBinaryExpression(node.expression) &&
|
|
131
|
-
node.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
132
|
-
) {
|
|
133
|
-
const lhs = node.expression.left
|
|
134
|
-
const rhs = node.expression.right
|
|
135
|
-
|
|
136
|
-
// --- module.exports = ... ------------------------------------
|
|
137
|
-
if (isModuleExports(lhs)) {
|
|
138
|
-
if (ts.isObjectLiteralExpression(rhs)) {
|
|
139
|
-
// module.exports = { foo, bar }
|
|
140
|
-
for (const prop of rhs.properties) {
|
|
141
|
-
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
142
|
-
result.push({ name: prop.name.text, type: 'const', file: fp })
|
|
143
|
-
} else if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
144
|
-
const isFunc = ts.isFunctionExpression(prop.initializer) || ts.isArrowFunction(prop.initializer)
|
|
145
|
-
result.push({ name: prop.name.text, type: isFunc ? 'function' : 'const', file: fp })
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
} else if (ts.isFunctionExpression(rhs) || ts.isArrowFunction(rhs)) {
|
|
149
|
-
// module.exports = function name() {} → use function name or 'default'
|
|
150
|
-
const name = ts.isFunctionExpression(rhs) && rhs.name ? rhs.name.text : 'default'
|
|
151
|
-
result.push({ name, type: 'default', file: fp })
|
|
152
|
-
} else if (ts.isClassExpression(rhs) && rhs.name) {
|
|
153
|
-
result.push({ name: rhs.name.text, type: 'class', file: fp })
|
|
154
|
-
} else if (ts.isIdentifier(rhs)) {
|
|
155
|
-
result.push({ name: rhs.text, type: 'default', file: fp })
|
|
156
|
-
} else {
|
|
157
|
-
result.push({ name: 'default', type: 'default', file: fp })
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// --- exports.foo = ... ---------------------------------------
|
|
162
|
-
if (isExportsDotProp(lhs)) {
|
|
163
|
-
const prop = lhs as ts.PropertyAccessExpression
|
|
164
|
-
const isFunc = ts.isFunctionExpression(rhs) || ts.isArrowFunction(rhs)
|
|
165
|
-
result.push({ name: prop.name.text, type: isFunc ? 'function' : 'const', file: fp })
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
// --- module.exports.foo = ... --------------------------------
|
|
169
|
-
if (isModuleExportsDotProp(lhs)) {
|
|
170
|
-
const prop = lhs as ts.PropertyAccessExpression
|
|
171
|
-
const isFunc = ts.isFunctionExpression(rhs) || ts.isArrowFunction(rhs)
|
|
172
|
-
result.push({ name: prop.name.text, type: isFunc ? 'function' : 'const', file: fp })
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
ts.forEachChild(node, walk)
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
ts.forEachChild(this.sourceFile, walk)
|
|
179
|
-
return result
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// --- CommonJS: module.exports / exports.x function bodies -----------------
|
|
183
|
-
|
|
184
|
-
/**
|
|
185
|
-
* Detect functions directly assigned via module.exports or exports.x:
|
|
186
|
-
* module.exports = function handleLogin(req, res) { ... }
|
|
187
|
-
* module.exports = function() { ... } ← name = 'default'
|
|
188
|
-
* exports.createUser = function(data) { ... }
|
|
189
|
-
* exports.createUser = (data) => { ... }
|
|
190
|
-
*/
|
|
191
|
-
private extractCommonJsFunctions(): ParsedFunction[] {
|
|
192
|
-
const result: ParsedFunction[] = []
|
|
193
|
-
|
|
194
|
-
const walk = (node: ts.Node) => {
|
|
195
|
-
if (
|
|
196
|
-
ts.isExpressionStatement(node) &&
|
|
197
|
-
ts.isBinaryExpression(node.expression) &&
|
|
198
|
-
node.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
199
|
-
) {
|
|
200
|
-
const lhs = node.expression.left
|
|
201
|
-
const rhs = node.expression.right
|
|
202
|
-
|
|
203
|
-
if (!ts.isFunctionExpression(rhs) && !ts.isArrowFunction(rhs)) {
|
|
204
|
-
ts.forEachChild(node, walk)
|
|
205
|
-
return
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
let fnName: string | null = null
|
|
209
|
-
if (isModuleExports(lhs)) {
|
|
210
|
-
// module.exports = function name() {} / function() {}
|
|
211
|
-
fnName = ts.isFunctionExpression(rhs) && rhs.name ? rhs.name.text : 'default'
|
|
212
|
-
} else if (isExportsDotProp(lhs)) {
|
|
213
|
-
// exports.foo = function() {}
|
|
214
|
-
fnName = (lhs as ts.PropertyAccessExpression).name.text
|
|
215
|
-
} else if (isModuleExportsDotProp(lhs)) {
|
|
216
|
-
// module.exports.foo = function() {}
|
|
217
|
-
fnName = (lhs as ts.PropertyAccessExpression).name.text
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
if (fnName !== null) {
|
|
221
|
-
const startLine = this.getLineNumber(node.getStart())
|
|
222
|
-
const endLine = this.getLineNumber(node.getEnd())
|
|
223
|
-
const isAsync = !!(rhs.modifiers?.some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AsyncKeyword))
|
|
224
|
-
result.push({
|
|
225
|
-
id: `fn:${this.filePath}:${fnName}`,
|
|
226
|
-
name: fnName,
|
|
227
|
-
file: this.filePath,
|
|
228
|
-
startLine,
|
|
229
|
-
endLine,
|
|
230
|
-
params: this.extractParams(rhs.parameters),
|
|
231
|
-
returnType: rhs.type ? rhs.type.getText(this.sourceFile) : 'void',
|
|
232
|
-
isExported: true,
|
|
233
|
-
isAsync,
|
|
234
|
-
calls: this.extractCallsFromNode(rhs),
|
|
235
|
-
hash: hashContent(rhs.getText(this.sourceFile)),
|
|
236
|
-
purpose: this.extractPurpose(node),
|
|
237
|
-
edgeCasesHandled: this.extractEdgeCases(rhs),
|
|
238
|
-
errorHandling: this.extractErrorHandling(rhs),
|
|
239
|
-
detailedLines: this.extractDetailedLines(rhs),
|
|
240
|
-
})
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
ts.forEachChild(node, walk)
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
ts.forEachChild(this.sourceFile, walk)
|
|
247
|
-
return result
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
// --- Helpers -----------------------------------------------------------------
|
|
252
|
-
|
|
253
|
-
/** node is `module.exports` */
|
|
254
|
-
function isModuleExports(node: ts.Node): boolean {
|
|
255
|
-
return (
|
|
256
|
-
ts.isPropertyAccessExpression(node) &&
|
|
257
|
-
ts.isIdentifier(node.expression) &&
|
|
258
|
-
node.expression.text === 'module' &&
|
|
259
|
-
node.name.text === 'exports'
|
|
260
|
-
)
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/** node is `exports.something` */
|
|
264
|
-
function isExportsDotProp(node: ts.Node): boolean {
|
|
265
|
-
return (
|
|
266
|
-
ts.isPropertyAccessExpression(node) &&
|
|
267
|
-
ts.isIdentifier(node.expression) &&
|
|
268
|
-
node.expression.text === 'exports'
|
|
269
|
-
)
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
/** node is `module.exports.something` */
|
|
273
|
-
function isModuleExportsDotProp(node: ts.Node): boolean {
|
|
274
|
-
return (
|
|
275
|
-
ts.isPropertyAccessExpression(node) &&
|
|
276
|
-
isModuleExports(node.expression)
|
|
277
|
-
)
|
|
278
|
-
}
|
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
import * as path from 'node:path'
|
|
2
|
-
import * as fs from 'node:fs'
|
|
3
|
-
import { BaseParser } from '../base-parser.js'
|
|
4
|
-
import { JavaScriptExtractor } from './js-extractor.js'
|
|
5
|
-
import { JavaScriptResolver } from './js-resolver.js'
|
|
6
|
-
import { hashContent } from '../../hash/file-hasher.js'
|
|
7
|
-
import type { ParsedFile } from '../types.js'
|
|
8
|
-
import { MIN_FILES_FOR_COMPLETE_SCAN, parseJsonWithComments } from '../parser-constants.js'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* JavaScriptParser -- implements BaseParser for .js / .mjs / .cjs / .jsx files.
|
|
12
|
-
*
|
|
13
|
-
* Uses the TypeScript Compiler API (ScriptKind.JS / ScriptKind.JSX) which correctly
|
|
14
|
-
* parses JavaScript without type annotations. JavaScriptExtractor extends
|
|
15
|
-
* TypeScriptExtractor and adds CommonJS require() / module.exports support.
|
|
16
|
-
*/
|
|
17
|
-
export class JavaScriptParser extends BaseParser {
|
|
18
|
-
async parse(filePath: string, content: string): Promise<ParsedFile> {
|
|
19
|
-
const extractor = new JavaScriptExtractor(filePath, content)
|
|
20
|
-
|
|
21
|
-
const functions = extractor.extractFunctions()
|
|
22
|
-
const classes = extractor.extractClasses()
|
|
23
|
-
const generics = extractor.extractGenerics()
|
|
24
|
-
const imports = extractor.extractImports()
|
|
25
|
-
const exports = extractor.extractExports()
|
|
26
|
-
const routes = extractor.extractRoutes()
|
|
27
|
-
|
|
28
|
-
// Cross-reference: CJS exports may mark a name exported even when the
|
|
29
|
-
// declaration itself had no `export` keyword.
|
|
30
|
-
//
|
|
31
|
-
// We only mark a symbol as exported when the export list contains an
|
|
32
|
-
// entry with BOTH a matching name AND a non-default type. This prevents
|
|
33
|
-
// `module.exports = function() {}` (which produces name='default', type='default')
|
|
34
|
-
// from accidentally marking an unrelated local function called 'default' as exported.
|
|
35
|
-
const exportedNonDefault = new Set(
|
|
36
|
-
exports.filter(e => e.type !== 'default').map(e => e.name)
|
|
37
|
-
)
|
|
38
|
-
for (const fn of functions) { if (!fn.isExported && exportedNonDefault.has(fn.name)) fn.isExported = true }
|
|
39
|
-
for (const cls of classes) { if (!cls.isExported && exportedNonDefault.has(cls.name)) cls.isExported = true }
|
|
40
|
-
for (const gen of generics) { if (!gen.isExported && exportedNonDefault.has(gen.name)) gen.isExported = true }
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
path: filePath,
|
|
44
|
-
language: 'javascript',
|
|
45
|
-
functions,
|
|
46
|
-
classes,
|
|
47
|
-
generics,
|
|
48
|
-
imports,
|
|
49
|
-
exports,
|
|
50
|
-
routes,
|
|
51
|
-
variables: [],
|
|
52
|
-
calls: [],
|
|
53
|
-
hash: hashContent(content),
|
|
54
|
-
parsedAt: Date.now(),
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
async resolveImports(files: ParsedFile[], projectRoot: string): Promise<ParsedFile[]> {
|
|
59
|
-
const aliases = loadAliases(projectRoot)
|
|
60
|
-
// Only pass the file list when it represents a reasonably complete scan.
|
|
61
|
-
// A sparse list (< MIN_FILES_FOR_COMPLETE_SCAN files) causes valid alias-resolved
|
|
62
|
-
// imports to return '' because the target file is not in the partial list.
|
|
63
|
-
const allFilePaths = files.length >= MIN_FILES_FOR_COMPLETE_SCAN ? files.map(f => f.path) : []
|
|
64
|
-
const resolver = new JavaScriptResolver(projectRoot, aliases)
|
|
65
|
-
return files.map(file => ({
|
|
66
|
-
...file,
|
|
67
|
-
imports: resolver.resolveAll(file.imports, file.path, allFilePaths),
|
|
68
|
-
}))
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
getSupportedExtensions(): string[] {
|
|
72
|
-
return ['.js', '.mjs', '.cjs', '.jsx']
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Load path aliases from jsconfig.json → tsconfig.json → tsconfig.base.json.
|
|
78
|
-
* Strips JSON5 comments via the shared helper and falls back to raw content if parsing fails.
|
|
79
|
-
* Returns {} when no config is found.
|
|
80
|
-
*/
|
|
81
|
-
function loadAliases(projectRoot: string): Record<string, string[]> {
|
|
82
|
-
for (const name of ['jsconfig.json', 'tsconfig.json', 'tsconfig.base.json']) {
|
|
83
|
-
const configPath = path.join(projectRoot, name)
|
|
84
|
-
try {
|
|
85
|
-
const raw = fs.readFileSync(configPath, 'utf-8')
|
|
86
|
-
const config: any = parseJsonWithComments(raw)
|
|
87
|
-
|
|
88
|
-
const options = config.compilerOptions ?? {}
|
|
89
|
-
const rawPaths: Record<string, string[]> = options.paths ?? {}
|
|
90
|
-
if (Object.keys(rawPaths).length === 0) continue
|
|
91
|
-
|
|
92
|
-
const baseUrl = options.baseUrl ?? '.'
|
|
93
|
-
const resolved: Record<string, string[]> = {}
|
|
94
|
-
for (const [alias, targets] of Object.entries(rawPaths)) {
|
|
95
|
-
resolved[alias] = (targets as string[]).map((t: string) => path.posix.join(baseUrl, t))
|
|
96
|
-
}
|
|
97
|
-
return resolved
|
|
98
|
-
} catch { /* config absent or unreadable — try next */ }
|
|
99
|
-
}
|
|
100
|
-
return {}
|
|
101
|
-
}
|