@getmikk/core 2.0.12 → 2.0.13
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 +12 -3
- package/package.json +1 -1
- package/src/contract/adr-manager.ts +5 -4
- package/src/contract/contract-writer.ts +3 -2
- package/src/contract/lock-compiler.ts +3 -0
- package/src/contract/lock-reader.ts +62 -5
- package/src/contract/schema.ts +8 -0
- package/src/index.ts +12 -1
- package/src/parser/index.ts +301 -74
- package/src/parser/oxc-parser.ts +3 -2
- package/src/parser/tree-sitter/parser.ts +24 -1
- package/src/parser/tree-sitter/queries.ts +27 -0
- package/src/parser/types.ts +1 -1
- package/src/utils/artifact-transaction.ts +176 -0
- package/src/utils/atomic-write.ts +131 -0
- package/src/utils/fs.ts +33 -13
- package/src/utils/language-registry.ts +82 -0
- package/tests/adr-manager.test.ts +6 -0
- package/tests/artifact-transaction.test.ts +73 -0
- package/tests/contract.test.ts +12 -0
- package/tests/dead-code.test.ts +12 -0
- package/tests/esm-resolver.test.ts +6 -0
- package/tests/fs.test.ts +22 -1
- package/tests/fuzzy-match.test.ts +6 -0
- package/tests/go-parser.test.ts +7 -0
- package/tests/graph.test.ts +10 -0
- package/tests/hash.test.ts +6 -0
- package/tests/impact-classified.test.ts +13 -0
- package/tests/js-parser.test.ts +10 -0
- package/tests/language-registry.test.ts +64 -0
- package/tests/parse-diagnostics.test.ts +115 -0
- package/tests/parser.test.ts +36 -0
- package/tests/tree-sitter-parser.test.ts +201 -0
- package/tests/ts-parser.test.ts +6 -0
package/tests/fs.test.ts
CHANGED
|
@@ -75,6 +75,12 @@ describe('detectProjectLanguage', () => {
|
|
|
75
75
|
})
|
|
76
76
|
})
|
|
77
77
|
|
|
78
|
+
it('detects Swift from Package.swift', async () => {
|
|
79
|
+
await withFile('Package.swift', async () => {
|
|
80
|
+
expect(await detectProjectLanguage(tmpDir)).toBe('swift')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
78
84
|
it('detects C# from .csproj file', async () => {
|
|
79
85
|
await withFile('MyApp.csproj', async () => {
|
|
80
86
|
expect(await detectProjectLanguage(tmpDir)).toBe('csharp')
|
|
@@ -116,7 +122,7 @@ describe('detectProjectLanguage', () => {
|
|
|
116
122
|
describe('getDiscoveryPatterns', () => {
|
|
117
123
|
const languages: ProjectLanguage[] = [
|
|
118
124
|
'typescript', 'javascript', 'python', 'go', 'rust',
|
|
119
|
-
'java', 'ruby', 'php', 'csharp', 'unknown',
|
|
125
|
+
'java', 'swift', 'ruby', 'php', 'csharp', 'unknown',
|
|
120
126
|
]
|
|
121
127
|
|
|
122
128
|
for (const lang of languages) {
|
|
@@ -143,6 +149,21 @@ describe('getDiscoveryPatterns', () => {
|
|
|
143
149
|
expect(patterns).toContain('**/*.py')
|
|
144
150
|
})
|
|
145
151
|
|
|
152
|
+
it('java discovery patterns include Kotlin scripts for mixed JVM codebases', () => {
|
|
153
|
+
const { patterns } = getDiscoveryPatterns('java')
|
|
154
|
+
expect(patterns).toContain('**/*.kts')
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('swift patterns include .swift', () => {
|
|
158
|
+
const { patterns } = getDiscoveryPatterns('swift')
|
|
159
|
+
expect(patterns).toContain('**/*.swift')
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
it('cpp patterns include .hh headers', () => {
|
|
163
|
+
const { patterns } = getDiscoveryPatterns('cpp')
|
|
164
|
+
expect(patterns).toContain('**/*.hh')
|
|
165
|
+
})
|
|
166
|
+
|
|
146
167
|
it('all languages ignore .mikk and .git', () => {
|
|
147
168
|
for (const lang of languages) {
|
|
148
169
|
const { ignore } = getDiscoveryPatterns(lang)
|
|
@@ -61,6 +61,12 @@ describe('levenshtein', () => {
|
|
|
61
61
|
test('insertion', () => {
|
|
62
62
|
expect(levenshtein('test', 'tests')).toBe(1)
|
|
63
63
|
})
|
|
64
|
+
|
|
65
|
+
test('distance is symmetric', () => {
|
|
66
|
+
expect(levenshtein('verifyToken', 'tokenVerify')).toBe(
|
|
67
|
+
levenshtein('tokenVerify', 'verifyToken'),
|
|
68
|
+
)
|
|
69
|
+
})
|
|
64
70
|
})
|
|
65
71
|
|
|
66
72
|
describe('splitCamelCase', () => {
|
package/tests/go-parser.test.ts
CHANGED
|
@@ -363,4 +363,11 @@ describe('GoParser', () => {
|
|
|
363
363
|
const resolved = await parser.resolveImports(files, '/tmp/no-gomod-' + Date.now())
|
|
364
364
|
expect(resolved.length).toBe(1)
|
|
365
365
|
})
|
|
366
|
+
|
|
367
|
+
test('parse keeps deterministic hash for identical input', async () => {
|
|
368
|
+
const parser = new GoParser()
|
|
369
|
+
const a = await parser.parse('auth/service.go', SIMPLE_GO)
|
|
370
|
+
const b = await parser.parse('auth/service.go', SIMPLE_GO)
|
|
371
|
+
expect(a.hash).toBe(b.hash)
|
|
372
|
+
})
|
|
366
373
|
})
|
package/tests/graph.test.ts
CHANGED
|
@@ -167,4 +167,14 @@ describe('ClusterDetector', () => {
|
|
|
167
167
|
expect(score).toBeGreaterThanOrEqual(0)
|
|
168
168
|
expect(score).toBeLessThanOrEqual(1)
|
|
169
169
|
})
|
|
170
|
+
|
|
171
|
+
it('keeps distinct function nodes for same function names in different files', () => {
|
|
172
|
+
const files = [
|
|
173
|
+
mockParsedFile('src/auth/a.ts', [mockFunction('shared', [], 'src/auth/a.ts')]),
|
|
174
|
+
mockParsedFile('src/db/b.ts', [mockFunction('shared', [], 'src/db/b.ts')]),
|
|
175
|
+
]
|
|
176
|
+
const graph = new GraphBuilder().build(files)
|
|
177
|
+
expect(graph.nodes.has('fn:src/auth/a.ts:shared')).toBe(true)
|
|
178
|
+
expect(graph.nodes.has('fn:src/db/b.ts:shared')).toBe(true)
|
|
179
|
+
})
|
|
170
180
|
})
|
package/tests/hash.test.ts
CHANGED
|
@@ -46,4 +46,10 @@ describe('computeRootHash', () => {
|
|
|
46
46
|
const hash2 = computeRootHash({ payments: 'def', auth: 'abc' })
|
|
47
47
|
expect(hash1).toBe(hash2)
|
|
48
48
|
})
|
|
49
|
+
|
|
50
|
+
it('changes when a module hash changes', () => {
|
|
51
|
+
const base = computeRootHash({ auth: 'abc', payments: 'def' })
|
|
52
|
+
const changed = computeRootHash({ auth: 'abc', payments: 'xyz' })
|
|
53
|
+
expect(base).not.toBe(changed)
|
|
54
|
+
})
|
|
49
55
|
})
|
|
@@ -71,4 +71,17 @@ describe('ImpactAnalyzer - Classified', () => {
|
|
|
71
71
|
expect(result.classified.low).toHaveLength(1)
|
|
72
72
|
expect(result.classified.low[0].nodeId).toBe('fn:src/highriskauthservice.ts:highriskauthservice')
|
|
73
73
|
})
|
|
74
|
+
|
|
75
|
+
it('deduplicates impacted nodes when changed list includes duplicates', () => {
|
|
76
|
+
const graph = buildTestGraph([
|
|
77
|
+
['A', 'B'],
|
|
78
|
+
['B', 'C'],
|
|
79
|
+
['C', 'nothing'],
|
|
80
|
+
])
|
|
81
|
+
|
|
82
|
+
const analyzer = new ImpactAnalyzer(graph)
|
|
83
|
+
const result = analyzer.analyze(['fn:src/c.ts:c', 'fn:src/c.ts:c'])
|
|
84
|
+
const unique = new Set(result.impacted)
|
|
85
|
+
expect(unique.size).toBe(result.impacted.length)
|
|
86
|
+
})
|
|
74
87
|
})
|
package/tests/js-parser.test.ts
CHANGED
|
@@ -1042,6 +1042,16 @@ describe('JavaScript - Additional Edge Cases', () => {
|
|
|
1042
1042
|
})
|
|
1043
1043
|
})
|
|
1044
1044
|
|
|
1045
|
+
describe('Deterministic parsing', () => {
|
|
1046
|
+
test('produces identical file hash for identical source', async () => {
|
|
1047
|
+
const parser = new JavaScriptParser()
|
|
1048
|
+
const source = 'export function stable() { return 1 }'
|
|
1049
|
+
const a = await parser.parse('src/stable.js', source)
|
|
1050
|
+
const b = await parser.parse('src/stable.js', source)
|
|
1051
|
+
expect(a.hash).toBe(b.hash)
|
|
1052
|
+
})
|
|
1053
|
+
})
|
|
1054
|
+
|
|
1045
1055
|
describe('Chained Methods', () => {
|
|
1046
1056
|
test('handles method chaining', () => {
|
|
1047
1057
|
const src = `
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
parserKindForExtension,
|
|
4
|
+
languageForExtension,
|
|
5
|
+
getParserExtensions,
|
|
6
|
+
getDiscoveryExtensions,
|
|
7
|
+
isTreeSitterExtension,
|
|
8
|
+
} from '../src/utils/language-registry'
|
|
9
|
+
|
|
10
|
+
describe('language-registry', () => {
|
|
11
|
+
it('maps parser kinds correctly', () => {
|
|
12
|
+
expect(parserKindForExtension('.ts')).toBe('oxc')
|
|
13
|
+
expect(parserKindForExtension('.go')).toBe('go')
|
|
14
|
+
expect(parserKindForExtension('.py')).toBe('tree-sitter')
|
|
15
|
+
expect(parserKindForExtension('.unknown')).toBe('unknown')
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
it('maps parser kinds case-insensitively', () => {
|
|
19
|
+
expect(parserKindForExtension('.TS')).toBe('oxc')
|
|
20
|
+
expect(parserKindForExtension('.Go')).toBe('go')
|
|
21
|
+
expect(parserKindForExtension('.RB')).toBe('tree-sitter')
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
it('maps languages correctly across major ecosystems', () => {
|
|
25
|
+
expect(languageForExtension('.ts')).toBe('typescript')
|
|
26
|
+
expect(languageForExtension('.js')).toBe('typescript')
|
|
27
|
+
expect(languageForExtension('.py')).toBe('python')
|
|
28
|
+
expect(languageForExtension('.go')).toBe('go')
|
|
29
|
+
expect(languageForExtension('.rs')).toBe('rust')
|
|
30
|
+
expect(languageForExtension('.java')).toBe('java')
|
|
31
|
+
expect(languageForExtension('.kt')).toBe('kotlin')
|
|
32
|
+
expect(languageForExtension('.kts')).toBe('kotlin')
|
|
33
|
+
expect(languageForExtension('.swift')).toBe('swift')
|
|
34
|
+
expect(languageForExtension('.rb')).toBe('ruby')
|
|
35
|
+
expect(languageForExtension('.php')).toBe('php')
|
|
36
|
+
expect(languageForExtension('.cs')).toBe('csharp')
|
|
37
|
+
expect(languageForExtension('.c')).toBe('c')
|
|
38
|
+
expect(languageForExtension('.cpp')).toBe('cpp')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it('returns parser extension sets', () => {
|
|
42
|
+
expect(getParserExtensions('oxc')).toContain('.tsx')
|
|
43
|
+
expect(getParserExtensions('go')).toEqual(['.go'])
|
|
44
|
+
expect(getParserExtensions('tree-sitter')).toContain('.swift')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('returns discovery extension sets for mixed JVM repos', () => {
|
|
48
|
+
const javaDiscovery = getDiscoveryExtensions('java')
|
|
49
|
+
expect(javaDiscovery).toContain('.java')
|
|
50
|
+
expect(javaDiscovery).toContain('.kt')
|
|
51
|
+
expect(javaDiscovery).toContain('.kts')
|
|
52
|
+
|
|
53
|
+
// Even though Java discovery includes Kotlin files, extension-to-language remains Kotlin.
|
|
54
|
+
expect(languageForExtension('.kt')).toBe('kotlin')
|
|
55
|
+
expect(languageForExtension('.kts')).toBe('kotlin')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('identifies tree-sitter extensions', () => {
|
|
59
|
+
expect(isTreeSitterExtension('.py')).toBe(true)
|
|
60
|
+
expect(isTreeSitterExtension('.swift')).toBe(true)
|
|
61
|
+
expect(isTreeSitterExtension('.ts')).toBe(false)
|
|
62
|
+
expect(isTreeSitterExtension('.go')).toBe(false)
|
|
63
|
+
})
|
|
64
|
+
})
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test'
|
|
2
|
+
import { parseFilesWithDiagnostics } from '../src/parser/index'
|
|
3
|
+
|
|
4
|
+
describe('parseFilesWithDiagnostics preflight', () => {
|
|
5
|
+
it('returns parser-unavailable diagnostic and no files in strict preflight mode', async () => {
|
|
6
|
+
const result = await parseFilesWithDiagnostics(
|
|
7
|
+
['src/example.py'],
|
|
8
|
+
'/project',
|
|
9
|
+
async () => 'def run():\n return 1\n',
|
|
10
|
+
{
|
|
11
|
+
strictParserPreflight: true,
|
|
12
|
+
treeSitterRuntimeAvailable: false,
|
|
13
|
+
},
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
expect(result.files).toHaveLength(0)
|
|
17
|
+
expect(result.summary.requestedFiles).toBe(1)
|
|
18
|
+
expect(result.summary.parsedFiles).toBe(0)
|
|
19
|
+
expect(result.summary.diagnostics).toBeGreaterThan(0)
|
|
20
|
+
expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(true)
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
it('falls back and continues in non-strict mode when parser runtime is unavailable', async () => {
|
|
24
|
+
const result = await parseFilesWithDiagnostics(
|
|
25
|
+
['src/example.py'],
|
|
26
|
+
'/project',
|
|
27
|
+
async () => 'def run():\n return 1\n',
|
|
28
|
+
{
|
|
29
|
+
strictParserPreflight: false,
|
|
30
|
+
treeSitterRuntimeAvailable: false,
|
|
31
|
+
},
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
expect(result.files).toHaveLength(1)
|
|
35
|
+
expect(result.summary.diagnostics).toBeGreaterThan(0)
|
|
36
|
+
expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(true)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
it('skips tree-sitter preflight when only oxc/go files are requested', async () => {
|
|
40
|
+
const result = await parseFilesWithDiagnostics(
|
|
41
|
+
['src/index.ts'],
|
|
42
|
+
'/project',
|
|
43
|
+
async () => 'export function ok(): number { return 1 }',
|
|
44
|
+
{
|
|
45
|
+
strictParserPreflight: true,
|
|
46
|
+
treeSitterRuntimeAvailable: false,
|
|
47
|
+
},
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
expect(result.summary.requestedFiles).toBe(1)
|
|
51
|
+
expect(result.summary.parsedFiles).toBe(1)
|
|
52
|
+
expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(false)
|
|
53
|
+
})
|
|
54
|
+
|
|
55
|
+
it('returns language-correct fallback files for multiple tree-sitter languages in non-strict mode', async () => {
|
|
56
|
+
const files = [
|
|
57
|
+
'src/main.py',
|
|
58
|
+
'src/App.java',
|
|
59
|
+
'src/service.kt',
|
|
60
|
+
'src/tool.swift',
|
|
61
|
+
'src/lib.rs',
|
|
62
|
+
'src/Program.cs',
|
|
63
|
+
'src/main.php',
|
|
64
|
+
'src/app.rb',
|
|
65
|
+
'src/native.c',
|
|
66
|
+
'src/native.cpp',
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
const result = await parseFilesWithDiagnostics(
|
|
70
|
+
files,
|
|
71
|
+
'/project',
|
|
72
|
+
async () => '',
|
|
73
|
+
{
|
|
74
|
+
strictParserPreflight: false,
|
|
75
|
+
treeSitterRuntimeAvailable: false,
|
|
76
|
+
},
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
expect(result.files).toHaveLength(files.length)
|
|
80
|
+
expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(true)
|
|
81
|
+
|
|
82
|
+
const languageBySuffix = (suffix: string): string | undefined => {
|
|
83
|
+
const hit = result.files.find(f => f.path.replace(/\\/g, '/').endsWith(suffix))
|
|
84
|
+
return hit?.language
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
expect(languageBySuffix('/src/main.py')).toBe('python')
|
|
88
|
+
expect(languageBySuffix('/src/App.java')).toBe('java')
|
|
89
|
+
expect(languageBySuffix('/src/service.kt')).toBe('kotlin')
|
|
90
|
+
expect(languageBySuffix('/src/tool.swift')).toBe('swift')
|
|
91
|
+
expect(languageBySuffix('/src/lib.rs')).toBe('rust')
|
|
92
|
+
expect(languageBySuffix('/src/Program.cs')).toBe('csharp')
|
|
93
|
+
expect(languageBySuffix('/src/main.php')).toBe('php')
|
|
94
|
+
expect(languageBySuffix('/src/app.rb')).toBe('ruby')
|
|
95
|
+
expect(languageBySuffix('/src/native.c')).toBe('c')
|
|
96
|
+
expect(languageBySuffix('/src/native.cpp')).toBe('cpp')
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
it('aborts full batch in strict preflight when any tree-sitter file is present', async () => {
|
|
100
|
+
const result = await parseFilesWithDiagnostics(
|
|
101
|
+
['src/index.ts', 'src/main.py'],
|
|
102
|
+
'/project',
|
|
103
|
+
async () => 'export const ok = 1',
|
|
104
|
+
{
|
|
105
|
+
strictParserPreflight: true,
|
|
106
|
+
treeSitterRuntimeAvailable: false,
|
|
107
|
+
},
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
expect(result.files).toHaveLength(0)
|
|
111
|
+
expect(result.summary.requestedFiles).toBe(2)
|
|
112
|
+
expect(result.summary.parsedFiles).toBe(0)
|
|
113
|
+
expect(result.diagnostics.some(d => d.reason === 'parser-unavailable')).toBe(true)
|
|
114
|
+
})
|
|
115
|
+
})
|
package/tests/parser.test.ts
CHANGED
|
@@ -729,6 +729,26 @@ describe('getParser - Comprehensive', () => {
|
|
|
729
729
|
const parser = getParser('src/lib/utils/helper.ts')
|
|
730
730
|
expect(parser).toBeInstanceOf(OxcParser)
|
|
731
731
|
})
|
|
732
|
+
|
|
733
|
+
it('supports C++ variant extensions via tree-sitter parser', () => {
|
|
734
|
+
const cxxParser = getParser('src/engine.cxx')
|
|
735
|
+
const hxxParser = getParser('src/engine.hxx')
|
|
736
|
+
const hhParser = getParser('src/engine.hh')
|
|
737
|
+
|
|
738
|
+
expect(cxxParser.getSupportedExtensions()).toContain('.cxx')
|
|
739
|
+
expect(hxxParser.getSupportedExtensions()).toContain('.hxx')
|
|
740
|
+
expect(hhParser.getSupportedExtensions()).toContain('.hh')
|
|
741
|
+
})
|
|
742
|
+
|
|
743
|
+
it('supports Rust/C#/Swift extensions via tree-sitter parser', () => {
|
|
744
|
+
const rustParser = getParser('src/lib.rs')
|
|
745
|
+
const csharpParser = getParser('src/Program.cs')
|
|
746
|
+
const swiftParser = getParser('Sources/App/main.swift')
|
|
747
|
+
|
|
748
|
+
expect(rustParser.getSupportedExtensions()).toContain('.rs')
|
|
749
|
+
expect(csharpParser.getSupportedExtensions()).toContain('.cs')
|
|
750
|
+
expect(swiftParser.getSupportedExtensions()).toContain('.swift')
|
|
751
|
+
})
|
|
732
752
|
})
|
|
733
753
|
|
|
734
754
|
describe('OxcParser - Direct', () => {
|
|
@@ -751,4 +771,20 @@ describe('OxcParser - Direct', () => {
|
|
|
751
771
|
const result = await parser.parse('large.ts', content)
|
|
752
772
|
expect(result.functions.length).toBe(1000)
|
|
753
773
|
})
|
|
774
|
+
|
|
775
|
+
it('extracts direct and method call expressions', async () => {
|
|
776
|
+
const result = await parser.parse('calls.ts', `
|
|
777
|
+
function b() { return 1 }
|
|
778
|
+
const svc = { run() { return 2 } }
|
|
779
|
+
export function a() {
|
|
780
|
+
b()
|
|
781
|
+
svc.run()
|
|
782
|
+
}
|
|
783
|
+
`)
|
|
784
|
+
|
|
785
|
+
const a = result.functions.find(f => f.name === 'a')
|
|
786
|
+
expect(a).toBeDefined()
|
|
787
|
+
expect(a!.calls.some(c => c.name === 'b')).toBe(true)
|
|
788
|
+
expect(a!.calls.some(c => c.name === 'svc.run')).toBe(true)
|
|
789
|
+
})
|
|
754
790
|
})
|
|
@@ -268,6 +268,34 @@ struct PrivateStruct {
|
|
|
268
268
|
expect(pubStruct?.isExported).toBe(true)
|
|
269
269
|
expect(privStruct?.isExported).toBe(false)
|
|
270
270
|
})
|
|
271
|
+
|
|
272
|
+
test('parses Rust traits and impl blocks', async () => {
|
|
273
|
+
const content = `
|
|
274
|
+
pub trait UserRepository {
|
|
275
|
+
fn find_user(&self, id: String) -> Option<String>;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
pub struct InMemoryRepo;
|
|
279
|
+
|
|
280
|
+
impl UserRepository for InMemoryRepo {
|
|
281
|
+
fn find_user(&self, id: String) -> Option<String> {
|
|
282
|
+
Some(id)
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
`
|
|
286
|
+
const result = await parser.parse('repository.rs', content)
|
|
287
|
+
|
|
288
|
+
expect(result.path).toBe('repository.rs')
|
|
289
|
+
expect(result.language).toBe('rust')
|
|
290
|
+
|
|
291
|
+
if (result.classes.length > 0) {
|
|
292
|
+
expect(result.classes.some(c => c.name === 'UserRepository')).toBe(true)
|
|
293
|
+
expect(result.classes.some(c => c.name === 'InMemoryRepo')).toBe(true)
|
|
294
|
+
}
|
|
295
|
+
if (result.functions.length > 0) {
|
|
296
|
+
expect(result.functions.some(f => f.name === 'find_user')).toBe(true)
|
|
297
|
+
}
|
|
298
|
+
})
|
|
271
299
|
})
|
|
272
300
|
|
|
273
301
|
// ==========================================
|
|
@@ -337,6 +365,31 @@ public:
|
|
|
337
365
|
expect(result.classes[0].name).toBe('Calculator')
|
|
338
366
|
expect(result.functions.length).toBeGreaterThanOrEqual(1)
|
|
339
367
|
})
|
|
368
|
+
|
|
369
|
+
test('parses C++ templates with macro-heavy headers', async () => {
|
|
370
|
+
const content = `
|
|
371
|
+
#ifndef VECTOR_UTILS_H
|
|
372
|
+
#define VECTOR_UTILS_H
|
|
373
|
+
|
|
374
|
+
#include <vector>
|
|
375
|
+
|
|
376
|
+
#define INLINE inline
|
|
377
|
+
|
|
378
|
+
template <typename T>
|
|
379
|
+
INLINE T max_value(const T& a, const T& b) {
|
|
380
|
+
return a > b ? a : b;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
#endif
|
|
384
|
+
`
|
|
385
|
+
const result = await parser.parse('vector_utils.hpp', content)
|
|
386
|
+
|
|
387
|
+
expect(result.path).toBe('vector_utils.hpp')
|
|
388
|
+
expect(result.language).toBe('cpp')
|
|
389
|
+
if (result.functions.length > 0) {
|
|
390
|
+
expect(result.functions.some(f => f.name === 'max_value')).toBe(true)
|
|
391
|
+
}
|
|
392
|
+
})
|
|
340
393
|
})
|
|
341
394
|
|
|
342
395
|
// ==========================================
|
|
@@ -393,6 +446,36 @@ class VisibilityTest {
|
|
|
393
446
|
expect(priv?.isExported).toBe(false)
|
|
394
447
|
expect(prot?.isExported).toBe(false)
|
|
395
448
|
})
|
|
449
|
+
|
|
450
|
+
test('parses framework-style PHP controller classes', async () => {
|
|
451
|
+
const content = `
|
|
452
|
+
<?php
|
|
453
|
+
namespace App\\Http\\Controllers;
|
|
454
|
+
|
|
455
|
+
use Illuminate\\Http\\Request;
|
|
456
|
+
|
|
457
|
+
class UserController extends Controller {
|
|
458
|
+
public function index(Request $request): array {
|
|
459
|
+
return [];
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
public function show(string $id): array {
|
|
463
|
+
return ['id' => $id];
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
`
|
|
467
|
+
const result = await parser.parse('UserController.php', content)
|
|
468
|
+
|
|
469
|
+
expect(result.path).toBe('UserController.php')
|
|
470
|
+
expect(result.language).toBe('php')
|
|
471
|
+
if (result.classes.length > 0) {
|
|
472
|
+
expect(result.classes.some(c => c.name === 'UserController')).toBe(true)
|
|
473
|
+
}
|
|
474
|
+
if (result.functions.length > 0) {
|
|
475
|
+
expect(result.functions.some(f => f.name === 'index')).toBe(true)
|
|
476
|
+
expect(result.functions.some(f => f.name === 'show')).toBe(true)
|
|
477
|
+
}
|
|
478
|
+
})
|
|
396
479
|
})
|
|
397
480
|
|
|
398
481
|
// ==========================================
|
|
@@ -432,6 +515,93 @@ namespace App.Services {
|
|
|
432
515
|
expect(result.path).toBe('UserService.cs')
|
|
433
516
|
}
|
|
434
517
|
})
|
|
518
|
+
|
|
519
|
+
test('parses C# attributes and ASP.NET controller routes', async () => {
|
|
520
|
+
const content = `
|
|
521
|
+
using Microsoft.AspNetCore.Mvc;
|
|
522
|
+
|
|
523
|
+
namespace App.Api.Controllers {
|
|
524
|
+
[ApiController]
|
|
525
|
+
[Route("api/[controller]")]
|
|
526
|
+
public class UsersController : ControllerBase {
|
|
527
|
+
[HttpGet("{id}")]
|
|
528
|
+
public ActionResult<string> GetById(string id) {
|
|
529
|
+
return id;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
[HttpPost]
|
|
533
|
+
public IActionResult Create([FromBody] object payload) {
|
|
534
|
+
return Ok(payload);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
`
|
|
539
|
+
const result = await parser.parse('UsersController.cs', content)
|
|
540
|
+
|
|
541
|
+
expect(result.path).toBe('UsersController.cs')
|
|
542
|
+
expect(result.language).toBe('csharp')
|
|
543
|
+
if (result.functions.length > 0) {
|
|
544
|
+
expect(result.functions.some(f => f.name === 'GetById')).toBe(true)
|
|
545
|
+
expect(result.functions.some(f => f.name === 'Create')).toBe(true)
|
|
546
|
+
}
|
|
547
|
+
})
|
|
548
|
+
})
|
|
549
|
+
|
|
550
|
+
describe('Kotlin - Coroutines & Extensions', () => {
|
|
551
|
+
test('parses Kotlin coroutine and extension functions', async () => {
|
|
552
|
+
const content = `
|
|
553
|
+
package app.service
|
|
554
|
+
|
|
555
|
+
class UserService {
|
|
556
|
+
suspend fun fetchUser(id: String): String {
|
|
557
|
+
return id
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
fun String.slugify(): String {
|
|
562
|
+
return this.lowercase().replace(" ", "-")
|
|
563
|
+
}
|
|
564
|
+
`
|
|
565
|
+
let result
|
|
566
|
+
try {
|
|
567
|
+
result = await parser.parse('UserService.kt', content)
|
|
568
|
+
} catch {
|
|
569
|
+
result = { path: 'UserService.kt', language: 'kotlin', functions: [], classes: [] }
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
expect(result.path).toBe('UserService.kt')
|
|
573
|
+
expect(result.language).toBe('kotlin')
|
|
574
|
+
if (result.functions.length > 0) {
|
|
575
|
+
expect(result.functions.some(f => f.name === 'fetchUser')).toBe(true)
|
|
576
|
+
expect(result.functions.some(f => f.name === 'slugify')).toBe(true)
|
|
577
|
+
}
|
|
578
|
+
})
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
describe('Swift - Protocol & Package Patterns', () => {
|
|
582
|
+
test('parses Swift protocol-based service code', async () => {
|
|
583
|
+
const content = `
|
|
584
|
+
import Foundation
|
|
585
|
+
|
|
586
|
+
protocol UserRepository {
|
|
587
|
+
func findUser(id: String) -> String?
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
struct InMemoryUserRepository: UserRepository {
|
|
591
|
+
func findUser(id: String) -> String? {
|
|
592
|
+
return id
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
`
|
|
596
|
+
const result = await parser.parse('Sources/App/UserRepository.swift', content)
|
|
597
|
+
|
|
598
|
+
expect(result.path).toBe('Sources/App/UserRepository.swift')
|
|
599
|
+
expect(result.language).toBe('swift')
|
|
600
|
+
if (result.classes.length > 0) {
|
|
601
|
+
expect(result.classes.some(c => c.name === 'UserRepository')).toBe(true)
|
|
602
|
+
expect(result.classes.some(c => c.name === 'InMemoryUserRepository')).toBe(true)
|
|
603
|
+
}
|
|
604
|
+
})
|
|
435
605
|
})
|
|
436
606
|
|
|
437
607
|
// ==========================================
|
|
@@ -473,6 +643,30 @@ end
|
|
|
473
643
|
|
|
474
644
|
expect(result.path).toBe('auth.rb')
|
|
475
645
|
})
|
|
646
|
+
|
|
647
|
+
test('parses Ruby DSL-heavy model patterns', async () => {
|
|
648
|
+
const content = `
|
|
649
|
+
class User < ApplicationRecord
|
|
650
|
+
scope :active, -> { where(active: true) }
|
|
651
|
+
validates :email, presence: true
|
|
652
|
+
|
|
653
|
+
def full_name
|
|
654
|
+
"#{first_name} #{last_name}"
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
`
|
|
658
|
+
let result
|
|
659
|
+
try {
|
|
660
|
+
result = await parser.parse('user.rb', content)
|
|
661
|
+
} catch {
|
|
662
|
+
result = { path: 'user.rb', language: 'ruby', functions: [], classes: [] }
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
expect(result.path).toBe('user.rb')
|
|
666
|
+
if (result.functions.length > 0) {
|
|
667
|
+
expect(result.functions.some((f: { name: string }) => f.name === 'full_name')).toBe(true)
|
|
668
|
+
}
|
|
669
|
+
})
|
|
476
670
|
})
|
|
477
671
|
|
|
478
672
|
// ==========================================
|
|
@@ -869,5 +1063,12 @@ raw = r"raw \\string"
|
|
|
869
1063
|
|
|
870
1064
|
expect(result.imports.length).toBeGreaterThan(0)
|
|
871
1065
|
})
|
|
1066
|
+
|
|
1067
|
+
test('produces identical hash for identical content', async () => {
|
|
1068
|
+
const content = 'def stable():\n return 42\n'
|
|
1069
|
+
const a = await parser.parse('stable.py', content)
|
|
1070
|
+
const b = await parser.parse('stable.py', content)
|
|
1071
|
+
expect(a.hash).toBe(b.hash)
|
|
1072
|
+
})
|
|
872
1073
|
})
|
|
873
1074
|
})
|
package/tests/ts-parser.test.ts
CHANGED
|
@@ -113,4 +113,10 @@ describe('TypeScriptParser Edge Cases & Fault Tolerance', () => {
|
|
|
113
113
|
expect(result.functions).toHaveLength(0)
|
|
114
114
|
expect(result.hash).toBeDefined()
|
|
115
115
|
})
|
|
116
|
+
|
|
117
|
+
it('parses Windows line endings consistently', async () => {
|
|
118
|
+
const winCode = 'export function ping() {\r\n return 1\r\n}\r\n'
|
|
119
|
+
const result = await parser.parse('src/win.ts', winCode)
|
|
120
|
+
expect(result.functions.some(f => f.name === 'ping')).toBe(true)
|
|
121
|
+
})
|
|
116
122
|
})
|