legacyver 2.1.0 → 2.1.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.
Files changed (126) hide show
  1. package/.agent/skills/openspec-apply-change/SKILL.md +156 -0
  2. package/.agent/skills/openspec-archive-change/SKILL.md +114 -0
  3. package/.agent/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  4. package/.agent/skills/openspec-continue-change/SKILL.md +118 -0
  5. package/.agent/skills/openspec-explore/SKILL.md +290 -0
  6. package/.agent/skills/openspec-ff-change/SKILL.md +101 -0
  7. package/.agent/skills/openspec-new-change/SKILL.md +74 -0
  8. package/.agent/skills/openspec-onboard/SKILL.md +529 -0
  9. package/.agent/skills/openspec-sync-specs/SKILL.md +138 -0
  10. package/.agent/skills/openspec-verify-change/SKILL.md +168 -0
  11. package/.agent/workflows/opsx-apply.md +149 -0
  12. package/.agent/workflows/opsx-archive.md +154 -0
  13. package/.agent/workflows/opsx-bulk-archive.md +239 -0
  14. package/.agent/workflows/opsx-continue.md +111 -0
  15. package/.agent/workflows/opsx-explore.md +171 -0
  16. package/.agent/workflows/opsx-ff.md +91 -0
  17. package/.agent/workflows/opsx-new.md +66 -0
  18. package/.agent/workflows/opsx-onboard.md +522 -0
  19. package/.agent/workflows/opsx-sync.md +131 -0
  20. package/.agent/workflows/opsx-verify.md +161 -0
  21. package/.github/prompts/opsx-apply.prompt.md +149 -0
  22. package/.github/prompts/opsx-archive.prompt.md +154 -0
  23. package/.github/prompts/opsx-bulk-archive.prompt.md +239 -0
  24. package/.github/prompts/opsx-continue.prompt.md +111 -0
  25. package/.github/prompts/opsx-explore.prompt.md +171 -0
  26. package/.github/prompts/opsx-ff.prompt.md +91 -0
  27. package/.github/prompts/opsx-new.prompt.md +66 -0
  28. package/.github/prompts/opsx-onboard.prompt.md +522 -0
  29. package/.github/prompts/opsx-sync.prompt.md +131 -0
  30. package/.github/prompts/opsx-verify.prompt.md +161 -0
  31. package/.github/skills/openspec-apply-change/SKILL.md +156 -0
  32. package/.github/skills/openspec-archive-change/SKILL.md +114 -0
  33. package/.github/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  34. package/.github/skills/openspec-continue-change/SKILL.md +118 -0
  35. package/.github/skills/openspec-explore/SKILL.md +290 -0
  36. package/.github/skills/openspec-ff-change/SKILL.md +101 -0
  37. package/.github/skills/openspec-new-change/SKILL.md +74 -0
  38. package/.github/skills/openspec-onboard/SKILL.md +529 -0
  39. package/.github/skills/openspec-sync-specs/SKILL.md +138 -0
  40. package/.github/skills/openspec-verify-change/SKILL.md +168 -0
  41. package/.legacyverignore.example +43 -0
  42. package/.legacyverrc +7 -0
  43. package/.opencode/command/opsx-apply.md +149 -0
  44. package/.opencode/command/opsx-archive.md +154 -0
  45. package/.opencode/command/opsx-bulk-archive.md +239 -0
  46. package/.opencode/command/opsx-continue.md +111 -0
  47. package/.opencode/command/opsx-explore.md +171 -0
  48. package/.opencode/command/opsx-ff.md +91 -0
  49. package/.opencode/command/opsx-new.md +66 -0
  50. package/.opencode/command/opsx-onboard.md +522 -0
  51. package/.opencode/command/opsx-sync.md +131 -0
  52. package/.opencode/command/opsx-verify.md +161 -0
  53. package/.opencode/skills/openspec-apply-change/SKILL.md +156 -0
  54. package/.opencode/skills/openspec-archive-change/SKILL.md +114 -0
  55. package/.opencode/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  56. package/.opencode/skills/openspec-continue-change/SKILL.md +118 -0
  57. package/.opencode/skills/openspec-explore/SKILL.md +290 -0
  58. package/.opencode/skills/openspec-ff-change/SKILL.md +101 -0
  59. package/.opencode/skills/openspec-new-change/SKILL.md +74 -0
  60. package/.opencode/skills/openspec-onboard/SKILL.md +529 -0
  61. package/.opencode/skills/openspec-sync-specs/SKILL.md +138 -0
  62. package/.opencode/skills/openspec-verify-change/SKILL.md +168 -0
  63. package/LICENSE +1 -1
  64. package/README.md +128 -83
  65. package/bin/legacyver.js +48 -25
  66. package/legacyver-docs/SUMMARY.md +3 -0
  67. package/legacyver-docs/components.md +57 -0
  68. package/legacyver-docs/index.md +15 -0
  69. package/nul +2 -0
  70. package/package.json +23 -25
  71. package/src/cache/hash.js +9 -10
  72. package/src/cache/index.js +43 -65
  73. package/src/cli/commands/analyze.js +212 -190
  74. package/src/cli/commands/cache.js +15 -35
  75. package/src/cli/commands/init.js +63 -107
  76. package/src/cli/commands/providers.js +56 -81
  77. package/src/cli/commands/version.js +7 -10
  78. package/src/cli/ui.js +58 -77
  79. package/src/crawler/filters.js +41 -40
  80. package/src/crawler/index.js +52 -36
  81. package/src/crawler/manifest.js +31 -43
  82. package/src/crawler/walk.js +32 -38
  83. package/src/llm/chunker.js +34 -56
  84. package/src/llm/cost-estimator.js +68 -51
  85. package/src/llm/free-model.js +67 -0
  86. package/src/llm/index.js +22 -43
  87. package/src/llm/prompts.js +45 -33
  88. package/src/llm/providers/gemini.js +94 -0
  89. package/src/llm/providers/groq.js +55 -40
  90. package/src/llm/providers/ollama.js +38 -65
  91. package/src/llm/providers/openrouter.js +67 -0
  92. package/src/llm/queue.js +59 -88
  93. package/src/llm/re-prompter.js +41 -0
  94. package/src/llm/validator.js +72 -0
  95. package/src/parser/ast/generic.js +45 -222
  96. package/src/parser/ast/go.js +86 -205
  97. package/src/parser/ast/java.js +76 -146
  98. package/src/parser/ast/javascript.js +173 -241
  99. package/src/parser/ast/laravel/blade.js +56 -0
  100. package/src/parser/ast/laravel/classifier.js +30 -0
  101. package/src/parser/ast/laravel/controller.js +35 -0
  102. package/src/parser/ast/laravel/index.js +54 -0
  103. package/src/parser/ast/laravel/model.js +41 -0
  104. package/src/parser/ast/laravel/provider.js +28 -0
  105. package/src/parser/ast/laravel/routes.js +45 -0
  106. package/src/parser/ast/php.js +129 -0
  107. package/src/parser/ast/python.js +76 -199
  108. package/src/parser/ast/typescript.js +10 -244
  109. package/src/parser/body-extractor.js +40 -0
  110. package/src/parser/call-graph.js +50 -67
  111. package/src/parser/complexity-scorer.js +59 -0
  112. package/src/parser/index.js +61 -86
  113. package/src/parser/pattern-detector.js +71 -0
  114. package/src/parser/pkg-builder.js +36 -83
  115. package/src/renderer/html.js +63 -135
  116. package/src/renderer/index.js +23 -35
  117. package/src/renderer/json.js +17 -35
  118. package/src/renderer/markdown.js +83 -117
  119. package/src/utils/config.js +52 -53
  120. package/src/utils/errors.js +26 -41
  121. package/src/utils/logger.js +32 -53
  122. package/src/cli/flags.js +0 -87
  123. package/src/llm/providers/anthropic.js +0 -57
  124. package/src/llm/providers/google.js +0 -65
  125. package/src/llm/providers/openai.js +0 -52
  126. package/src/parser/ast/tree-sitter-init.js +0 -80
@@ -0,0 +1,28 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract Service Provider specifics.
5
+ */
6
+ function extract(sourceText, fileFacts) {
7
+ const laravelContext = { type: 'provider', registerBindings: [], bootActions: [] };
8
+
9
+ // register() method body
10
+ const registerFn = fileFacts.functions.find(f => f.name === 'register');
11
+ if (registerFn && registerFn.bodySnippet) {
12
+ const bindRegex = /\$this->app->(?:bind|singleton|instance|scoped)\s*\(\s*(['"])([\w\\]+)\1/g;
13
+ let m;
14
+ while ((m = bindRegex.exec(registerFn.bodySnippet)) !== null) {
15
+ laravelContext.registerBindings.push(m[2]);
16
+ }
17
+ }
18
+
19
+ // boot() method body
20
+ const bootFn = fileFacts.functions.find(f => f.name === 'boot');
21
+ if (bootFn && bootFn.bodySnippet) {
22
+ laravelContext.bootActions.push('boot() method present');
23
+ }
24
+
25
+ return laravelContext;
26
+ }
27
+
28
+ module.exports = { extract };
@@ -0,0 +1,45 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Extract route definitions from Laravel route files.
5
+ */
6
+ function extract(sourceText) {
7
+ const routes = [];
8
+
9
+ // Route::METHOD('uri', [Controller::class, 'method'])->middleware(...)->name(...)
10
+ const routeRegex = /Route::(get|post|put|patch|delete|options|any)\s*\(\s*['"]([^'"]+)['"]\s*,\s*(?:\[([^\]]+)\]|['"]([^'"]+)['"]|\$?(\w+))/gi;
11
+ let m;
12
+ while ((m = routeRegex.exec(sourceText)) !== null) {
13
+ const method = m[1].toUpperCase();
14
+ const uri = m[2];
15
+ let controller = null;
16
+ let action = null;
17
+
18
+ if (m[3]) {
19
+ // Array syntax: [Controller::class, 'method']
20
+ const parts = m[3].split(',').map(s => s.trim().replace(/['"]/g, '').replace(/::class$/, ''));
21
+ controller = parts[0] ? parts[0].split('\\').pop() : null;
22
+ action = parts[1] || null;
23
+ } else if (m[4]) {
24
+ // String 'Controller@method'
25
+ const parts = m[4].split('@');
26
+ controller = parts[0] ? parts[0].split('\\').pop() : null;
27
+ action = parts[1] || null;
28
+ }
29
+
30
+ // Extract middleware
31
+ const afterRoute = sourceText.slice(m.index, m.index + 200);
32
+ const middlewareMatch = afterRoute.match(/->middleware\s*\(\s*['"]([^'"]+)['"]\s*\)/);
33
+ const middleware = middlewareMatch ? middlewareMatch[1] : null;
34
+
35
+ // Extract name
36
+ const nameMatch = afterRoute.match(/->name\s*\(\s*['"]([^'"]+)['"]\s*\)/);
37
+ const name = nameMatch ? nameMatch[1] : null;
38
+
39
+ routes.push({ method, uri, controller, action, middleware, name });
40
+ }
41
+
42
+ return { type: 'route_file', routes };
43
+ }
44
+
45
+ module.exports = { extract };
@@ -0,0 +1,129 @@
1
+ 'use strict';
2
+
3
+ const { scoreComplexity } = require('../complexity-scorer');
4
+ const { extractBodySnippet } = require('../body-extractor');
5
+
6
+ function parse(sourceText, relativePath) {
7
+ const lines = sourceText.split('\n');
8
+ const functions = [];
9
+ const classes = [];
10
+ const imports = [];
11
+ const exports = [];
12
+
13
+ // Namespace detection
14
+ let namespace = null;
15
+ const nsMatch = sourceText.match(/^namespace\s+([\w\\]+)\s*;/m);
16
+ if (nsMatch) namespace = nsMatch[1];
17
+
18
+ for (let i = 0; i < lines.length; i++) {
19
+ const line = lines[i];
20
+ const trimmed = line.trim();
21
+
22
+ // Use statements
23
+ let m = trimmed.match(/^use\s+([\w\\]+)(?:\s+as\s+\w+)?;/);
24
+ if (m) { imports.push({ module: m[1], specifiers: [] }); continue; }
25
+
26
+ // Class definition
27
+ m = trimmed.match(/^(?:abstract\s+|final\s+)?class\s+(\w+)(?:\s+extends\s+(\w+))?(?:\s+implements\s+([\w,\s\\]+))?/);
28
+ if (m) {
29
+ const cls = { name: m[1], methods: [], extends: m[2] || null, implements: m[3] ? m[3].split(',').map(s => s.trim()) : [] };
30
+ classes.push(cls);
31
+ continue;
32
+ }
33
+
34
+ // Interface
35
+ m = trimmed.match(/^interface\s+(\w+)/);
36
+ if (m) {
37
+ classes.push({ name: m[1], methods: [], extends: null, isInterface: true });
38
+ continue;
39
+ }
40
+
41
+ // Methods
42
+ m = trimmed.match(/^(?:(public|protected|private)\s+)?(?:(static)\s+)?(?:(abstract|final)\s+)?(?:(async)\s+)?function\s+(\w+)\s*\(([^)]*)\)(?:\s*:\s*\??([\w\\|]+))?/);
43
+ if (m) {
44
+ const visibility = m[1] || 'public';
45
+ const isStatic = !!m[2];
46
+ const name = m[5];
47
+ const paramsStr = m[6] || '';
48
+ const returnType = m[7] || null;
49
+ const params = parsePhpParams(paramsStr);
50
+ const lineEnd = findBlockEnd(lines, i);
51
+ const body = lines.slice(i, lineEnd).join('\n');
52
+ const complexity = scoreComplexity(body);
53
+ const snippet = extractBodySnippet(sourceText, i + 1, lineEnd, complexity.complexityScore);
54
+ const fn = {
55
+ name,
56
+ params,
57
+ returnType,
58
+ isExported: visibility === 'public',
59
+ isStatic,
60
+ isAsync: !!m[4],
61
+ visibility,
62
+ lineStart: i + 1,
63
+ lineEnd,
64
+ calls: extractCalls(body),
65
+ ...complexity,
66
+ bodySnippet: snippet.bodySnippet,
67
+ bodySnippetTruncated: snippet.bodySnippetTruncated,
68
+ };
69
+ functions.push(fn);
70
+ if (classes.length > 0) classes[classes.length - 1].methods.push(name);
71
+ }
72
+ }
73
+
74
+ const publicFns = functions.filter(f => f.isExported).map(f => f.name);
75
+
76
+ return {
77
+ relativePath,
78
+ language: 'php',
79
+ linesOfCode: lines.length,
80
+ namespace,
81
+ functions,
82
+ classes,
83
+ imports,
84
+ exports: publicFns,
85
+ callsTo: [],
86
+ calledBy: [],
87
+ hash: null,
88
+ parserType: 'ast',
89
+ laravelContext: null,
90
+ };
91
+ }
92
+
93
+ function parsePhpParams(paramsStr) {
94
+ if (!paramsStr.trim()) return [];
95
+ return paramsStr.split(',').map(p => {
96
+ const trimmed = p.trim();
97
+ // Type hint + variable: e.g. "Request $request" or "?string $name = null"
98
+ const m = trimmed.match(/^(?:\?)?([\w\\|]+)\s+\$(\w+)/) || trimmed.match(/^\$(\w+)/);
99
+ if (!m) return { name: trimmed, type: null };
100
+ if (m[2]) return { name: '$' + m[2], type: m[1] };
101
+ return { name: '$' + m[1], type: null };
102
+ }).filter(p => p.name);
103
+ }
104
+
105
+ function findBlockEnd(lines, startIdx) {
106
+ let depth = 0;
107
+ for (let i = startIdx; i < lines.length; i++) {
108
+ for (const ch of lines[i]) {
109
+ if (ch === '{') depth++;
110
+ else if (ch === '}') {
111
+ depth--;
112
+ if (depth === 0) return i + 1;
113
+ }
114
+ }
115
+ }
116
+ return lines.length;
117
+ }
118
+
119
+ function extractCalls(body) {
120
+ const calls = new Set();
121
+ const keywords = new Set(['if', 'foreach', 'for', 'while', 'switch', 'catch', 'function', 'return', 'throw', 'new', 'echo', 'print', 'empty', 'isset', 'unset', 'list', 'array', 'class', 'interface', 'abstract', 'static', 'public', 'private', 'protected', 'namespace', 'use', 'require', 'include', 'elseif', 'else', 'try', 'finally', 'match', 'fn']);
122
+ for (const match of body.matchAll(/\b(\w+)\s*\(/g)) {
123
+ const name = match[1];
124
+ if (!keywords.has(name) && name.length > 1) calls.add(name);
125
+ }
126
+ return [...calls];
127
+ }
128
+
129
+ module.exports = { parse };
@@ -1,227 +1,104 @@
1
- /**
2
- * Python AST parser using web-tree-sitter.
3
- * Extracts def functions, class definitions, import/from...import statements.
4
- */
1
+ 'use strict';
5
2
 
6
- import { initTreeSitter, loadLanguage, createParser } from './tree-sitter-init.js';
7
- import { parseGeneric } from './generic.js';
8
-
9
- let pyParser = null;
10
- let pyInitialized = false;
11
-
12
- async function ensureParser() {
13
- if (pyInitialized) return pyParser;
14
- pyInitialized = true;
15
-
16
- const Parser = await initTreeSitter();
17
- if (!Parser) return null;
18
-
19
- const language = await loadLanguage('python');
20
- if (!language) return null;
21
-
22
- pyParser = createParser(language);
23
- return pyParser;
24
- }
25
-
26
- /**
27
- * Parse a Python file and extract structural facts.
28
- * @param {string} source - Raw file content
29
- * @param {string} relativePath - Relative file path
30
- * @returns {Promise<import('../../parser/index.js').FileFacts>}
31
- */
32
- export async function parsePython(source, relativePath) {
33
- const parser = await ensureParser();
34
-
35
- if (!parser) {
36
- return parseGeneric(source, relativePath, 'python');
37
- }
38
-
39
- const tree = parser.parse(source);
40
- const rootNode = tree.rootNode;
3
+ const { scoreComplexity } = require('../complexity-scorer');
4
+ const { extractBodySnippet } = require('../body-extractor');
41
5
 
6
+ function parse(sourceText, relativePath) {
7
+ const lines = sourceText.split('\n');
42
8
  const functions = [];
43
9
  const classes = [];
44
10
  const imports = [];
45
11
  const exports = [];
46
- const lines = source.split('\n');
47
- const linesOfCode = lines.filter((l) => l.trim().length > 0).length;
48
-
49
- walkNode(rootNode, { functions, classes, imports, exports });
50
12
 
51
- // In Python, top-level functions and classes are the "exports"
52
- for (const fn of functions) {
53
- if (!fn.name.startsWith('_')) {
54
- exports.push(fn.name);
13
+ for (let i = 0; i < lines.length; i++) {
14
+ const line = lines[i];
15
+ const trimmed = line.trim();
16
+
17
+ // Imports
18
+ let m = trimmed.match(/^import\s+(\w+)$/);
19
+ if (m) { imports.push({ module: m[1], specifiers: [] }); continue; }
20
+ m = trimmed.match(/^from\s+(\S+)\s+import\s+(.+)$/);
21
+ if (m) {
22
+ const specifiers = m[2].split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim());
23
+ imports.push({ module: m[1], specifiers });
24
+ continue;
55
25
  }
56
- }
57
- for (const cls of classes) {
58
- if (!cls.name.startsWith('_')) {
59
- exports.push(cls.name);
26
+ m = trimmed.match(/^import\s+(\S+)\s+as\s+\w+$/);
27
+ if (m) { imports.push({ module: m[1], specifiers: [] }); continue; }
28
+
29
+ // Class
30
+ m = trimmed.match(/^class\s+(\w+)/);
31
+ if (m) {
32
+ classes.push({ name: m[1], methods: [], extends: null });
33
+ continue;
34
+ }
35
+
36
+ // Function / def
37
+ m = trimmed.match(/^(?:async\s+)?def\s+(\w+)\s*\(([^)]*)\)(?:\s*->\s*(\S+))?:/);
38
+ if (m) {
39
+ const name = m[1];
40
+ const params = m[2].split(',').map(p => {
41
+ const pm = p.trim().match(/(\w+)(?:\s*:\s*(\S+?))?(?:\s*=.*)?$/);
42
+ return pm ? { name: pm[1], type: pm[2] || null } : { name: p.trim(), type: null };
43
+ }).filter(p => p.name && p.name !== 'self' && p.name !== 'cls');
44
+ const returnType = m[3] || null;
45
+ const lineEnd = findBlockEnd(lines, i);
46
+ const body = lines.slice(i, lineEnd).join('\n');
47
+ const complexity = scoreComplexity(body);
48
+ const snippet = extractBodySnippet(sourceText, i + 1, lineEnd, complexity.complexityScore);
49
+ functions.push({
50
+ name,
51
+ params,
52
+ returnType,
53
+ isExported: !name.startsWith('_'),
54
+ isAsync: /^async\s+def/.test(trimmed),
55
+ lineStart: i + 1,
56
+ lineEnd,
57
+ calls: extractCalls(body),
58
+ ...complexity,
59
+ bodySnippet: snippet.bodySnippet,
60
+ bodySnippetTruncated: snippet.bodySnippetTruncated,
61
+ });
60
62
  }
61
63
  }
62
64
 
65
+ const publicFns = functions.filter(f => f.isExported).map(f => f.name);
66
+
63
67
  return {
64
68
  relativePath,
65
69
  language: 'python',
66
- linesOfCode,
67
- parserType: 'tree-sitter',
70
+ linesOfCode: lines.length,
68
71
  functions,
69
72
  classes,
70
73
  imports,
71
- exports,
74
+ exports: publicFns,
72
75
  callsTo: [],
73
76
  calledBy: [],
74
- hash: '',
77
+ hash: null,
78
+ parserType: 'ast',
75
79
  };
76
80
  }
77
81
 
78
- function walkNode(node, ctx) {
79
- switch (node.type) {
80
- case 'function_definition':
81
- extractFunction(node, ctx);
82
- break;
83
- case 'class_definition':
84
- extractClass(node, ctx);
85
- break;
86
- case 'import_statement':
87
- extractImport(node, ctx);
88
- break;
89
- case 'import_from_statement':
90
- extractFromImport(node, ctx);
91
- break;
92
- }
93
-
94
- // Only walk top-level and class-level nodes
95
- if (node.type === 'module' || node.type === 'class_definition') {
96
- for (let i = 0; i < node.childCount; i++) {
97
- walkNode(node.child(i), ctx);
98
- }
82
+ function findBlockEnd(lines, startIdx) {
83
+ const baseIndent = lines[startIdx] ? lines[startIdx].match(/^(\s*)/)[1].length : 0;
84
+ for (let i = startIdx + 1; i < lines.length; i++) {
85
+ const line = lines[i];
86
+ if (line.trim() === '') continue;
87
+ const indent = line.match(/^(\s*)/)[1].length;
88
+ if (indent <= baseIndent) return i;
99
89
  }
90
+ return lines.length;
100
91
  }
101
92
 
102
- function extractFunction(node, ctx) {
103
- const nameNode = node.childForFieldName('name');
104
- const paramsNode = node.childForFieldName('parameters');
105
- const returnTypeNode = node.childForFieldName('return_type');
106
- const isAsync = node.type === 'function_definition' && node.text.startsWith('async');
107
-
108
- if (nameNode) {
109
- ctx.functions.push({
110
- name: nameNode.text,
111
- params: extractParams(paramsNode),
112
- returnType: returnTypeNode ? returnTypeNode.text : null,
113
- isExported: !nameNode.text.startsWith('_'),
114
- isAsync,
115
- lineStart: node.startPosition.row + 1,
116
- lineEnd: node.endPosition.row + 1,
117
- calls: [],
118
- });
93
+ function extractCalls(body) {
94
+ const calls = new Set();
95
+ const m = body.matchAll(/\b(\w+)\s*\(/g);
96
+ const keywords = new Set(['if', 'for', 'while', 'def', 'class', 'return', 'import', 'from', 'with', 'as', 'not', 'and', 'or', 'in', 'is', 'elif', 'else', 'try', 'except', 'finally', 'raise', 'del', 'lambda', 'yield', 'async', 'await', 'pass', 'break', 'continue', 'print']);
97
+ for (const match of m) {
98
+ const name = match[1];
99
+ if (!keywords.has(name) && name.length > 1) calls.add(name);
119
100
  }
101
+ return [...calls];
120
102
  }
121
103
 
122
- function extractClass(node, ctx) {
123
- const nameNode = node.childForFieldName('name');
124
- const bodyNode = node.childForFieldName('body');
125
-
126
- let superclass = null;
127
- const superclassNode = node.childForFieldName('superclasses');
128
- if (superclassNode && superclassNode.childCount > 0) {
129
- for (let i = 0; i < superclassNode.childCount; i++) {
130
- const child = superclassNode.child(i);
131
- if (child.type === 'identifier') {
132
- superclass = child.text;
133
- break;
134
- }
135
- }
136
- }
137
-
138
- if (nameNode) {
139
- const methods = [];
140
- if (bodyNode) {
141
- for (let i = 0; i < bodyNode.childCount; i++) {
142
- const child = bodyNode.child(i);
143
- if (child.type === 'function_definition') {
144
- const methodName = child.childForFieldName('name');
145
- if (methodName) methods.push(methodName.text);
146
- }
147
- }
148
- }
149
-
150
- ctx.classes.push({
151
- name: nameNode.text,
152
- methods,
153
- extends: superclass,
154
- });
155
- }
156
- }
157
-
158
- function extractImport(node, ctx) {
159
- // import module
160
- for (let i = 0; i < node.childCount; i++) {
161
- const child = node.child(i);
162
- if (child.type === 'dotted_name') {
163
- ctx.imports.push({
164
- module: child.text,
165
- specifiers: [child.text],
166
- });
167
- } else if (child.type === 'aliased_import') {
168
- const nameNode = child.childForFieldName('name');
169
- if (nameNode) {
170
- ctx.imports.push({
171
- module: nameNode.text,
172
- specifiers: [nameNode.text],
173
- });
174
- }
175
- }
176
- }
177
- }
178
-
179
- function extractFromImport(node, ctx) {
180
- // from module import ...
181
- const moduleNode = node.childForFieldName('module_name');
182
- if (!moduleNode) return;
183
-
184
- const specifiers = [];
185
- for (let i = 0; i < node.childCount; i++) {
186
- const child = node.child(i);
187
- if (child.type === 'dotted_name' && child !== moduleNode) {
188
- specifiers.push(child.text);
189
- } else if (child.type === 'aliased_import') {
190
- const nameNode = child.childForFieldName('name');
191
- if (nameNode) specifiers.push(nameNode.text);
192
- }
193
- }
194
-
195
- ctx.imports.push({
196
- module: moduleNode.text,
197
- specifiers,
198
- });
199
- }
200
-
201
- function extractParams(paramsNode) {
202
- if (!paramsNode) return [];
203
- const params = [];
204
- for (let i = 0; i < paramsNode.childCount; i++) {
205
- const child = paramsNode.child(i);
206
- if (child.type === 'identifier') {
207
- if (child.text !== 'self' && child.text !== 'cls') {
208
- params.push({ name: child.text, type: null });
209
- }
210
- } else if (child.type === 'typed_parameter') {
211
- const nameNode = child.childCount > 0 ? child.child(0) : null;
212
- const typeNode = child.childForFieldName('type');
213
- if (nameNode && nameNode.text !== 'self' && nameNode.text !== 'cls') {
214
- params.push({
215
- name: nameNode.text,
216
- type: typeNode ? typeNode.text : null,
217
- });
218
- }
219
- } else if (child.type === 'default_parameter') {
220
- const nameNode = child.childForFieldName('name');
221
- if (nameNode && nameNode.text !== 'self' && nameNode.text !== 'cls') {
222
- params.push({ name: nameNode.text, type: null });
223
- }
224
- }
225
- }
226
- return params;
227
- }
104
+ module.exports = { parse };