@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.
Files changed (71) hide show
  1. package/README.md +4 -4
  2. package/package.json +2 -1
  3. package/src/analysis/index.ts +9 -0
  4. package/src/analysis/taint-analysis.ts +419 -0
  5. package/src/analysis/type-flow.ts +247 -0
  6. package/src/cache/incremental-cache.ts +278 -0
  7. package/src/cache/index.ts +1 -0
  8. package/src/contract/contract-generator.ts +31 -3
  9. package/src/contract/contract-reader.ts +1 -0
  10. package/src/contract/lock-compiler.ts +125 -12
  11. package/src/contract/schema.ts +4 -0
  12. package/src/error-handler.ts +2 -1
  13. package/src/graph/cluster-detector.ts +2 -4
  14. package/src/graph/dead-code-detector.ts +303 -117
  15. package/src/graph/graph-builder.ts +21 -161
  16. package/src/graph/impact-analyzer.ts +1 -0
  17. package/src/graph/index.ts +2 -0
  18. package/src/graph/rich-function-index.ts +1080 -0
  19. package/src/graph/symbol-table.ts +252 -0
  20. package/src/hash/hash-store.ts +1 -0
  21. package/src/index.ts +4 -0
  22. package/src/parser/base-extractor.ts +19 -0
  23. package/src/parser/boundary-checker.ts +31 -12
  24. package/src/parser/error-recovery.ts +647 -0
  25. package/src/parser/function-body-extractor.ts +248 -0
  26. package/src/parser/go/go-extractor.ts +249 -676
  27. package/src/parser/index.ts +138 -295
  28. package/src/parser/language-registry.ts +57 -0
  29. package/src/parser/oxc-parser.ts +166 -28
  30. package/src/parser/oxc-resolver.ts +179 -11
  31. package/src/parser/parser-constants.ts +1 -0
  32. package/src/parser/rust/rust-extractor.ts +109 -0
  33. package/src/parser/tree-sitter/parser.ts +400 -66
  34. package/src/parser/tree-sitter/queries.ts +106 -10
  35. package/src/parser/types.ts +20 -1
  36. package/src/search/bm25.ts +21 -8
  37. package/src/search/direct-search.ts +472 -0
  38. package/src/search/embedding-provider.ts +249 -0
  39. package/src/search/index.ts +12 -0
  40. package/src/search/semantic-search.ts +435 -0
  41. package/src/security/index.ts +1 -0
  42. package/src/security/scanner.ts +342 -0
  43. package/src/utils/artifact-transaction.ts +1 -0
  44. package/src/utils/atomic-write.ts +1 -0
  45. package/src/utils/errors.ts +89 -4
  46. package/src/utils/fs.ts +150 -65
  47. package/src/utils/json.ts +1 -0
  48. package/src/utils/language-registry.ts +96 -5
  49. package/src/utils/minimatch.ts +49 -6
  50. package/src/utils/path.ts +26 -0
  51. package/tests/dead-code.test.ts +3 -2
  52. package/tests/direct-search.test.ts +435 -0
  53. package/tests/error-recovery.test.ts +143 -0
  54. package/tests/fixtures/simple-api/src/index.ts +1 -1
  55. package/tests/go-parser.test.ts +19 -335
  56. package/tests/js-parser.test.ts +18 -1089
  57. package/tests/language-registry-all.test.ts +276 -0
  58. package/tests/language-registry.test.ts +6 -4
  59. package/tests/parse-diagnostics.test.ts +9 -96
  60. package/tests/parser.test.ts +42 -771
  61. package/tests/polyglot-parser.test.ts +117 -0
  62. package/tests/rich-function-index.test.ts +703 -0
  63. package/tests/tree-sitter-parser.test.ts +108 -80
  64. package/tests/ts-parser.test.ts +8 -8
  65. package/tests/verification.test.ts +175 -0
  66. package/src/parser/base-parser.ts +0 -16
  67. package/src/parser/go/go-parser.ts +0 -43
  68. package/src/parser/javascript/js-extractor.ts +0 -278
  69. package/src/parser/javascript/js-parser.ts +0 -101
  70. package/src/parser/typescript/ts-extractor.ts +0 -447
  71. 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
- }