@getmikk/core 1.5.1 → 1.7.0
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 +44 -6
- package/out.log +0 -0
- package/package.json +4 -3
- package/src/contract/adr-manager.ts +75 -0
- package/src/contract/index.ts +2 -0
- package/src/contract/lock-compiler.ts +6 -5
- package/src/contract/lock-reader.ts +40 -26
- package/src/contract/schema.ts +2 -7
- package/src/graph/dead-code-detector.ts +194 -0
- package/src/graph/graph-builder.ts +6 -2
- package/src/graph/impact-analyzer.ts +53 -2
- package/src/graph/index.ts +4 -1
- package/src/graph/types.ts +21 -0
- package/src/index.ts +1 -1
- package/src/parser/go/go-extractor.ts +712 -0
- package/src/parser/go/go-parser.ts +41 -0
- package/src/parser/go/go-resolver.ts +70 -0
- package/src/parser/index.ts +46 -6
- package/src/parser/javascript/js-extractor.ts +262 -0
- package/src/parser/javascript/js-parser.ts +92 -0
- package/src/parser/javascript/js-resolver.ts +83 -0
- package/src/parser/types.ts +1 -1
- package/src/parser/typescript/ts-extractor.ts +93 -42
- package/src/parser/typescript/ts-parser.ts +120 -1
- package/test-output.txt +0 -0
- package/tests/adr-manager.test.ts +97 -0
- package/tests/contract.test.ts +1 -1
- package/tests/dead-code.test.ts +134 -0
- package/tests/go-parser.test.ts +366 -0
- package/tests/helpers.ts +0 -1
- package/tests/impact-classified.test.ts +78 -0
- package/tests/js-parser.test.ts +616 -0
- package/tests/ts-parser.test.ts +93 -0
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
parse(filePath: string, content: string): 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
|
+
hash: hashContent(content),
|
|
26
|
+
parsedAt: Date.now(),
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
|
|
31
|
+
const resolver = new GoResolver(projectRoot)
|
|
32
|
+
return files.map(file => ({
|
|
33
|
+
...file,
|
|
34
|
+
imports: resolver.resolveAll(file.imports),
|
|
35
|
+
}))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
getSupportedExtensions(): string[] {
|
|
39
|
+
return ['.go']
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import * as fs from 'node:fs'
|
|
3
|
+
import type { ParsedImport } from '../types.js'
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* GoResolver — resolves Go import paths to project-relative file paths.
|
|
7
|
+
*
|
|
8
|
+
* Go import paths follow the module path declared in go.mod:
|
|
9
|
+
* module github.com/user/project
|
|
10
|
+
*
|
|
11
|
+
* An import "github.com/user/project/internal/auth" resolves to
|
|
12
|
+
* the directory internal/auth/ relative to the project root.
|
|
13
|
+
*
|
|
14
|
+
* Third-party imports (not matching the module path) are left unresolved.
|
|
15
|
+
*/
|
|
16
|
+
export class GoResolver {
|
|
17
|
+
private modulePath: string
|
|
18
|
+
|
|
19
|
+
constructor(private readonly projectRoot: string) {
|
|
20
|
+
this.modulePath = readModulePath(projectRoot)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Resolve a list of imports for a single file */
|
|
24
|
+
resolveAll(imports: ParsedImport[]): ParsedImport[] {
|
|
25
|
+
return imports.map(imp => this.resolve(imp))
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private resolve(imp: ParsedImport): ParsedImport {
|
|
29
|
+
const src = imp.source
|
|
30
|
+
|
|
31
|
+
// Third-party (doesn't start with our module path) → leave as-is
|
|
32
|
+
if (this.modulePath && !src.startsWith(this.modulePath)) {
|
|
33
|
+
return { ...imp, resolvedPath: '' }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Internal import: strip module path prefix, map to relative dir
|
|
37
|
+
const relPath = this.modulePath
|
|
38
|
+
? src.slice(this.modulePath.length).replace(/^\//, '')
|
|
39
|
+
: src
|
|
40
|
+
|
|
41
|
+
// Try to find the entry file in the directory
|
|
42
|
+
const dirPath = relPath.replace(/\//g, path.sep)
|
|
43
|
+
const candidates = [
|
|
44
|
+
path.join(dirPath, path.basename(dirPath) + '.go'),
|
|
45
|
+
path.join(dirPath, 'index.go'),
|
|
46
|
+
dirPath + '.go',
|
|
47
|
+
]
|
|
48
|
+
|
|
49
|
+
for (const candidate of candidates) {
|
|
50
|
+
if (fs.existsSync(path.join(this.projectRoot, candidate))) {
|
|
51
|
+
return { ...imp, resolvedPath: candidate.replace(/\\/g, '/') }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Fallback: point to directory (graph builder handles directories separately)
|
|
56
|
+
return { ...imp, resolvedPath: relPath.replace(/\\/g, '/') }
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Read module path from go.mod (e.g. "module github.com/user/myapp") */
|
|
61
|
+
function readModulePath(projectRoot: string): string {
|
|
62
|
+
const goModPath = path.join(projectRoot, 'go.mod')
|
|
63
|
+
try {
|
|
64
|
+
const content = fs.readFileSync(goModPath, 'utf-8')
|
|
65
|
+
const m = /^module\s+(\S+)/m.exec(content)
|
|
66
|
+
return m ? m[1] : ''
|
|
67
|
+
} catch {
|
|
68
|
+
return ''
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/parser/index.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import * as path from 'node:path'
|
|
2
2
|
import { BaseParser } from './base-parser.js'
|
|
3
3
|
import { TypeScriptParser } from './typescript/ts-parser.js'
|
|
4
|
+
import { GoParser } from './go/go-parser.js'
|
|
5
|
+
import { JavaScriptParser } from './javascript/js-parser.js'
|
|
4
6
|
import { UnsupportedLanguageError } from '../utils/errors.js'
|
|
5
7
|
import type { ParsedFile } from './types.js'
|
|
6
8
|
|
|
@@ -9,6 +11,12 @@ export { BaseParser } from './base-parser.js'
|
|
|
9
11
|
export { TypeScriptParser } from './typescript/ts-parser.js'
|
|
10
12
|
export { TypeScriptExtractor } from './typescript/ts-extractor.js'
|
|
11
13
|
export { TypeScriptResolver } from './typescript/ts-resolver.js'
|
|
14
|
+
export { GoParser } from './go/go-parser.js'
|
|
15
|
+
export { GoExtractor } from './go/go-extractor.js'
|
|
16
|
+
export { GoResolver } from './go/go-resolver.js'
|
|
17
|
+
export { JavaScriptParser } from './javascript/js-parser.js'
|
|
18
|
+
export { JavaScriptExtractor } from './javascript/js-extractor.js'
|
|
19
|
+
export { JavaScriptResolver } from './javascript/js-resolver.js'
|
|
12
20
|
export { BoundaryChecker } from './boundary-checker.js'
|
|
13
21
|
|
|
14
22
|
/** Get the appropriate parser for a file based on its extension */
|
|
@@ -18,6 +26,13 @@ export function getParser(filePath: string): BaseParser {
|
|
|
18
26
|
case '.ts':
|
|
19
27
|
case '.tsx':
|
|
20
28
|
return new TypeScriptParser()
|
|
29
|
+
case '.js':
|
|
30
|
+
case '.mjs':
|
|
31
|
+
case '.cjs':
|
|
32
|
+
case '.jsx':
|
|
33
|
+
return new JavaScriptParser()
|
|
34
|
+
case '.go':
|
|
35
|
+
return new GoParser()
|
|
21
36
|
default:
|
|
22
37
|
throw new UnsupportedLanguageError(ext)
|
|
23
38
|
}
|
|
@@ -30,17 +45,42 @@ export async function parseFiles(
|
|
|
30
45
|
readFile: (fp: string) => Promise<string>
|
|
31
46
|
): Promise<ParsedFile[]> {
|
|
32
47
|
const tsParser = new TypeScriptParser()
|
|
33
|
-
const
|
|
48
|
+
const jsParser = new JavaScriptParser()
|
|
49
|
+
const goParser = new GoParser()
|
|
50
|
+
const tsFiles: ParsedFile[] = []
|
|
51
|
+
const jsFiles: ParsedFile[] = []
|
|
52
|
+
const goFiles: ParsedFile[] = []
|
|
34
53
|
|
|
35
54
|
for (const fp of filePaths) {
|
|
36
55
|
const ext = path.extname(fp)
|
|
37
56
|
if (ext === '.ts' || ext === '.tsx') {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
57
|
+
try {
|
|
58
|
+
const content = await readFile(path.join(projectRoot, fp))
|
|
59
|
+
tsFiles.push(tsParser.parse(fp, content))
|
|
60
|
+
} catch {
|
|
61
|
+
// Skip unreadable files (permissions, binary, etc.) — don't abort the whole parse
|
|
62
|
+
}
|
|
63
|
+
} else if (ext === '.js' || ext === '.mjs' || ext === '.cjs' || ext === '.jsx') {
|
|
64
|
+
try {
|
|
65
|
+
const content = await readFile(path.join(projectRoot, fp))
|
|
66
|
+
jsFiles.push(jsParser.parse(fp, content))
|
|
67
|
+
} catch {
|
|
68
|
+
// Skip unreadable files
|
|
69
|
+
}
|
|
70
|
+
} else if (ext === '.go') {
|
|
71
|
+
try {
|
|
72
|
+
const content = await readFile(path.join(projectRoot, fp))
|
|
73
|
+
goFiles.push(goParser.parse(fp, content))
|
|
74
|
+
} catch {
|
|
75
|
+
// Skip unreadable files
|
|
76
|
+
}
|
|
41
77
|
}
|
|
42
78
|
}
|
|
43
79
|
|
|
44
|
-
// Resolve
|
|
45
|
-
|
|
80
|
+
// Resolve imports per language after all files of that language are parsed
|
|
81
|
+
const resolvedTs = tsParser.resolveImports(tsFiles, projectRoot)
|
|
82
|
+
const resolvedJs = jsParser.resolveImports(jsFiles, projectRoot)
|
|
83
|
+
const resolvedGo = goParser.resolveImports(goFiles, projectRoot)
|
|
84
|
+
|
|
85
|
+
return [...resolvedTs, ...resolvedJs, ...resolvedGo]
|
|
46
86
|
}
|
|
@@ -0,0 +1,262 @@
|
|
|
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
|
+
const seen = new Set(fns.map(f => f.name))
|
|
26
|
+
for (const fn of this.extractCommonJsFunctions()) {
|
|
27
|
+
if (!seen.has(fn.name)) { fns.push(fn); seen.add(fn.name) }
|
|
28
|
+
}
|
|
29
|
+
return fns
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** ESM imports + CommonJS require() calls */
|
|
33
|
+
override extractImports(): ParsedImport[] {
|
|
34
|
+
const esm = super.extractImports()
|
|
35
|
+
const seen = new Set(esm.map(i => i.source))
|
|
36
|
+
for (const imp of this.extractRequireImports()) {
|
|
37
|
+
if (!seen.has(imp.source)) { esm.push(imp); seen.add(imp.source) }
|
|
38
|
+
}
|
|
39
|
+
return esm
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** ESM exports + CommonJS module.exports / exports.x */
|
|
43
|
+
override extractExports(): ParsedExport[] {
|
|
44
|
+
const esm = super.extractExports()
|
|
45
|
+
const seen = new Set(esm.map(e => e.name))
|
|
46
|
+
for (const exp of this.extractCommonJsExports()) {
|
|
47
|
+
if (!seen.has(exp.name)) { esm.push(exp); seen.add(exp.name) }
|
|
48
|
+
}
|
|
49
|
+
return esm
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ── CommonJS: require() ────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
private extractRequireImports(): ParsedImport[] {
|
|
55
|
+
const imports: ParsedImport[] = []
|
|
56
|
+
const walk = (node: ts.Node) => {
|
|
57
|
+
if (
|
|
58
|
+
ts.isCallExpression(node) &&
|
|
59
|
+
ts.isIdentifier(node.expression) &&
|
|
60
|
+
node.expression.text === 'require' &&
|
|
61
|
+
node.arguments.length === 1 &&
|
|
62
|
+
ts.isStringLiteral(node.arguments[0])
|
|
63
|
+
) {
|
|
64
|
+
// Ignore require.resolve(), require.cache etc. — those are property accesses
|
|
65
|
+
// on the result, not on `require` itself: require.resolve() has a
|
|
66
|
+
// PropertyAccessExpression as node.expression, not an Identifier.
|
|
67
|
+
const source = (node.arguments[0] as ts.StringLiteral).text
|
|
68
|
+
const names = this.getRequireBindingNames(node)
|
|
69
|
+
// isDefault = true when binding is a plain identifier (const x = require(...))
|
|
70
|
+
// or when there's no binding at all (require(...) used for side effects).
|
|
71
|
+
// Only destructured object bindings (const { a } = require(...)) are named imports.
|
|
72
|
+
const parent = node.parent
|
|
73
|
+
const isDestructured = parent &&
|
|
74
|
+
ts.isVariableDeclaration(parent) &&
|
|
75
|
+
ts.isObjectBindingPattern(parent.name)
|
|
76
|
+
imports.push({
|
|
77
|
+
source,
|
|
78
|
+
resolvedPath: '',
|
|
79
|
+
names,
|
|
80
|
+
isDefault: !isDestructured,
|
|
81
|
+
isDynamic: false,
|
|
82
|
+
})
|
|
83
|
+
}
|
|
84
|
+
ts.forEachChild(node, walk)
|
|
85
|
+
}
|
|
86
|
+
ts.forEachChild(this.sourceFile, walk)
|
|
87
|
+
return imports
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Names extracted from the variable declaration that receives the require() call. */
|
|
91
|
+
private getRequireBindingNames(call: ts.CallExpression): string[] {
|
|
92
|
+
const parent = call.parent
|
|
93
|
+
if (!parent || !ts.isVariableDeclaration(parent)) return []
|
|
94
|
+
// const { a, b } = require('...') → ['a', 'b']
|
|
95
|
+
if (ts.isObjectBindingPattern(parent.name)) {
|
|
96
|
+
return parent.name.elements
|
|
97
|
+
.filter(e => ts.isIdentifier(e.name))
|
|
98
|
+
.map(e => (e.name as ts.Identifier).text)
|
|
99
|
+
}
|
|
100
|
+
// const x = require('...') → ['x']
|
|
101
|
+
if (ts.isIdentifier(parent.name)) return [parent.name.text]
|
|
102
|
+
return []
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ── CommonJS: module.exports / exports.x exports ─────────────────────────
|
|
106
|
+
|
|
107
|
+
private extractCommonJsExports(): ParsedExport[] {
|
|
108
|
+
const result: ParsedExport[] = []
|
|
109
|
+
const fp = this.filePath
|
|
110
|
+
|
|
111
|
+
const walk = (node: ts.Node) => {
|
|
112
|
+
if (
|
|
113
|
+
ts.isExpressionStatement(node) &&
|
|
114
|
+
ts.isBinaryExpression(node.expression) &&
|
|
115
|
+
node.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
116
|
+
) {
|
|
117
|
+
const lhs = node.expression.left
|
|
118
|
+
const rhs = node.expression.right
|
|
119
|
+
|
|
120
|
+
// ── module.exports = ... ────────────────────────────────────
|
|
121
|
+
if (isModuleExports(lhs)) {
|
|
122
|
+
if (ts.isObjectLiteralExpression(rhs)) {
|
|
123
|
+
// module.exports = { foo, bar }
|
|
124
|
+
for (const prop of rhs.properties) {
|
|
125
|
+
if (ts.isShorthandPropertyAssignment(prop)) {
|
|
126
|
+
result.push({ name: prop.name.text, type: 'const', file: fp })
|
|
127
|
+
} else if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
128
|
+
const isFunc = ts.isFunctionExpression(prop.initializer) || ts.isArrowFunction(prop.initializer)
|
|
129
|
+
result.push({ name: prop.name.text, type: isFunc ? 'function' : 'const', file: fp })
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} else if (ts.isFunctionExpression(rhs) || ts.isArrowFunction(rhs)) {
|
|
133
|
+
// module.exports = function name() {} → use function name or 'default'
|
|
134
|
+
const name = ts.isFunctionExpression(rhs) && rhs.name ? rhs.name.text : 'default'
|
|
135
|
+
result.push({ name, type: 'default', file: fp })
|
|
136
|
+
} else if (ts.isClassExpression(rhs) && rhs.name) {
|
|
137
|
+
result.push({ name: rhs.name.text, type: 'class', file: fp })
|
|
138
|
+
} else if (ts.isIdentifier(rhs)) {
|
|
139
|
+
result.push({ name: rhs.text, type: 'default', file: fp })
|
|
140
|
+
} else {
|
|
141
|
+
result.push({ name: 'default', type: 'default', file: fp })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// ── exports.foo = ... ───────────────────────────────────────
|
|
146
|
+
if (isExportsDotProp(lhs)) {
|
|
147
|
+
const prop = lhs as ts.PropertyAccessExpression
|
|
148
|
+
const isFunc = ts.isFunctionExpression(rhs) || ts.isArrowFunction(rhs)
|
|
149
|
+
result.push({ name: prop.name.text, type: isFunc ? 'function' : 'const', file: fp })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── module.exports.foo = ... ────────────────────────────────
|
|
153
|
+
if (isModuleExportsDotProp(lhs)) {
|
|
154
|
+
const prop = lhs as ts.PropertyAccessExpression
|
|
155
|
+
const isFunc = ts.isFunctionExpression(rhs) || ts.isArrowFunction(rhs)
|
|
156
|
+
result.push({ name: prop.name.text, type: isFunc ? 'function' : 'const', file: fp })
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
ts.forEachChild(node, walk)
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
ts.forEachChild(this.sourceFile, walk)
|
|
163
|
+
return result
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// ── CommonJS: module.exports / exports.x function bodies ──────────────────
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Detect functions directly assigned via module.exports or exports.x:
|
|
170
|
+
* module.exports = function handleLogin(req, res) { ... }
|
|
171
|
+
* module.exports = function() { ... } ← name = 'default'
|
|
172
|
+
* exports.createUser = function(data) { ... }
|
|
173
|
+
* exports.createUser = (data) => { ... }
|
|
174
|
+
*/
|
|
175
|
+
private extractCommonJsFunctions(): ParsedFunction[] {
|
|
176
|
+
const result: ParsedFunction[] = []
|
|
177
|
+
|
|
178
|
+
const walk = (node: ts.Node) => {
|
|
179
|
+
if (
|
|
180
|
+
ts.isExpressionStatement(node) &&
|
|
181
|
+
ts.isBinaryExpression(node.expression) &&
|
|
182
|
+
node.expression.operatorToken.kind === ts.SyntaxKind.EqualsToken
|
|
183
|
+
) {
|
|
184
|
+
const lhs = node.expression.left
|
|
185
|
+
const rhs = node.expression.right
|
|
186
|
+
|
|
187
|
+
if (!ts.isFunctionExpression(rhs) && !ts.isArrowFunction(rhs)) {
|
|
188
|
+
ts.forEachChild(node, walk)
|
|
189
|
+
return
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
let fnName: string | null = null
|
|
193
|
+
if (isModuleExports(lhs)) {
|
|
194
|
+
// module.exports = function name() {} / function() {}
|
|
195
|
+
fnName = ts.isFunctionExpression(rhs) && rhs.name ? rhs.name.text : 'default'
|
|
196
|
+
} else if (isExportsDotProp(lhs)) {
|
|
197
|
+
// exports.foo = function() {}
|
|
198
|
+
fnName = (lhs as ts.PropertyAccessExpression).name.text
|
|
199
|
+
} else if (isModuleExportsDotProp(lhs)) {
|
|
200
|
+
// module.exports.foo = function() {}
|
|
201
|
+
fnName = (lhs as ts.PropertyAccessExpression).name.text
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (fnName !== null) {
|
|
205
|
+
const startLine = this.getLineNumber(node.getStart())
|
|
206
|
+
const endLine = this.getLineNumber(node.getEnd())
|
|
207
|
+
const isAsync = !!(rhs.modifiers?.some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AsyncKeyword))
|
|
208
|
+
result.push({
|
|
209
|
+
id: `fn:${this.filePath}:${fnName}`,
|
|
210
|
+
name: fnName,
|
|
211
|
+
file: this.filePath,
|
|
212
|
+
startLine,
|
|
213
|
+
endLine,
|
|
214
|
+
params: this.extractParams(rhs.parameters),
|
|
215
|
+
returnType: rhs.type ? rhs.type.getText(this.sourceFile) : 'void',
|
|
216
|
+
isExported: true,
|
|
217
|
+
isAsync,
|
|
218
|
+
calls: this.extractCalls(rhs),
|
|
219
|
+
hash: hashContent(rhs.getText(this.sourceFile)),
|
|
220
|
+
purpose: this.extractPurpose(node),
|
|
221
|
+
edgeCasesHandled: this.extractEdgeCases(rhs),
|
|
222
|
+
errorHandling: this.extractErrorHandling(rhs),
|
|
223
|
+
detailedLines: this.extractDetailedLines(rhs),
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
ts.forEachChild(node, walk)
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
ts.forEachChild(this.sourceFile, walk)
|
|
231
|
+
return result
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Helpers ─────────────────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
/** node is `module.exports` */
|
|
238
|
+
function isModuleExports(node: ts.Node): boolean {
|
|
239
|
+
return (
|
|
240
|
+
ts.isPropertyAccessExpression(node) &&
|
|
241
|
+
ts.isIdentifier(node.expression) &&
|
|
242
|
+
node.expression.text === 'module' &&
|
|
243
|
+
node.name.text === 'exports'
|
|
244
|
+
)
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/** node is `exports.something` */
|
|
248
|
+
function isExportsDotProp(node: ts.Node): boolean {
|
|
249
|
+
return (
|
|
250
|
+
ts.isPropertyAccessExpression(node) &&
|
|
251
|
+
ts.isIdentifier(node.expression) &&
|
|
252
|
+
node.expression.text === 'exports'
|
|
253
|
+
)
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/** node is `module.exports.something` */
|
|
257
|
+
function isModuleExportsDotProp(node: ts.Node): boolean {
|
|
258
|
+
return (
|
|
259
|
+
ts.isPropertyAccessExpression(node) &&
|
|
260
|
+
isModuleExports(node.expression)
|
|
261
|
+
)
|
|
262
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
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
|
+
|
|
9
|
+
/**
|
|
10
|
+
* JavaScriptParser — implements BaseParser for .js / .mjs / .cjs / .jsx files.
|
|
11
|
+
*
|
|
12
|
+
* Uses the TypeScript Compiler API (ScriptKind.JS / ScriptKind.JSX) which correctly
|
|
13
|
+
* parses JavaScript without type annotations. JavaScriptExtractor extends
|
|
14
|
+
* TypeScriptExtractor and adds CommonJS require() / module.exports support.
|
|
15
|
+
*/
|
|
16
|
+
export class JavaScriptParser extends BaseParser {
|
|
17
|
+
parse(filePath: string, content: string): ParsedFile {
|
|
18
|
+
const extractor = new JavaScriptExtractor(filePath, content)
|
|
19
|
+
|
|
20
|
+
const functions = extractor.extractFunctions()
|
|
21
|
+
const classes = extractor.extractClasses()
|
|
22
|
+
const generics = extractor.extractGenerics()
|
|
23
|
+
const imports = extractor.extractImports()
|
|
24
|
+
const exports = extractor.extractExports()
|
|
25
|
+
const routes = extractor.extractRoutes()
|
|
26
|
+
|
|
27
|
+
// Cross-reference: CJS exports may mark a name exported even when the
|
|
28
|
+
// declaration itself had no `export` keyword.
|
|
29
|
+
const exportedNames = new Set(exports.map(e => e.name))
|
|
30
|
+
for (const fn of functions) { if (!fn.isExported && exportedNames.has(fn.name)) fn.isExported = true }
|
|
31
|
+
for (const cls of classes) { if (!cls.isExported && exportedNames.has(cls.name)) cls.isExported = true }
|
|
32
|
+
for (const gen of generics) { if (!gen.isExported && exportedNames.has(gen.name)) gen.isExported = true }
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
path: filePath,
|
|
36
|
+
language: 'javascript',
|
|
37
|
+
functions,
|
|
38
|
+
classes,
|
|
39
|
+
generics,
|
|
40
|
+
imports,
|
|
41
|
+
exports,
|
|
42
|
+
routes,
|
|
43
|
+
hash: hashContent(content),
|
|
44
|
+
parsedAt: Date.now(),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
|
|
49
|
+
const aliases = loadAliases(projectRoot)
|
|
50
|
+
const allFilePaths = files.map(f => f.path)
|
|
51
|
+
const resolver = new JavaScriptResolver(projectRoot, aliases)
|
|
52
|
+
return files.map(file => ({
|
|
53
|
+
...file,
|
|
54
|
+
imports: resolver.resolveAll(file.imports, file.path, allFilePaths),
|
|
55
|
+
}))
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
getSupportedExtensions(): string[] {
|
|
59
|
+
return ['.js', '.mjs', '.cjs', '.jsx']
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Load path aliases from jsconfig.json → tsconfig.json → tsconfig.base.json.
|
|
65
|
+
* Strips JS/block comments before parsing (both formats allow them).
|
|
66
|
+
* Falls back to raw content if comment-stripping breaks a URL.
|
|
67
|
+
* Returns {} when no config is found.
|
|
68
|
+
*/
|
|
69
|
+
function loadAliases(projectRoot: string): Record<string, string[]> {
|
|
70
|
+
for (const name of ['jsconfig.json', 'tsconfig.json', 'tsconfig.base.json']) {
|
|
71
|
+
const configPath = path.join(projectRoot, name)
|
|
72
|
+
try {
|
|
73
|
+
const raw = fs.readFileSync(configPath, 'utf-8')
|
|
74
|
+
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
75
|
+
let config: any
|
|
76
|
+
try { config = JSON.parse(stripped) }
|
|
77
|
+
catch { config = JSON.parse(raw) } // URL stripping may have broken JSON
|
|
78
|
+
|
|
79
|
+
const options = config.compilerOptions ?? {}
|
|
80
|
+
const rawPaths: Record<string, string[]> = options.paths ?? {}
|
|
81
|
+
if (Object.keys(rawPaths).length === 0) continue
|
|
82
|
+
|
|
83
|
+
const baseUrl = options.baseUrl ?? '.'
|
|
84
|
+
const resolved: Record<string, string[]> = {}
|
|
85
|
+
for (const [alias, targets] of Object.entries(rawPaths)) {
|
|
86
|
+
resolved[alias] = (targets as string[]).map((t: string) => path.posix.join(baseUrl, t))
|
|
87
|
+
}
|
|
88
|
+
return resolved
|
|
89
|
+
} catch { /* config absent or unreadable — try next */ }
|
|
90
|
+
}
|
|
91
|
+
return {}
|
|
92
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as path from 'node:path'
|
|
2
|
+
import type { ParsedImport } from '../types.js'
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* JavaScriptResolver — resolves JS/JSX/CJS import paths to project-relative files.
|
|
6
|
+
*
|
|
7
|
+
* Handles:
|
|
8
|
+
* - Relative ESM imports: import './utils' → ./utils.js / ./utils/index.js / ...
|
|
9
|
+
* - CommonJS require(): require('./db') → same resolution order
|
|
10
|
+
* - Path aliases from jsconfig.json / tsconfig.json
|
|
11
|
+
* - Mixed TS/JS projects: falls back to .ts/.tsx if no JS file matched
|
|
12
|
+
*
|
|
13
|
+
* Extension probe order: .js → .jsx → .mjs → .cjs → index.js → index.jsx →
|
|
14
|
+
* .ts → .tsx → index.ts → index.tsx
|
|
15
|
+
*/
|
|
16
|
+
export class JavaScriptResolver {
|
|
17
|
+
constructor(
|
|
18
|
+
private readonly projectRoot: string,
|
|
19
|
+
private readonly aliases: Record<string, string[]> = {},
|
|
20
|
+
) {}
|
|
21
|
+
|
|
22
|
+
resolve(imp: ParsedImport, fromFile: string, allProjectFiles: string[] = []): ParsedImport {
|
|
23
|
+
// External packages (no ./ / alias prefix) — leave unresolved
|
|
24
|
+
if (
|
|
25
|
+
!imp.source.startsWith('.') &&
|
|
26
|
+
!imp.source.startsWith('/') &&
|
|
27
|
+
!this.matchesAlias(imp.source)
|
|
28
|
+
) {
|
|
29
|
+
return { ...imp, resolvedPath: '' }
|
|
30
|
+
}
|
|
31
|
+
return { ...imp, resolvedPath: this.resolvePath(imp.source, fromFile, allProjectFiles) }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
resolveAll(imports: ParsedImport[], fromFile: string, allProjectFiles: string[] = []): ParsedImport[] {
|
|
35
|
+
return imports.map(imp => this.resolve(imp, fromFile, allProjectFiles))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
private resolvePath(source: string, fromFile: string, allProjectFiles: string[]): string {
|
|
39
|
+
let resolvedSource = source
|
|
40
|
+
|
|
41
|
+
// 1. Alias substitution
|
|
42
|
+
for (const [alias, targets] of Object.entries(this.aliases)) {
|
|
43
|
+
const prefix = alias.replace('/*', '')
|
|
44
|
+
if (source.startsWith(prefix)) {
|
|
45
|
+
resolvedSource = targets[0].replace('/*', '') + source.slice(prefix.length)
|
|
46
|
+
break
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// 2. Build absolute-like posix path
|
|
51
|
+
let resolved: string
|
|
52
|
+
if (resolvedSource.startsWith('.')) {
|
|
53
|
+
resolved = path.posix.normalize(path.posix.join(path.dirname(fromFile), resolvedSource))
|
|
54
|
+
} else {
|
|
55
|
+
resolved = resolvedSource
|
|
56
|
+
}
|
|
57
|
+
resolved = resolved.replace(/\\/g, '/')
|
|
58
|
+
|
|
59
|
+
// 3. Already has a concrete extension — return as-is
|
|
60
|
+
const knownExts = ['.js', '.mjs', '.cjs', '.jsx', '.ts', '.tsx']
|
|
61
|
+
if (knownExts.some(e => resolved.endsWith(e))) return resolved
|
|
62
|
+
|
|
63
|
+
// 4. Probe extensions: prefer JS-family first, fall back to TS for mixed projects
|
|
64
|
+
const probeOrder = [
|
|
65
|
+
'.js', '.jsx', '.mjs', '.cjs',
|
|
66
|
+
'/index.js', '/index.jsx', '/index.mjs',
|
|
67
|
+
'.ts', '.tsx',
|
|
68
|
+
'/index.ts', '/index.tsx',
|
|
69
|
+
]
|
|
70
|
+
for (const ext of probeOrder) {
|
|
71
|
+
const candidate = resolved + ext
|
|
72
|
+
if (allProjectFiles.length === 0 || allProjectFiles.includes(candidate)) {
|
|
73
|
+
return candidate
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return resolved + '.js'
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
private matchesAlias(source: string): boolean {
|
|
81
|
+
return Object.keys(this.aliases).some(a => source.startsWith(a.replace('/*', '')))
|
|
82
|
+
}
|
|
83
|
+
}
|
package/src/parser/types.ts
CHANGED
|
@@ -89,7 +89,7 @@ export interface ParsedGeneric {
|
|
|
89
89
|
/** Everything extracted from a single file */
|
|
90
90
|
export interface ParsedFile {
|
|
91
91
|
path: string // "src/auth/verify.ts"
|
|
92
|
-
language: 'typescript' | 'python'
|
|
92
|
+
language: 'typescript' | 'javascript' | 'python' | 'go'
|
|
93
93
|
functions: ParsedFunction[]
|
|
94
94
|
classes: ParsedClass[]
|
|
95
95
|
generics: ParsedGeneric[]
|