@ophan/core 0.0.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.
- package/README.md +107 -0
- package/dist/index.d.ts +115 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +715 -0
- package/dist/index.test.d.ts +2 -0
- package/dist/index.test.d.ts.map +1 -0
- package/dist/index.test.js +492 -0
- package/dist/parsers/__fixtures__/arrow-functions.d.ts +5 -0
- package/dist/parsers/__fixtures__/arrow-functions.d.ts.map +1 -0
- package/dist/parsers/__fixtures__/arrow-functions.js +16 -0
- package/dist/parsers/__fixtures__/class-methods.d.ts +6 -0
- package/dist/parsers/__fixtures__/class-methods.d.ts.map +1 -0
- package/dist/parsers/__fixtures__/class-methods.js +12 -0
- package/dist/parsers/__fixtures__/no-functions.d.ts +9 -0
- package/dist/parsers/__fixtures__/no-functions.d.ts.map +1 -0
- package/dist/parsers/__fixtures__/no-functions.js +4 -0
- package/dist/parsers/index.d.ts +3 -0
- package/dist/parsers/index.d.ts.map +1 -0
- package/dist/parsers/index.js +18 -0
- package/dist/parsers/python.d.ts +8 -0
- package/dist/parsers/python.d.ts.map +1 -0
- package/dist/parsers/python.js +137 -0
- package/dist/parsers/python.test.d.ts +2 -0
- package/dist/parsers/python.test.d.ts.map +1 -0
- package/dist/parsers/python.test.js +96 -0
- package/dist/parsers/registry.d.ts +8 -0
- package/dist/parsers/registry.d.ts.map +1 -0
- package/dist/parsers/registry.js +68 -0
- package/dist/parsers/types.d.ts +10 -0
- package/dist/parsers/types.d.ts.map +1 -0
- package/dist/parsers/types.js +18 -0
- package/dist/parsers/typescript.d.ts +8 -0
- package/dist/parsers/typescript.d.ts.map +1 -0
- package/dist/parsers/typescript.js +110 -0
- package/dist/parsers/typescript.test.d.ts +2 -0
- package/dist/parsers/typescript.test.d.ts.map +1 -0
- package/dist/parsers/typescript.test.js +106 -0
- package/dist/schemas.d.ts +100 -0
- package/dist/schemas.d.ts.map +1 -0
- package/dist/schemas.js +128 -0
- package/dist/shared.d.ts +12 -0
- package/dist/shared.d.ts.map +1 -0
- package/dist/shared.js +10 -0
- package/dist/test-utils.d.ts +46 -0
- package/dist/test-utils.d.ts.map +1 -0
- package/dist/test-utils.js +141 -0
- package/package.json +37 -0
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Python parser — uses Python's own ast module via python3 subprocess
|
|
3
|
+
//
|
|
4
|
+
// Zero npm dependencies. Shells out to `python3 -c "..."` with an inline script that uses
|
|
5
|
+
// Python's built-in ast module to extract functions, async functions, and class methods.
|
|
6
|
+
// The script outputs JSON to stdout which we parse in Node.js.
|
|
7
|
+
//
|
|
8
|
+
// If python3 is not installed, the parser logs a warning and returns empty results.
|
|
9
|
+
// This means Python files are simply skipped during analysis — TypeScript analysis
|
|
10
|
+
// continues working regardless.
|
|
11
|
+
//
|
|
12
|
+
// Why not a JS-based Python parser?
|
|
13
|
+
// - Python's ast module is the canonical parser, maintained by CPython itself
|
|
14
|
+
// - It handles all Python syntax perfectly (decorators, async, walrus operator, etc.)
|
|
15
|
+
// - Zero maintenance burden — ast evolves with Python
|
|
16
|
+
// - The subprocess call adds ~50ms per file, acceptable for batch analysis
|
|
17
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
20
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
21
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
22
|
+
}
|
|
23
|
+
Object.defineProperty(o, k2, desc);
|
|
24
|
+
}) : (function(o, m, k, k2) {
|
|
25
|
+
if (k2 === undefined) k2 = k;
|
|
26
|
+
o[k2] = m[k];
|
|
27
|
+
}));
|
|
28
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
29
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
30
|
+
}) : function(o, v) {
|
|
31
|
+
o["default"] = v;
|
|
32
|
+
});
|
|
33
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
34
|
+
var ownKeys = function(o) {
|
|
35
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
36
|
+
var ar = [];
|
|
37
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
38
|
+
return ar;
|
|
39
|
+
};
|
|
40
|
+
return ownKeys(o);
|
|
41
|
+
};
|
|
42
|
+
return function (mod) {
|
|
43
|
+
if (mod && mod.__esModule) return mod;
|
|
44
|
+
var result = {};
|
|
45
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
46
|
+
__setModuleDefault(result, mod);
|
|
47
|
+
return result;
|
|
48
|
+
};
|
|
49
|
+
})();
|
|
50
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
51
|
+
exports.PythonParser = void 0;
|
|
52
|
+
const child_process_1 = require("child_process");
|
|
53
|
+
const fs = __importStar(require("fs"));
|
|
54
|
+
const shared_1 = require("../shared");
|
|
55
|
+
// Inline Python script that extracts function info using the ast module.
|
|
56
|
+
// Outputs JSON array to stdout. Uses parent tracking to detect methods vs functions.
|
|
57
|
+
const PYTHON_EXTRACT_SCRIPT = `
|
|
58
|
+
import ast, json, sys
|
|
59
|
+
|
|
60
|
+
source = sys.stdin.read()
|
|
61
|
+
tree = ast.parse(source)
|
|
62
|
+
|
|
63
|
+
# Annotate parent references so we can detect methods (FunctionDef inside ClassDef)
|
|
64
|
+
for node in ast.walk(tree):
|
|
65
|
+
for child in ast.iter_child_nodes(node):
|
|
66
|
+
child._parent = node
|
|
67
|
+
|
|
68
|
+
functions = []
|
|
69
|
+
lines = source.split('\\n')
|
|
70
|
+
|
|
71
|
+
for node in ast.walk(tree):
|
|
72
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
73
|
+
end_line = getattr(node, 'end_lineno', None) or node.lineno
|
|
74
|
+
source_code = '\\n'.join(lines[node.lineno - 1 : end_line])
|
|
75
|
+
|
|
76
|
+
parent = getattr(node, '_parent', None)
|
|
77
|
+
entity_type = "method" if isinstance(parent, ast.ClassDef) else "function"
|
|
78
|
+
|
|
79
|
+
functions.append({
|
|
80
|
+
"name": node.name,
|
|
81
|
+
"startLine": node.lineno,
|
|
82
|
+
"endLine": end_line,
|
|
83
|
+
"sourceCode": source_code,
|
|
84
|
+
"entityType": entity_type
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
json.dump(functions, sys.stdout)
|
|
88
|
+
`;
|
|
89
|
+
let pythonAvailable = null;
|
|
90
|
+
function isPythonAvailable() {
|
|
91
|
+
if (pythonAvailable !== null)
|
|
92
|
+
return pythonAvailable;
|
|
93
|
+
try {
|
|
94
|
+
(0, child_process_1.execFileSync)("python3", ["--version"], { stdio: "ignore" });
|
|
95
|
+
pythonAvailable = true;
|
|
96
|
+
}
|
|
97
|
+
catch {
|
|
98
|
+
pythonAvailable = false;
|
|
99
|
+
}
|
|
100
|
+
return pythonAvailable;
|
|
101
|
+
}
|
|
102
|
+
class PythonParser {
|
|
103
|
+
constructor() {
|
|
104
|
+
this.language = "python";
|
|
105
|
+
this.extensions = [".py"];
|
|
106
|
+
}
|
|
107
|
+
extractFunctions(filePath) {
|
|
108
|
+
if (!isPythonAvailable()) {
|
|
109
|
+
console.warn("python3 not found — skipping Python files. Install Python 3 to enable Python analysis.");
|
|
110
|
+
return [];
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const source = fs.readFileSync(filePath, "utf-8");
|
|
114
|
+
const output = (0, child_process_1.execFileSync)("python3", ["-c", PYTHON_EXTRACT_SCRIPT], {
|
|
115
|
+
input: source,
|
|
116
|
+
encoding: "utf-8",
|
|
117
|
+
timeout: 10000, // 10s timeout per file
|
|
118
|
+
});
|
|
119
|
+
const raw = JSON.parse(output);
|
|
120
|
+
return raw.map((fn) => ({
|
|
121
|
+
name: fn.name,
|
|
122
|
+
filePath,
|
|
123
|
+
startLine: fn.startLine,
|
|
124
|
+
endLine: fn.endLine,
|
|
125
|
+
sourceCode: fn.sourceCode,
|
|
126
|
+
contentHash: (0, shared_1.computeHash)(fn.sourceCode),
|
|
127
|
+
language: "python",
|
|
128
|
+
entityType: fn.entityType,
|
|
129
|
+
}));
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.error(`Failed to parse Python file ${filePath}:`, err.message);
|
|
133
|
+
return [];
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
exports.PythonParser = PythonParser;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"python.test.d.ts","sourceRoot":"","sources":["../../src/parsers/python.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const vitest_1 = require("vitest");
|
|
37
|
+
const child_process_1 = require("child_process");
|
|
38
|
+
const path = __importStar(require("path"));
|
|
39
|
+
const python_1 = require("./python");
|
|
40
|
+
const parser = new python_1.PythonParser();
|
|
41
|
+
const fixturesDir = path.join(__dirname, "__fixtures__");
|
|
42
|
+
// Must be synchronous at module level — describe.skipIf evaluates immediately
|
|
43
|
+
let pythonAvailable = false;
|
|
44
|
+
try {
|
|
45
|
+
(0, child_process_1.execFileSync)("python3", ["--version"], { stdio: "ignore" });
|
|
46
|
+
pythonAvailable = true;
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
pythonAvailable = false;
|
|
50
|
+
}
|
|
51
|
+
(0, vitest_1.describe)("PythonParser", () => {
|
|
52
|
+
vitest_1.describe.skipIf(!pythonAvailable)("method detection", () => {
|
|
53
|
+
(0, vitest_1.it)('class methods get entityType "method"', () => {
|
|
54
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.py"));
|
|
55
|
+
const getUser = functions.find((f) => f.name === "get_user");
|
|
56
|
+
const createUser = functions.find((f) => f.name === "create_user");
|
|
57
|
+
(0, vitest_1.expect)(getUser?.entityType).toBe("method");
|
|
58
|
+
(0, vitest_1.expect)(createUser?.entityType).toBe("method");
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)('top-level functions get entityType "function"', () => {
|
|
61
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.py"));
|
|
62
|
+
const standalone = functions.find((f) => f.name === "standalone");
|
|
63
|
+
(0, vitest_1.expect)(standalone?.entityType).toBe("function");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
vitest_1.describe.skipIf(!pythonAvailable)("async function handling", () => {
|
|
67
|
+
(0, vitest_1.it)("extracts async def functions", () => {
|
|
68
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "simple.py"));
|
|
69
|
+
const fetchData = functions.find((f) => f.name === "fetch_data");
|
|
70
|
+
(0, vitest_1.expect)(fetchData).toBeDefined();
|
|
71
|
+
(0, vitest_1.expect)(fetchData?.language).toBe("python");
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
vitest_1.describe.skipIf(!pythonAvailable)("basic extraction", () => {
|
|
75
|
+
(0, vitest_1.it)("extracts all functions from simple file", () => {
|
|
76
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "simple.py"));
|
|
77
|
+
const names = functions.map((f) => f.name);
|
|
78
|
+
(0, vitest_1.expect)(names).toContain("greet");
|
|
79
|
+
(0, vitest_1.expect)(names).toContain("fetch_data");
|
|
80
|
+
(0, vitest_1.expect)(names).toContain("add");
|
|
81
|
+
});
|
|
82
|
+
(0, vitest_1.it)("all extracted functions have contentHash set", () => {
|
|
83
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "simple.py"));
|
|
84
|
+
for (const fn of functions) {
|
|
85
|
+
(0, vitest_1.expect)(fn.contentHash).toBeDefined();
|
|
86
|
+
(0, vitest_1.expect)(fn.contentHash.length).toBe(64);
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
(0, vitest_1.it)("all functions have language python", () => {
|
|
90
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "simple.py"));
|
|
91
|
+
for (const fn of functions) {
|
|
92
|
+
(0, vitest_1.expect)(fn.language).toBe("python");
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
});
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { LanguageParser } from "./types";
|
|
2
|
+
export declare function registerParser(parser: LanguageParser): void;
|
|
3
|
+
export declare function getParserForFile(filePath: string): LanguageParser | null;
|
|
4
|
+
/** Returns all supported file extensions (e.g. [".ts", ".tsx", ".js", ".jsx", ".py"]) */
|
|
5
|
+
export declare function getSupportedExtensions(): string[];
|
|
6
|
+
/** Returns all registered parsers */
|
|
7
|
+
export declare function getRegisteredParsers(): readonly LanguageParser[];
|
|
8
|
+
//# sourceMappingURL=registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"registry.d.ts","sourceRoot":"","sources":["../../src/parsers/registry.ts"],"names":[],"mappings":"AAQA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAQ9C,wBAAgB,cAAc,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAK3D;AAED,wBAAgB,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,cAAc,GAAG,IAAI,CAGxE;AAED,yFAAyF;AACzF,wBAAgB,sBAAsB,IAAI,MAAM,EAAE,CAEjD;AAED,qCAAqC;AACrC,wBAAgB,oBAAoB,IAAI,SAAS,cAAc,EAAE,CAEhE"}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Parser registry — maps file extensions to language parsers
|
|
3
|
+
//
|
|
4
|
+
// The registry is the single lookup point used by analyzeRepository() and refreshFileIndex()
|
|
5
|
+
// to determine which parser handles a given file. Parsers self-register on import via
|
|
6
|
+
// registerParser(), and the glob pattern for file discovery is built dynamically from
|
|
7
|
+
// getSupportedExtensions() so adding a new language automatically includes its files.
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.registerParser = registerParser;
|
|
43
|
+
exports.getParserForFile = getParserForFile;
|
|
44
|
+
exports.getSupportedExtensions = getSupportedExtensions;
|
|
45
|
+
exports.getRegisteredParsers = getRegisteredParsers;
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
/** Maps file extension (e.g. ".ts") to its parser */
|
|
48
|
+
const parsersByExtension = new Map();
|
|
49
|
+
/** All registered parsers for iteration */
|
|
50
|
+
const allParsers = [];
|
|
51
|
+
function registerParser(parser) {
|
|
52
|
+
allParsers.push(parser);
|
|
53
|
+
for (const ext of parser.extensions) {
|
|
54
|
+
parsersByExtension.set(ext.toLowerCase(), parser);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function getParserForFile(filePath) {
|
|
58
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
59
|
+
return parsersByExtension.get(ext) || null;
|
|
60
|
+
}
|
|
61
|
+
/** Returns all supported file extensions (e.g. [".ts", ".tsx", ".js", ".jsx", ".py"]) */
|
|
62
|
+
function getSupportedExtensions() {
|
|
63
|
+
return Array.from(parsersByExtension.keys());
|
|
64
|
+
}
|
|
65
|
+
/** Returns all registered parsers */
|
|
66
|
+
function getRegisteredParsers() {
|
|
67
|
+
return allParsers;
|
|
68
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FunctionInfo } from "../shared";
|
|
2
|
+
export interface LanguageParser {
|
|
3
|
+
/** Language identifier stored in DB, e.g. "typescript", "python" */
|
|
4
|
+
readonly language: string;
|
|
5
|
+
/** File extensions this parser handles, including the dot, e.g. [".ts", ".tsx"] */
|
|
6
|
+
readonly extensions: string[];
|
|
7
|
+
/** Extract function-like entities from a source file */
|
|
8
|
+
extractFunctions(filePath: string): FunctionInfo[];
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/parsers/types.ts"],"names":[],"mappings":"AAiBA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,oEAAoE;IACpE,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAE1B,mFAAmF;IACnF,QAAQ,CAAC,UAAU,EAAE,MAAM,EAAE,CAAC;IAE9B,wDAAwD;IACxD,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE,CAAC;CACpD"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Parser interface — the abstraction that enables multi-language support
|
|
3
|
+
//
|
|
4
|
+
// Each language implements LanguageParser to extract function-like entities from source files.
|
|
5
|
+
// The interface is deliberately minimal: given a file path, return an array of FunctionInfo.
|
|
6
|
+
//
|
|
7
|
+
// Why not tree-sitter or a custom LSP?
|
|
8
|
+
// - Tree-sitter adds native C dependencies and grammar management complexity.
|
|
9
|
+
// - A custom LSP is unnecessary — the CLI + SQLite DB is the cross-IDE abstraction.
|
|
10
|
+
// - Instead, each parser uses the language's own tooling: TypeScript compiler API for TS/JS,
|
|
11
|
+
// Python's ast module for Python, go/parser for Go, etc. Zero extra dependencies per language.
|
|
12
|
+
//
|
|
13
|
+
// To add a new language:
|
|
14
|
+
// 1. Create a new file in this directory (e.g., go.ts)
|
|
15
|
+
// 2. Implement LanguageParser with the correct extensions and extractFunctions()
|
|
16
|
+
// 3. Register it in parsers/index.ts
|
|
17
|
+
// That's it — the analysis pipeline, hashing, DB storage, and sync all work unchanged.
|
|
18
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FunctionInfo } from "../shared";
|
|
2
|
+
import type { LanguageParser } from "./types";
|
|
3
|
+
export declare class TypeScriptParser implements LanguageParser {
|
|
4
|
+
readonly language = "typescript";
|
|
5
|
+
readonly extensions: string[];
|
|
6
|
+
extractFunctions(filePath: string): FunctionInfo[];
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=typescript.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typescript.d.ts","sourceRoot":"","sources":["../../src/parsers/typescript.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,WAAW,CAAC;AAE9C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAa9C,qBAAa,gBAAiB,YAAW,cAAc;IACrD,QAAQ,CAAC,QAAQ,gBAAgB;IACjC,QAAQ,CAAC,UAAU,WAAkC;IAErD,gBAAgB,CAAC,QAAQ,EAAE,MAAM,GAAG,YAAY,EAAE;CAyDnD"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// TypeScript/JavaScript parser — uses the TypeScript compiler API
|
|
3
|
+
//
|
|
4
|
+
// Handles .ts, .tsx, .js, .jsx files. The TypeScript compiler can parse JavaScript too,
|
|
5
|
+
// so one parser covers both languages (with correct language labels).
|
|
6
|
+
//
|
|
7
|
+
// The TS compiler API gives us deep understanding of function types: declarations, methods,
|
|
8
|
+
// arrow functions, and function expressions. This is more reliable than regex or tree-sitter
|
|
9
|
+
// for TypeScript specifically, and it's already a dependency.
|
|
10
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
11
|
+
if (k2 === undefined) k2 = k;
|
|
12
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
13
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
14
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
15
|
+
}
|
|
16
|
+
Object.defineProperty(o, k2, desc);
|
|
17
|
+
}) : (function(o, m, k, k2) {
|
|
18
|
+
if (k2 === undefined) k2 = k;
|
|
19
|
+
o[k2] = m[k];
|
|
20
|
+
}));
|
|
21
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
22
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
23
|
+
}) : function(o, v) {
|
|
24
|
+
o["default"] = v;
|
|
25
|
+
});
|
|
26
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
27
|
+
var ownKeys = function(o) {
|
|
28
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
29
|
+
var ar = [];
|
|
30
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
31
|
+
return ar;
|
|
32
|
+
};
|
|
33
|
+
return ownKeys(o);
|
|
34
|
+
};
|
|
35
|
+
return function (mod) {
|
|
36
|
+
if (mod && mod.__esModule) return mod;
|
|
37
|
+
var result = {};
|
|
38
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
39
|
+
__setModuleDefault(result, mod);
|
|
40
|
+
return result;
|
|
41
|
+
};
|
|
42
|
+
})();
|
|
43
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
44
|
+
exports.TypeScriptParser = void 0;
|
|
45
|
+
const ts = __importStar(require("typescript"));
|
|
46
|
+
const fs = __importStar(require("fs"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const shared_1 = require("../shared");
|
|
49
|
+
function detectLanguage(filePath) {
|
|
50
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
51
|
+
if (ext === ".ts" || ext === ".tsx")
|
|
52
|
+
return "typescript";
|
|
53
|
+
return "javascript";
|
|
54
|
+
}
|
|
55
|
+
function detectEntityType(node) {
|
|
56
|
+
if (ts.isMethodDeclaration(node))
|
|
57
|
+
return "method";
|
|
58
|
+
return "function";
|
|
59
|
+
}
|
|
60
|
+
class TypeScriptParser {
|
|
61
|
+
constructor() {
|
|
62
|
+
this.language = "typescript"; // label for the primary language
|
|
63
|
+
this.extensions = [".ts", ".tsx", ".js", ".jsx"];
|
|
64
|
+
}
|
|
65
|
+
extractFunctions(filePath) {
|
|
66
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
67
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.Latest, true);
|
|
68
|
+
const language = detectLanguage(filePath);
|
|
69
|
+
const functions = [];
|
|
70
|
+
function visit(node) {
|
|
71
|
+
if (ts.isFunctionDeclaration(node) ||
|
|
72
|
+
ts.isMethodDeclaration(node) ||
|
|
73
|
+
ts.isArrowFunction(node) ||
|
|
74
|
+
ts.isFunctionExpression(node)) {
|
|
75
|
+
const name = getFunctionName(node);
|
|
76
|
+
if (!name)
|
|
77
|
+
return;
|
|
78
|
+
const start = sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
79
|
+
const end = sourceFile.getLineAndCharacterOfPosition(node.getEnd());
|
|
80
|
+
const sourceCode = node.getText(sourceFile);
|
|
81
|
+
functions.push({
|
|
82
|
+
name,
|
|
83
|
+
filePath,
|
|
84
|
+
startLine: start.line + 1,
|
|
85
|
+
endLine: end.line + 1,
|
|
86
|
+
sourceCode,
|
|
87
|
+
contentHash: (0, shared_1.computeHash)(sourceCode),
|
|
88
|
+
language,
|
|
89
|
+
entityType: detectEntityType(node),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
ts.forEachChild(node, visit);
|
|
93
|
+
}
|
|
94
|
+
function getFunctionName(node) {
|
|
95
|
+
if (ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) {
|
|
96
|
+
return node.name?.getText(sourceFile) || null;
|
|
97
|
+
}
|
|
98
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node)) {
|
|
99
|
+
const parent = node.parent;
|
|
100
|
+
if (ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name)) {
|
|
101
|
+
return parent.name.getText(sourceFile);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
visit(sourceFile);
|
|
107
|
+
return functions;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
exports.TypeScriptParser = TypeScriptParser;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"typescript.test.d.ts","sourceRoot":"","sources":["../../src/parsers/typescript.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
const vitest_1 = require("vitest");
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const typescript_1 = require("./typescript");
|
|
39
|
+
const parser = new typescript_1.TypeScriptParser();
|
|
40
|
+
const fixturesDir = path.join(__dirname, "__fixtures__");
|
|
41
|
+
(0, vitest_1.describe)("TypeScriptParser", () => {
|
|
42
|
+
(0, vitest_1.describe)("name extraction", () => {
|
|
43
|
+
(0, vitest_1.it)("extracts name from const arrow function declarations", () => {
|
|
44
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "arrow-functions.ts"));
|
|
45
|
+
const names = functions.map((f) => f.name);
|
|
46
|
+
(0, vitest_1.expect)(names).toContain("greet");
|
|
47
|
+
(0, vitest_1.expect)(names).toContain("add");
|
|
48
|
+
(0, vitest_1.expect)(names).toContain("handler");
|
|
49
|
+
});
|
|
50
|
+
(0, vitest_1.it)("skips unnamed default export arrows", () => {
|
|
51
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "arrow-functions.ts"));
|
|
52
|
+
const names = functions.map((f) => f.name);
|
|
53
|
+
// The default export `(x: number) => x * 2` has no variable declaration parent
|
|
54
|
+
// so it should not appear in the extracted functions
|
|
55
|
+
const unnamed = functions.filter((f) => f.name === null || f.name === undefined);
|
|
56
|
+
(0, vitest_1.expect)(unnamed).toHaveLength(0);
|
|
57
|
+
});
|
|
58
|
+
(0, vitest_1.it)("skips callback arrows without variable declaration parent", () => {
|
|
59
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "arrow-functions.ts"));
|
|
60
|
+
const names = functions.map((f) => f.name);
|
|
61
|
+
// The `(n) => n * 2` inside .map() should not appear
|
|
62
|
+
// (its parent is a CallExpression argument, not a VariableDeclaration)
|
|
63
|
+
(0, vitest_1.expect)(names).not.toContain("n");
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
(0, vitest_1.describe)("entity type detection", () => {
|
|
67
|
+
(0, vitest_1.it)('class methods get entityType "method"', () => {
|
|
68
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
|
|
69
|
+
const getUser = functions.find((f) => f.name === "getUser");
|
|
70
|
+
const createUser = functions.find((f) => f.name === "createUser");
|
|
71
|
+
(0, vitest_1.expect)(getUser?.entityType).toBe("method");
|
|
72
|
+
(0, vitest_1.expect)(createUser?.entityType).toBe("method");
|
|
73
|
+
});
|
|
74
|
+
(0, vitest_1.it)('standalone functions get entityType "function"', () => {
|
|
75
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
|
|
76
|
+
const standalone = functions.find((f) => f.name === "standalone");
|
|
77
|
+
(0, vitest_1.expect)(standalone?.entityType).toBe("function");
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
(0, vitest_1.describe)("language detection", () => {
|
|
81
|
+
(0, vitest_1.it)(".ts files get language typescript", () => {
|
|
82
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
|
|
83
|
+
for (const fn of functions) {
|
|
84
|
+
(0, vitest_1.expect)(fn.language).toBe("typescript");
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
(0, vitest_1.describe)("edge cases", () => {
|
|
89
|
+
(0, vitest_1.it)("file with no functions returns empty array", () => {
|
|
90
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "no-functions.ts"));
|
|
91
|
+
(0, vitest_1.expect)(functions).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
(0, vitest_1.it)("all extracted functions have contentHash set", () => {
|
|
94
|
+
const functions = parser.extractFunctions(path.join(fixturesDir, "arrow-functions.ts"));
|
|
95
|
+
for (const fn of functions) {
|
|
96
|
+
(0, vitest_1.expect)(fn.contentHash).toBeDefined();
|
|
97
|
+
(0, vitest_1.expect)(fn.contentHash.length).toBe(64); // SHA256 hex
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
(0, vitest_1.it)("same source code produces same hash (deterministic)", () => {
|
|
101
|
+
const fns1 = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
|
|
102
|
+
const fns2 = parser.extractFunctions(path.join(fixturesDir, "class-methods.ts"));
|
|
103
|
+
(0, vitest_1.expect)(fns1.map((f) => f.contentHash)).toEqual(fns2.map((f) => f.contentHash));
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
});
|