@getmikk/core 1.7.1 → 1.8.1

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.
@@ -1,19 +1,28 @@
1
1
  /**
2
- * Simple minimatch-like glob matching utility.
3
- * Supports ** (any depth directory) and * (wildcard) patterns.
2
+ * minimatch glob matching with plain-path prefix fallback.
3
+ *
4
+ * Rules:
5
+ * - Pattern with no glob chars (*, ?, {, [) → directory prefix match
6
+ * "src/auth" matches "src/auth/jwt.ts" and "src/auth" itself
7
+ * - "**" matches any depth
8
+ * - "*" matches within a single directory segment
4
9
  */
5
10
  export function minimatch(filePath: string, pattern: string): boolean {
6
- // Normalize both to forward slashes
7
- const normalizedPath = filePath.replace(/\\/g, '/')
8
- const normalizedPattern = pattern.replace(/\\/g, '/')
11
+ const normalizedPath = filePath.replace(/\\/g, '/')
12
+ const normalizedPattern = pattern.replace(/\\/g, '/')
9
13
 
10
- // Convert glob pattern to regex
11
- const regexStr = normalizedPattern
12
- .replace(/\./g, '\\.')
13
- .replace(/\*\*\//g, '(?:.+/)?')
14
- .replace(/\*\*/g, '.*')
15
- .replace(/\*/g, '[^/]*')
14
+ // Plain path (no glob chars) prefix match
15
+ if (!/[*?{[]/.test(normalizedPattern)) {
16
+ const bare = normalizedPattern.replace(/\/$/, '')
17
+ return normalizedPath === bare || normalizedPath.startsWith(bare + '/')
18
+ }
16
19
 
17
- const regex = new RegExp(`^${regexStr}$`)
18
- return regex.test(normalizedPath)
19
- }
20
+ // Convert glob to regex
21
+ const regexStr = normalizedPattern
22
+ .replace(/\./g, '\\.')
23
+ .replace(/\*\*\//g, '(?:.+/)?')
24
+ .replace(/\*\*/g, '.*')
25
+ .replace(/\*/g, '[^/]*')
26
+
27
+ return new RegExp(`^${regexStr}$`).test(normalizedPath)
28
+ }
package/test-output.txt CHANGED
Binary file
@@ -320,9 +320,9 @@ describe('GoParser', () => {
320
320
  expect(parser.getSupportedExtensions()).toContain('.go')
321
321
  })
322
322
 
323
- test('parse returns a well-formed ParsedFile', () => {
323
+ test('parse returns a well-formed ParsedFile', async () => {
324
324
  const parser = new GoParser()
325
- const result = parser.parse('auth/service.go', SIMPLE_GO)
325
+ const result = await parser.parse('auth/service.go', SIMPLE_GO)
326
326
  expect(result.path).toBe('auth/service.go')
327
327
  expect(result.language).toBe('go')
328
328
  expect(Array.isArray(result.functions)).toBe(true)
@@ -334,31 +334,31 @@ describe('GoParser', () => {
334
334
  expect(result.hash.length).toBeGreaterThan(0)
335
335
  })
336
336
 
337
- test('parse populates functions from a real Go service', () => {
337
+ test('parse populates functions from a real Go service', async () => {
338
338
  const parser = new GoParser()
339
- const result = parser.parse('auth/service.go', SIMPLE_GO)
339
+ const result = await parser.parse('auth/service.go', SIMPLE_GO)
340
340
  // Top-level funcs: hashPassword, keyFunc
341
341
  expect(result.functions.some(f => f.name === 'hashPassword')).toBe(true)
342
342
  expect(result.functions.some(f => f.name === 'keyFunc')).toBe(true)
343
343
  })
344
344
 
345
- test('parse populates classes for structs with methods', () => {
345
+ test('parse populates classes for structs with methods', async () => {
346
346
  const parser = new GoParser()
347
- const result = parser.parse('auth/service.go', SIMPLE_GO)
347
+ const result = await parser.parse('auth/service.go', SIMPLE_GO)
348
348
  const authService = result.classes.find(c => c.name === 'AuthService')
349
349
  expect(authService).toBeDefined()
350
350
  expect(authService!.methods.length).toBeGreaterThan(0)
351
351
  })
352
352
 
353
- test('parse detects routes in a Gin router file', () => {
353
+ test('parse detects routes in a Gin router file', async () => {
354
354
  const parser = new GoParser()
355
- const result = parser.parse('api/routes.go', ROUTES_GO)
355
+ const result = await parser.parse('api/routes.go', ROUTES_GO)
356
356
  expect(result.routes.length).toBeGreaterThanOrEqual(4)
357
357
  })
358
358
 
359
- test('resolveImports passes through without crashing on no go.mod', () => {
359
+ test('resolveImports passes through without crashing on no go.mod', async () => {
360
360
  const parser = new GoParser()
361
- const files = [parser.parse('utils/format.go', TOPLEVEL_GO)]
361
+ const files = [await parser.parse('utils/format.go', TOPLEVEL_GO)]
362
362
  // Should not throw even without go.mod
363
363
  const resolved = parser.resolveImports(files, '/tmp/no-gomod-' + Date.now())
364
364
  expect(resolved.length).toBe(1)
@@ -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 .py', () => {
614
- expect(() => getParser('src/app.py')).toThrow()
628
+ test('still throws UnsupportedLanguageError for .xyz', () => {
629
+ expect(() => getParser('src/app.xyz')).toThrow()
615
630
  })
616
631
  })
@@ -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.rb')).toThrow(UnsupportedLanguageError)
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
+ })
@@ -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