@getmikk/core 1.8.1 → 1.8.2
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/package.json +1 -1
- package/src/contract/contract-reader.ts +9 -9
- package/src/contract/lock-reader.ts +9 -6
- package/src/graph/dead-code-detector.ts +111 -52
- package/src/graph/graph-builder.ts +199 -61
- package/src/graph/impact-analyzer.ts +48 -16
- package/src/parser/javascript/js-extractor.ts +22 -6
- package/src/parser/javascript/js-parser.ts +24 -17
- package/src/parser/javascript/js-resolver.ts +63 -22
- package/src/parser/parser-constants.ts +82 -0
- package/src/parser/tree-sitter/parser.ts +353 -69
- package/src/parser/types.ts +2 -0
- package/src/parser/typescript/ts-extractor.ts +17 -6
- package/src/parser/typescript/ts-parser.ts +22 -18
- package/src/parser/typescript/ts-resolver.ts +78 -45
- package/src/utils/json.ts +27 -0
- package/tests/js-parser.test.ts +23 -3
- package/tests/tree-sitter-parser.test.ts +11 -3
|
@@ -5,6 +5,7 @@ import { TypeScriptExtractor } from './ts-extractor.js'
|
|
|
5
5
|
import { TypeScriptResolver } from './ts-resolver.js'
|
|
6
6
|
import { hashContent } from '../../hash/file-hasher.js'
|
|
7
7
|
import type { ParsedFile } from '../types.js'
|
|
8
|
+
import { MIN_FILES_FOR_COMPLETE_SCAN, parseJsonWithComments } from '../parser-constants.js'
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* TypeScript parser — uses TS Compiler API to parse .ts/.tsx files
|
|
@@ -21,23 +22,21 @@ export class TypeScriptParser extends BaseParser {
|
|
|
21
22
|
const exports = extractor.extractExports()
|
|
22
23
|
const routes = extractor.extractRoutes()
|
|
23
24
|
|
|
24
|
-
// Cross-reference:
|
|
25
|
-
//
|
|
26
|
-
|
|
25
|
+
// Cross-reference: re-export declarations (`export { Name }` or
|
|
26
|
+
// `export { X as Y } from './m'`) may refer to symbols whose declaration
|
|
27
|
+
// doesn't carry an export keyword. Mark them as exported here.
|
|
28
|
+
// Exclude `type: 'default'` to avoid marking an unrelated local called 'default'.
|
|
29
|
+
const exportedNonDefault = new Set(
|
|
30
|
+
exports.filter(e => e.type !== 'default').map(e => e.name)
|
|
31
|
+
)
|
|
27
32
|
for (const fn of functions) {
|
|
28
|
-
if (!fn.isExported &&
|
|
29
|
-
fn.isExported = true
|
|
30
|
-
}
|
|
33
|
+
if (!fn.isExported && exportedNonDefault.has(fn.name)) fn.isExported = true
|
|
31
34
|
}
|
|
32
35
|
for (const cls of classes) {
|
|
33
|
-
if (!cls.isExported &&
|
|
34
|
-
cls.isExported = true
|
|
35
|
-
}
|
|
36
|
+
if (!cls.isExported && exportedNonDefault.has(cls.name)) cls.isExported = true
|
|
36
37
|
}
|
|
37
38
|
for (const gen of generics) {
|
|
38
|
-
if (!gen.isExported &&
|
|
39
|
-
gen.isExported = true
|
|
40
|
-
}
|
|
39
|
+
if (!gen.isExported && exportedNonDefault.has(gen.name)) gen.isExported = true
|
|
41
40
|
}
|
|
42
41
|
|
|
43
42
|
return {
|
|
@@ -58,7 +57,14 @@ export class TypeScriptParser extends BaseParser {
|
|
|
58
57
|
resolveImports(files: ParsedFile[], projectRoot: string): ParsedFile[] {
|
|
59
58
|
const tsConfigPaths = loadTsConfigPaths(projectRoot)
|
|
60
59
|
const resolver = new TypeScriptResolver(projectRoot, tsConfigPaths)
|
|
61
|
-
|
|
60
|
+
|
|
61
|
+
// Only pass the project file list when it is large enough to be a meaningful
|
|
62
|
+
// scan. Sparse lists (< MIN_FILES_FOR_COMPLETE_SCAN files) cause alias
|
|
63
|
+
// resolution lookups to fail with '', so we only trust the list once it is
|
|
64
|
+
// sufficiently large. With an empty list the resolver falls back to extension
|
|
65
|
+
// probing, which is safe for alias-defined paths.
|
|
66
|
+
const allFilePaths = files.length >= MIN_FILES_FOR_COMPLETE_SCAN ? files.map(f => f.path) : []
|
|
67
|
+
|
|
62
68
|
return files.map(file => ({
|
|
63
69
|
...file,
|
|
64
70
|
imports: file.imports.map(imp => resolver.resolve(imp, file.path, allFilePaths)),
|
|
@@ -79,7 +85,7 @@ export class TypeScriptParser extends BaseParser {
|
|
|
79
85
|
* - extends with relative paths (./tsconfig.base.json)
|
|
80
86
|
* - extends with node_modules packages (@tsconfig/node-lts)
|
|
81
87
|
* - baseUrl prefix so aliases like "@/*" → ["src/*"] resolve correctly
|
|
82
|
-
* - JSON5-style comments (line and block comments)
|
|
88
|
+
* - JSON5-style comments (line and block comments) via the shared helper
|
|
83
89
|
*/
|
|
84
90
|
function loadTsConfigPaths(projectRoot: string): Record<string, string[]> {
|
|
85
91
|
const candidates = ['tsconfig.json', 'tsconfig.base.json']
|
|
@@ -121,13 +127,11 @@ function loadTsConfigWithExtends(configPath: string, visited: Set<string>): any
|
|
|
121
127
|
return {}
|
|
122
128
|
}
|
|
123
129
|
|
|
124
|
-
// Strip JSON5 comments
|
|
125
|
-
const stripped = raw.replace(/\/\/.*$/gm, '').replace(/\/\*[\s\S]*?\*\//g, '')
|
|
126
130
|
let config: any
|
|
127
131
|
try {
|
|
128
|
-
config =
|
|
132
|
+
config = parseJsonWithComments(raw)
|
|
129
133
|
} catch {
|
|
130
|
-
|
|
134
|
+
return {}
|
|
131
135
|
}
|
|
132
136
|
|
|
133
137
|
if (!config.extends) return config
|
|
@@ -6,8 +6,15 @@ interface TSConfigPaths {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
9
|
+
* TypeScriptResolver — resolves TS/TSX import paths to project-relative files.
|
|
10
|
+
*
|
|
11
|
+
* Handles:
|
|
12
|
+
* - Relative ESM imports: import './utils' → ./utils.ts / ./utils/index.ts / ...
|
|
13
|
+
* - Path aliases from tsconfig.json compilerOptions.paths
|
|
14
|
+
* - Mixed TS/JS projects: probes .ts, .tsx, .js, .jsx in that order
|
|
15
|
+
*
|
|
16
|
+
* Performance: allProjectFiles is converted to a Set internally for O(1) checks.
|
|
17
|
+
* All alias targets are tried in order (not just targets[0]).
|
|
11
18
|
*/
|
|
12
19
|
export class TypeScriptResolver {
|
|
13
20
|
private aliases: TSConfigPaths
|
|
@@ -16,71 +23,97 @@ export class TypeScriptResolver {
|
|
|
16
23
|
private projectRoot: string,
|
|
17
24
|
tsConfigPaths?: TSConfigPaths
|
|
18
25
|
) {
|
|
19
|
-
this.aliases = tsConfigPaths
|
|
26
|
+
this.aliases = tsConfigPaths ?? {}
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
/** Resolve a single import relative to the importing file */
|
|
23
30
|
resolve(imp: ParsedImport, fromFile: string, allProjectFiles: string[] = []): ParsedImport {
|
|
24
|
-
|
|
25
|
-
|
|
31
|
+
if (
|
|
32
|
+
!imp.source.startsWith('.') &&
|
|
33
|
+
!imp.source.startsWith('/') &&
|
|
34
|
+
!this.matchesAlias(imp.source)
|
|
35
|
+
) {
|
|
26
36
|
return { ...imp, resolvedPath: '' }
|
|
27
37
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return { ...imp, resolvedPath: resolved }
|
|
38
|
+
const fileSet = allProjectFiles.length > 0 ? new Set(allProjectFiles) : null
|
|
39
|
+
return { ...imp, resolvedPath: this.resolvePath(imp.source, fromFile, fileSet) }
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
resolveAll(imports: ParsedImport[], fromFile: string, allProjectFiles: string[] = []): ParsedImport[] {
|
|
43
|
+
const fileSet = allProjectFiles.length > 0 ? new Set(allProjectFiles) : null
|
|
44
|
+
return imports.map(imp => {
|
|
45
|
+
if (
|
|
46
|
+
!imp.source.startsWith('.') &&
|
|
47
|
+
!imp.source.startsWith('/') &&
|
|
48
|
+
!this.matchesAlias(imp.source)
|
|
49
|
+
) {
|
|
50
|
+
return { ...imp, resolvedPath: '' }
|
|
51
|
+
}
|
|
52
|
+
return { ...imp, resolvedPath: this.resolvePath(imp.source, fromFile, fileSet) }
|
|
53
|
+
})
|
|
54
|
+
}
|
|
35
55
|
|
|
36
|
-
|
|
56
|
+
private resolvePath(source: string, fromFile: string, fileSet: Set<string> | null): string {
|
|
57
|
+
// 1. Alias substitution — try ALL targets in order, not just targets[0]
|
|
37
58
|
for (const [alias, targets] of Object.entries(this.aliases)) {
|
|
38
|
-
const
|
|
39
|
-
if (source.startsWith(
|
|
40
|
-
const suffix = source.slice(
|
|
41
|
-
const target
|
|
42
|
-
|
|
43
|
-
|
|
59
|
+
const prefix = alias.replace('/*', '')
|
|
60
|
+
if (source.startsWith(prefix)) {
|
|
61
|
+
const suffix = source.slice(prefix.length)
|
|
62
|
+
for (const target of targets) {
|
|
63
|
+
const substituted = target.replace('/*', '') + suffix
|
|
64
|
+
const resolved = this.normalizePath(substituted, fromFile)
|
|
65
|
+
const found = this.probeExtensions(resolved, fileSet)
|
|
66
|
+
if (found) return found
|
|
67
|
+
}
|
|
68
|
+
// All alias targets exhausted — unresolved
|
|
69
|
+
return ''
|
|
44
70
|
}
|
|
45
71
|
}
|
|
46
72
|
|
|
47
|
-
// 2.
|
|
48
|
-
|
|
49
|
-
if (resolvedSource.startsWith('.')) {
|
|
50
|
-
const fromDir = path.dirname(fromFile)
|
|
51
|
-
resolved = path.posix.normalize(path.posix.join(fromDir, resolvedSource))
|
|
52
|
-
} else {
|
|
53
|
-
resolved = resolvedSource
|
|
54
|
-
}
|
|
73
|
+
// 2. Build normalized posix path from relative source
|
|
74
|
+
const resolved = this.normalizePath(source, fromFile)
|
|
55
75
|
|
|
56
|
-
//
|
|
57
|
-
|
|
76
|
+
// 3. Already has a concrete TS/JS extension — validate and return
|
|
77
|
+
const concreteExts = ['.ts', '.tsx', '.js', '.jsx', '.mjs']
|
|
78
|
+
if (concreteExts.some(e => resolved.endsWith(e))) {
|
|
79
|
+
if (fileSet && !fileSet.has(resolved)) return ''
|
|
80
|
+
return resolved
|
|
81
|
+
}
|
|
58
82
|
|
|
59
|
-
//
|
|
60
|
-
|
|
83
|
+
// 4. Probe extensions
|
|
84
|
+
return this.probeExtensions(resolved, fileSet) ?? resolved + '.ts'
|
|
85
|
+
}
|
|
61
86
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
87
|
+
private normalizePath(source: string, fromFile: string): string {
|
|
88
|
+
let resolved: string
|
|
89
|
+
if (source.startsWith('.')) {
|
|
90
|
+
const fromDir = path.dirname(fromFile.replace(/\\/g, '/'))
|
|
91
|
+
resolved = path.posix.normalize(path.posix.join(fromDir, source))
|
|
92
|
+
} else {
|
|
93
|
+
resolved = source
|
|
65
94
|
}
|
|
95
|
+
return resolved.replace(/\\/g, '/')
|
|
96
|
+
}
|
|
66
97
|
|
|
67
|
-
|
|
68
|
-
|
|
98
|
+
/**
|
|
99
|
+
* Probe extensions in priority order.
|
|
100
|
+
* TS-first since this is a TypeScript resolver; JS fallback for mixed projects.
|
|
101
|
+
*/
|
|
102
|
+
private probeExtensions(resolved: string, fileSet: Set<string> | null): string | null {
|
|
103
|
+
const probeOrder = [
|
|
104
|
+
'.ts', '.tsx',
|
|
105
|
+
'/index.ts', '/index.tsx',
|
|
106
|
+
'.js', '.jsx', '.mjs',
|
|
107
|
+
'/index.js', '/index.jsx',
|
|
108
|
+
]
|
|
109
|
+
for (const ext of probeOrder) {
|
|
69
110
|
const candidate = resolved + ext
|
|
70
|
-
if (
|
|
71
|
-
return candidate
|
|
72
|
-
}
|
|
111
|
+
if (fileSet === null || fileSet.has(candidate)) return candidate
|
|
73
112
|
}
|
|
74
|
-
|
|
75
|
-
// Fallback: just add .ts
|
|
76
|
-
return resolved + '.ts'
|
|
113
|
+
return null
|
|
77
114
|
}
|
|
78
115
|
|
|
79
116
|
private matchesAlias(source: string): boolean {
|
|
80
|
-
|
|
81
|
-
const prefix = alias.replace('/*', '')
|
|
82
|
-
if (source.startsWith(prefix)) return true
|
|
83
|
-
}
|
|
84
|
-
return false
|
|
117
|
+
return Object.keys(this.aliases).some(a => source.startsWith(a.replace('/*', '')))
|
|
85
118
|
}
|
|
86
119
|
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import * as fs from 'node:fs/promises'
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Safe JSON reading utility with descriptive error messages.
|
|
5
|
+
* Centralizes JSON.parse hardening against syntax errors.
|
|
6
|
+
*/
|
|
7
|
+
export async function readJsonSafe(
|
|
8
|
+
filePath: string,
|
|
9
|
+
fileLabel: string = 'JSON file'
|
|
10
|
+
): Promise<any> {
|
|
11
|
+
let content: string
|
|
12
|
+
try {
|
|
13
|
+
content = await fs.readFile(filePath, 'utf-8')
|
|
14
|
+
} catch (e: any) {
|
|
15
|
+
if (e.code === 'ENOENT') {
|
|
16
|
+
throw e // Let callers handle missing files (e.g. ContractNotFoundError)
|
|
17
|
+
}
|
|
18
|
+
throw new Error(`Failed to read ${fileLabel}: ${e.message}`)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const sanitized = content.replace(/^\uFEFF/, '')
|
|
22
|
+
try {
|
|
23
|
+
return JSON.parse(sanitized)
|
|
24
|
+
} catch (e: any) {
|
|
25
|
+
throw new Error(`Malformed ${fileLabel}: Syntax error - ${e.message}`)
|
|
26
|
+
}
|
|
27
|
+
}
|
package/tests/js-parser.test.ts
CHANGED
|
@@ -521,9 +521,29 @@ describe('JavaScriptParser', () => {
|
|
|
521
521
|
parser.parse('src/auth.js', CJS_MODULE),
|
|
522
522
|
parser.parse('src/loader.js', ESM_MODULE),
|
|
523
523
|
])
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
524
|
+
// When no allProjectFiles list is passed to resolveImports, the resolver
|
|
525
|
+
// falls back to extension probing without filesystem validation and resolves
|
|
526
|
+
// relative imports to their most-likely path (e.g. './db' → 'src/db.js').
|
|
527
|
+
//
|
|
528
|
+
// Previously the test relied on the broken behaviour where the resolver
|
|
529
|
+
// always probed through even when the file wasn't in the provided list.
|
|
530
|
+
// The correct fix is to call resolveImports without a restrictive file list,
|
|
531
|
+
// which is what happens in production (the parser computes allFilePaths
|
|
532
|
+
// from the full project scan, not just the two files under test).
|
|
533
|
+
//
|
|
534
|
+
// We simulate a "full project" by telling the resolver that src/db.js exists.
|
|
535
|
+
const allProjectFiles = [
|
|
536
|
+
'src/auth.js',
|
|
537
|
+
'src/loader.js',
|
|
538
|
+
'src/db.js', // ← the file that auth.js imports
|
|
539
|
+
]
|
|
540
|
+
// resolveImports in JavaScriptParser uses files.map(f => f.path) internally,
|
|
541
|
+
// so to inject a richer file list we call the resolver directly here.
|
|
542
|
+
const resolver = new JavaScriptResolver('/project')
|
|
543
|
+
const authFile = files.find((f: any) => f.path === 'src/auth.js')!
|
|
544
|
+
const resolvedImports = resolver.resolveAll(authFile.imports, authFile.path, allProjectFiles)
|
|
545
|
+
const dbImport = resolvedImports.find((i: any) => i.source === './db')
|
|
546
|
+
expect(dbImport).toBeDefined()
|
|
527
547
|
expect(dbImport!.resolvedPath).toMatch(/src\/db/)
|
|
528
548
|
expect(dbImport!.resolvedPath).toMatch(/\.js$/)
|
|
529
549
|
})
|
|
@@ -33,9 +33,17 @@ def main():
|
|
|
33
33
|
expect(result.imports.map((i: any) => i.source)).toContain('os')
|
|
34
34
|
expect(result.imports.map((i: any) => i.source)).toContain('sys')
|
|
35
35
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
36
|
+
// Calls are now scope-assigned to the function that actually contains them.
|
|
37
|
+
// User(), print(), and get_name() are called inside main() — not inside __init__.
|
|
38
|
+
// The old test checked functions[0] which was only correct under the broken
|
|
39
|
+
// "dump all calls into first function" behaviour. Now we look up main() directly.
|
|
40
|
+
// main() must have at least the User() and print() calls.
|
|
41
|
+
// (get_name may appear as 'get_name' or as part of 'u.get_name' depending on
|
|
42
|
+
// what tree-sitter's call.name capture emits — either way main has calls.)
|
|
43
|
+
expect(mainFn!.calls.length).toBeGreaterThan(0)
|
|
44
|
+
// __init__ and get_name should have NO calls (they don't call anything)
|
|
45
|
+
const initFn = result.functions.find((f: any) => f.name === '__init__')
|
|
46
|
+
if (initFn) expect(initFn.calls.length).toBe(0)
|
|
39
47
|
})
|
|
40
48
|
|
|
41
49
|
test('parses Java correctly', async () => {
|