@getmikk/core 1.7.0 → 1.8.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 +82 -412
- package/package.json +3 -1
- package/src/contract/contract-reader.ts +2 -2
- package/src/contract/lock-compiler.ts +15 -14
- package/src/contract/lock-reader.ts +14 -14
- package/src/contract/schema.ts +3 -3
- package/src/index.ts +1 -0
- package/src/parser/base-parser.ts +1 -1
- package/src/parser/boundary-checker.ts +74 -212
- package/src/parser/go/go-extractor.ts +10 -10
- package/src/parser/go/go-parser.ts +2 -2
- package/src/parser/index.ts +45 -31
- package/src/parser/javascript/js-extractor.ts +9 -9
- package/src/parser/javascript/js-parser.ts +2 -2
- package/src/parser/tree-sitter/parser.ts +228 -0
- package/src/parser/tree-sitter/queries.ts +181 -0
- package/src/parser/types.ts +1 -1
- package/src/parser/typescript/ts-extractor.ts +15 -15
- package/src/parser/typescript/ts-parser.ts +1 -1
- package/src/parser/typescript/ts-resolver.ts +2 -2
- package/src/search/bm25.ts +206 -0
- package/src/search/index.ts +3 -0
- package/src/utils/fs.ts +31 -31
- package/src/utils/minimatch.ts +23 -14
- package/test-output.txt +0 -0
- package/tests/go-parser.test.ts +10 -10
- package/tests/js-parser.test.ts +34 -19
- package/tests/parser.test.ts +5 -5
- package/tests/tree-sitter-parser.test.ts +168 -0
- package/tests/ts-parser.test.ts +49 -1
- package/out.log +0 -0
package/tests/js-parser.test.ts
CHANGED
|
@@ -468,6 +468,21 @@ describe('JavaScriptExtractor', () => {
|
|
|
468
468
|
const greetExports = exports.filter(e => e.name === 'greet')
|
|
469
469
|
expect(greetExports.length).toBe(1)
|
|
470
470
|
})
|
|
471
|
+
|
|
472
|
+
test('malformed code gracefully degrades without crashing', () => {
|
|
473
|
+
const malformed = `
|
|
474
|
+
function breakMe() {
|
|
475
|
+
const x =
|
|
476
|
+
if (true) {
|
|
477
|
+
// missing braces, missing assignments
|
|
478
|
+
`
|
|
479
|
+
const ext = new JavaScriptExtractor('src/malformed.js', malformed)
|
|
480
|
+
// TS compiler API is very fault tolerant, so it might extract breakMe anyway,
|
|
481
|
+
// but the key assertion is that it doesn't throw.
|
|
482
|
+
expect(() => ext.extractFunctions()).not.toThrow()
|
|
483
|
+
const fn = ext.extractFunctions().find(f => f.name === 'breakMe')
|
|
484
|
+
expect(fn).toBeDefined()
|
|
485
|
+
})
|
|
471
486
|
})
|
|
472
487
|
})
|
|
473
488
|
|
|
@@ -482,47 +497,47 @@ describe('JavaScriptParser', () => {
|
|
|
482
497
|
expect(exts).toContain('.jsx')
|
|
483
498
|
})
|
|
484
499
|
|
|
485
|
-
test('parse returns language: javascript', () => {
|
|
486
|
-
const result = parser.parse('src/index.js', CJS_MODULE)
|
|
500
|
+
test('parse returns language: javascript', async () => {
|
|
501
|
+
const result = await parser.parse('src/index.js', CJS_MODULE)
|
|
487
502
|
expect(result.language).toBe('javascript')
|
|
488
503
|
})
|
|
489
504
|
|
|
490
|
-
test('parse includes hash and parsedAt', () => {
|
|
491
|
-
const result = parser.parse('src/index.js', CJS_MODULE)
|
|
505
|
+
test('parse includes hash and parsedAt', async () => {
|
|
506
|
+
const result = await parser.parse('src/index.js', CJS_MODULE)
|
|
492
507
|
expect(typeof result.hash).toBe('string')
|
|
493
508
|
expect(result.hash.length).toBe(64) // SHA-256 hex
|
|
494
509
|
expect(typeof result.parsedAt).toBe('number')
|
|
495
510
|
})
|
|
496
511
|
|
|
497
|
-
test('CJS-exported functions are marked isExported via cross-reference', () => {
|
|
498
|
-
const result = parser.parse('src/auth.js', CJS_MODULE)
|
|
499
|
-
const hashPw = result.functions.find(f => f.name === 'hashPassword')
|
|
512
|
+
test('CJS-exported functions are marked isExported via cross-reference', async () => {
|
|
513
|
+
const result = await parser.parse('src/auth.js', CJS_MODULE)
|
|
514
|
+
const hashPw = result.functions.find((f: any) => f.name === 'hashPassword')
|
|
500
515
|
expect(hashPw).toBeDefined()
|
|
501
516
|
expect(hashPw!.isExported).toBe(true)
|
|
502
517
|
})
|
|
503
518
|
|
|
504
|
-
test('resolveImports resolves relative paths with .js extension probing', () => {
|
|
505
|
-
const files = [
|
|
519
|
+
test('resolveImports resolves relative paths with .js extension probing', async () => {
|
|
520
|
+
const files = await Promise.all([
|
|
506
521
|
parser.parse('src/auth.js', CJS_MODULE),
|
|
507
522
|
parser.parse('src/loader.js', ESM_MODULE),
|
|
508
|
-
]
|
|
523
|
+
])
|
|
509
524
|
const resolved = parser.resolveImports(files, '/project')
|
|
510
|
-
const authFile = resolved.find(f => f.path === 'src/auth.js')!
|
|
511
|
-
const dbImport = authFile.imports.find(i => i.source === './db')
|
|
525
|
+
const authFile = resolved.find((f: any) => f.path === 'src/auth.js')!
|
|
526
|
+
const dbImport = authFile.imports.find((i: any) => i.source === './db')
|
|
512
527
|
expect(dbImport!.resolvedPath).toMatch(/src\/db/)
|
|
513
528
|
expect(dbImport!.resolvedPath).toMatch(/\.js$/)
|
|
514
529
|
})
|
|
515
530
|
|
|
516
|
-
test('resolveImports leaves external packages unresolved (empty resolvedPath)', () => {
|
|
517
|
-
const files = [parser.parse('src/auth.js', CJS_MODULE)]
|
|
531
|
+
test('resolveImports leaves external packages unresolved (empty resolvedPath)', async () => {
|
|
532
|
+
const files = [await parser.parse('src/auth.js', CJS_MODULE)]
|
|
518
533
|
const resolved = parser.resolveImports(files, '/project')
|
|
519
534
|
const file = resolved[0]
|
|
520
|
-
const cryptoImp = file.imports.find(i => i.source === 'crypto')
|
|
535
|
+
const cryptoImp = file.imports.find((i: any) => i.source === 'crypto')
|
|
521
536
|
expect(cryptoImp!.resolvedPath).toBe('')
|
|
522
537
|
})
|
|
523
538
|
|
|
524
|
-
test('parse .jsx file language is javascript', () => {
|
|
525
|
-
const result = parser.parse('src/UserCard.jsx', JSX_COMPONENT)
|
|
539
|
+
test('parse .jsx file language is javascript', async () => {
|
|
540
|
+
const result = await parser.parse('src/UserCard.jsx', JSX_COMPONENT)
|
|
526
541
|
expect(result.language).toBe('javascript')
|
|
527
542
|
})
|
|
528
543
|
})
|
|
@@ -610,7 +625,7 @@ describe('getParser — JS extensions', () => {
|
|
|
610
625
|
expect(p.getSupportedExtensions()).toContain('.ts')
|
|
611
626
|
})
|
|
612
627
|
|
|
613
|
-
test('still throws UnsupportedLanguageError for .
|
|
614
|
-
expect(() => getParser('src/app.
|
|
628
|
+
test('still throws UnsupportedLanguageError for .xyz', () => {
|
|
629
|
+
expect(() => getParser('src/app.xyz')).toThrow()
|
|
615
630
|
})
|
|
616
631
|
})
|
package/tests/parser.test.ts
CHANGED
|
@@ -134,13 +134,13 @@ describe('TypeScriptExtractor', () => {
|
|
|
134
134
|
describe('TypeScriptParser', () => {
|
|
135
135
|
const parser = new TypeScriptParser()
|
|
136
136
|
|
|
137
|
-
it('returns correct language', () => {
|
|
138
|
-
const result = parser.parse('src/test.ts', 'const x = 1')
|
|
137
|
+
it('returns correct language', async () => {
|
|
138
|
+
const result = await parser.parse('src/test.ts', 'const x = 1')
|
|
139
139
|
expect(result.language).toBe('typescript')
|
|
140
140
|
})
|
|
141
141
|
|
|
142
|
-
it('parses a complete file', () => {
|
|
143
|
-
const result = parser.parse('src/auth.ts', `
|
|
142
|
+
it('parses a complete file', async () => {
|
|
143
|
+
const result = await parser.parse('src/auth.ts', `
|
|
144
144
|
import { jwtDecode } from '../utils/jwt'
|
|
145
145
|
export function verifyToken(token: string): boolean {
|
|
146
146
|
return jwtDecode(token).exp > Date.now()
|
|
@@ -213,6 +213,6 @@ describe('getParser', () => {
|
|
|
213
213
|
})
|
|
214
214
|
|
|
215
215
|
it('throws for unsupported extensions', () => {
|
|
216
|
-
expect(() => getParser('src/auth.
|
|
216
|
+
expect(() => getParser('src/auth.xyz')).toThrow(UnsupportedLanguageError)
|
|
217
217
|
})
|
|
218
218
|
})
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { describe, expect, test } from 'bun:test'
|
|
2
|
+
import { TreeSitterParser } from '../src/parser/tree-sitter/parser.js'
|
|
3
|
+
import { resolve } from 'node:path'
|
|
4
|
+
|
|
5
|
+
describe('TreeSitterParser', () => {
|
|
6
|
+
test('parses Python correctly', async () => {
|
|
7
|
+
const parser = new TreeSitterParser()
|
|
8
|
+
const pyContent = `
|
|
9
|
+
import os
|
|
10
|
+
from sys import argv
|
|
11
|
+
|
|
12
|
+
class User:
|
|
13
|
+
def __init__(self, name: str):
|
|
14
|
+
self.name = name
|
|
15
|
+
|
|
16
|
+
def get_name(self) -> str:
|
|
17
|
+
return self.name
|
|
18
|
+
|
|
19
|
+
def main():
|
|
20
|
+
u = User("Alice")
|
|
21
|
+
print(u.get_name())
|
|
22
|
+
`
|
|
23
|
+
const result = await parser.parse('test.py', pyContent)
|
|
24
|
+
|
|
25
|
+
expect(result.classes.length).toBe(1)
|
|
26
|
+
expect(result.classes[0].name).toBe('User')
|
|
27
|
+
|
|
28
|
+
expect(result.functions.length).toBe(3) // __init__, get_name, main
|
|
29
|
+
const mainFn = result.functions.find((f: any) => f.name === 'main')
|
|
30
|
+
expect(mainFn).toBeDefined()
|
|
31
|
+
|
|
32
|
+
expect(result.imports.length).toBe(2)
|
|
33
|
+
expect(result.imports.map((i: any) => i.source)).toContain('os')
|
|
34
|
+
expect(result.imports.map((i: any) => i.source)).toContain('sys')
|
|
35
|
+
|
|
36
|
+
expect(result.functions[0].calls).toContain('User')
|
|
37
|
+
expect(result.functions[0].calls).toContain('print')
|
|
38
|
+
expect(result.functions[0].calls).toContain('get_name')
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
test('parses Java correctly', async () => {
|
|
42
|
+
const parser = new TreeSitterParser()
|
|
43
|
+
const javaContent = `
|
|
44
|
+
import java.util.List;
|
|
45
|
+
|
|
46
|
+
public class App {
|
|
47
|
+
public static void main(String[] args) {
|
|
48
|
+
System.out.println("Hello");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
private int calculate(int a, int b) {
|
|
52
|
+
return a + b;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
`
|
|
56
|
+
const result = await parser.parse('App.java', javaContent)
|
|
57
|
+
|
|
58
|
+
expect(result.classes.length).toBe(1)
|
|
59
|
+
expect(result.classes[0].name).toBe('App')
|
|
60
|
+
|
|
61
|
+
expect(result.functions.length).toBe(2) // main, calculate
|
|
62
|
+
|
|
63
|
+
expect(result.imports.length).toBe(1)
|
|
64
|
+
expect(result.imports[0].source).toBe('java.util.List')
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('parses Go correctly', async () => {
|
|
68
|
+
const parser = new TreeSitterParser()
|
|
69
|
+
const goContent = `
|
|
70
|
+
package main
|
|
71
|
+
|
|
72
|
+
import (
|
|
73
|
+
"fmt"
|
|
74
|
+
"net/http"
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
type Server struct {
|
|
78
|
+
port int
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
func (s *Server) Start() {
|
|
82
|
+
fmt.Println("Starting")
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
func main() {
|
|
86
|
+
s := Server{port: 8080}
|
|
87
|
+
s.Start()
|
|
88
|
+
}
|
|
89
|
+
`
|
|
90
|
+
const result = await parser.parse('main.go', goContent)
|
|
91
|
+
|
|
92
|
+
expect(result.classes.length).toBe(1)
|
|
93
|
+
// struct maps to class in our universal AST
|
|
94
|
+
expect(result.classes[0].name).toBe('Server')
|
|
95
|
+
|
|
96
|
+
expect(result.functions.length).toBe(2) // Start, main
|
|
97
|
+
|
|
98
|
+
expect(result.imports.length).toBe(2)
|
|
99
|
+
expect(result.imports.map((i: any) => i.source)).toContain('fmt')
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
// EDGE CASES & ERROR HANDLING
|
|
103
|
+
|
|
104
|
+
test('handles empty files gracefully', async () => {
|
|
105
|
+
const parser = new TreeSitterParser()
|
|
106
|
+
const result = await parser.parse('empty.py', '')
|
|
107
|
+
|
|
108
|
+
expect(result.functions.length).toBe(0)
|
|
109
|
+
expect(result.classes.length).toBe(0)
|
|
110
|
+
expect(result.imports.length).toBe(0)
|
|
111
|
+
expect(result.path).toBe('empty.py')
|
|
112
|
+
expect(result.language).toBe('python')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
test('handles syntax errors in Python code gracefully', async () => {
|
|
116
|
+
const parser = new TreeSitterParser()
|
|
117
|
+
const badPyContent = `
|
|
118
|
+
def good_function():
|
|
119
|
+
print("This is fine")
|
|
120
|
+
|
|
121
|
+
def bad_function(
|
|
122
|
+
print("Missing closing paren and colon"
|
|
123
|
+
|
|
124
|
+
class GoodClass:
|
|
125
|
+
pass
|
|
126
|
+
`
|
|
127
|
+
// Tree-sitter is fault-tolerant, so it should not crash.
|
|
128
|
+
const result = await parser.parse('malformed.py', badPyContent)
|
|
129
|
+
|
|
130
|
+
// We assert that it successfully returns a ParsedFile object without throwing any errors
|
|
131
|
+
expect(result.path).toBe('malformed.py')
|
|
132
|
+
expect(result.language).toBe('python')
|
|
133
|
+
expect(Array.isArray(result.functions)).toBe(true)
|
|
134
|
+
expect(Array.isArray(result.classes)).toBe(true)
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
test('handles unsupported extensions gracefully', async () => {
|
|
138
|
+
const parser = new TreeSitterParser()
|
|
139
|
+
const result = await parser.parse('unknown.xyz', 'some weird content')
|
|
140
|
+
|
|
141
|
+
// Should return a fallback ParsedFile without crashing
|
|
142
|
+
expect(result.functions.length).toBe(0)
|
|
143
|
+
expect(result.classes.length).toBe(0)
|
|
144
|
+
// TreeSitter fallback is 'unknown' for unsupported extensions
|
|
145
|
+
expect(result.language).toBe('unknown')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
test('handles syntax errors in Java gracefully', async () => {
|
|
149
|
+
const parser = new TreeSitterParser()
|
|
150
|
+
const badJavaContent = `
|
|
151
|
+
public class Main {
|
|
152
|
+
public void goodMethod() {
|
|
153
|
+
int x = 5;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
public void badMethod( { // Syntax error here
|
|
157
|
+
System.out.println("Hello"
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
`
|
|
161
|
+
// Ensure no crash on parse
|
|
162
|
+
const result = await parser.parse('Main.java', badJavaContent)
|
|
163
|
+
|
|
164
|
+
expect(result.path).toBe('Main.java')
|
|
165
|
+
expect(result.language).toBe('java')
|
|
166
|
+
expect(Array.isArray(result.classes)).toBe(true)
|
|
167
|
+
})
|
|
168
|
+
})
|
package/tests/ts-parser.test.ts
CHANGED
|
@@ -74,7 +74,7 @@ describe('ts-parser config "extends" resolution', () => {
|
|
|
74
74
|
`)
|
|
75
75
|
|
|
76
76
|
// Parse and resolve imports
|
|
77
|
-
const parsed = parser.parse(filePath, await fs.readFile(filePath, 'utf-8'))
|
|
77
|
+
const parsed = await parser.parse(filePath, await fs.readFile(filePath, 'utf-8'))
|
|
78
78
|
const resolved = parser.resolveImports([parsed], FIXTURE_DIR)[0]
|
|
79
79
|
|
|
80
80
|
// Check if the aliases mapped correctly using all 3 layers of paths
|
|
@@ -91,3 +91,51 @@ describe('ts-parser config "extends" resolution', () => {
|
|
|
91
91
|
expect(impApp?.resolvedPath).toBe('src/app/local.ts')
|
|
92
92
|
})
|
|
93
93
|
})
|
|
94
|
+
|
|
95
|
+
describe('TypeScriptParser Edge Cases & Fault Tolerance', () => {
|
|
96
|
+
const parser = new TypeScriptParser()
|
|
97
|
+
|
|
98
|
+
it('handles completely empty files', async () => {
|
|
99
|
+
const result = await parser.parse('src/empty.ts', '')
|
|
100
|
+
expect(result.functions).toHaveLength(0)
|
|
101
|
+
expect(result.classes).toHaveLength(0)
|
|
102
|
+
expect(result.language).toBe('typescript')
|
|
103
|
+
expect(result.hash).toBeDefined()
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('gracefully degrades on severe syntax errors', async () => {
|
|
107
|
+
const malformedCode = `
|
|
108
|
+
export interface Broke {
|
|
109
|
+
val: string
|
|
110
|
+
// missing closing brace
|
|
111
|
+
|
|
112
|
+
function doThing() {
|
|
113
|
+
const x =
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@Injectable()
|
|
117
|
+
export class HalfClass implements {
|
|
118
|
+
`
|
|
119
|
+
const result = await parser.parse('src/broken.ts', malformedCode)
|
|
120
|
+
|
|
121
|
+
// Should not crash, and should extract whatever it can
|
|
122
|
+
expect(result.functions.length).toBeGreaterThanOrEqual(0)
|
|
123
|
+
expect(result.classes.length).toBeGreaterThanOrEqual(0)
|
|
124
|
+
// Must maintain object structure
|
|
125
|
+
expect(Array.isArray(result.imports)).toBe(true)
|
|
126
|
+
expect(typeof result.hash).toBe('string')
|
|
127
|
+
expect(result.language).toBe('typescript')
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
it('handles files with only comments', async () => {
|
|
131
|
+
const commentsCode = `
|
|
132
|
+
/**
|
|
133
|
+
* This file is just documentation
|
|
134
|
+
*/
|
|
135
|
+
// End of file
|
|
136
|
+
`
|
|
137
|
+
const result = await parser.parse('src/docs.ts', commentsCode)
|
|
138
|
+
expect(result.functions).toHaveLength(0)
|
|
139
|
+
expect(result.hash).toBeDefined()
|
|
140
|
+
})
|
|
141
|
+
})
|
package/out.log
DELETED
|
Binary file
|