@davesheffer/hunch 1.14.0 → 1.15.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.
|
@@ -8,7 +8,8 @@
|
|
|
8
8
|
* `indexRepo` persists that exact scan into the JSON source of truth; its caller
|
|
9
9
|
* then runs HunchStore.reindex() to refresh the SQLite index.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { dirname, join, posix } from "node:path";
|
|
12
13
|
import { parseSource, attributeCalls } from "./parse.js";
|
|
13
14
|
import { symbolId, componentId, edgeId, sha1 } from "../core/ids.js";
|
|
14
15
|
import { externalImportNodeId, externalPackage } from "../core/externalImports.js";
|
|
@@ -135,9 +136,15 @@ export function scanRepo(store, root, opts = {}) {
|
|
|
135
136
|
// JS/TS resolver and look unimported.
|
|
136
137
|
const hasSrcLayout = [...fileSymbols.keys()].some((f) => f.startsWith("src/"));
|
|
137
138
|
const pyRoots = hasSrcLayout ? ["", "src"] : [""];
|
|
138
|
-
const
|
|
139
|
-
|
|
140
|
-
|
|
139
|
+
const goModule = readGoModulePath(root);
|
|
140
|
+
const resolveImportTarget = (file, spec) => {
|
|
141
|
+
const langId = languageFor(file)?.id;
|
|
142
|
+
if (langId === "python")
|
|
143
|
+
return resolvePythonImport(file, spec, fileSymbols, pyRoots);
|
|
144
|
+
if (langId === "go")
|
|
145
|
+
return resolveGoImport(spec, fileSymbols, goModule);
|
|
146
|
+
return resolveImport(file, spec, fileSymbols);
|
|
147
|
+
};
|
|
141
148
|
const importedFiles = new Map(perFileImports.map(({ file, imports }) => [
|
|
142
149
|
file,
|
|
143
150
|
new Set(imports.map((specifier) => resolveImportTarget(file, specifier)).filter((target) => !!target)),
|
|
@@ -353,6 +360,48 @@ function resolvePythonImport(fromFile, spec, fileSymbols, pyRoots) {
|
|
|
353
360
|
const modulePath = baseDir ? `${baseDir}/${tailPath}` : tailPath;
|
|
354
361
|
return firstExistingPyModule(modulePath, fileSymbols);
|
|
355
362
|
}
|
|
363
|
+
/** The `module` path declared in the repo's go.mod, or null. A resolution HINT
|
|
364
|
+
* only (it widens depends_on edge coverage); reading it best-effort from the
|
|
365
|
+
* filesystem never gates a scan. */
|
|
366
|
+
function readGoModulePath(root) {
|
|
367
|
+
try {
|
|
368
|
+
const match = /^module\s+(\S+)/m.exec(readFileSync(join(root, "go.mod"), "utf8"));
|
|
369
|
+
return match ? match[1] : null;
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
return null;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
/** Lexicographically-first tracked .go file whose directory is exactly `dir`
|
|
376
|
+
* ("" = repo root) — a Go import names a PACKAGE (directory), so any file in it
|
|
377
|
+
* identifies the right component for a depends_on edge. */
|
|
378
|
+
function firstGoFileInDir(dir, fileSymbols) {
|
|
379
|
+
let best = null;
|
|
380
|
+
for (const f of fileSymbols.keys()) {
|
|
381
|
+
if (!f.endsWith(".go"))
|
|
382
|
+
continue;
|
|
383
|
+
const d = toPosix(dirname(f));
|
|
384
|
+
const matches = dir === "" ? d === "." : d === dir;
|
|
385
|
+
if (matches && (!best || f < best))
|
|
386
|
+
best = f;
|
|
387
|
+
}
|
|
388
|
+
return best;
|
|
389
|
+
}
|
|
390
|
+
/** Resolve a Go import path to a tracked file. Sibling to resolvePythonImport():
|
|
391
|
+
* an in-module import is the go.mod module path plus the package directory, so
|
|
392
|
+
* strip the declared module prefix and look the directory up exactly; with no
|
|
393
|
+
* go.mod, try the path as a repo-relative directory. Anything else (stdlib,
|
|
394
|
+
* external modules) resolves to null — no suffix guessing, a wrong depends_on
|
|
395
|
+
* edge is worse than a missing one. */
|
|
396
|
+
function resolveGoImport(spec, fileSymbols, goModule) {
|
|
397
|
+
if (goModule) {
|
|
398
|
+
if (spec === goModule)
|
|
399
|
+
return firstGoFileInDir("", fileSymbols);
|
|
400
|
+
if (spec.startsWith(`${goModule}/`))
|
|
401
|
+
return firstGoFileInDir(spec.slice(goModule.length + 1), fileSymbols);
|
|
402
|
+
}
|
|
403
|
+
return firstGoFileInDir(spec, fileSymbols);
|
|
404
|
+
}
|
|
356
405
|
/** Derive components from the directory layout: the directory immediately under
|
|
357
406
|
* `src/` (or the top-level dir) groups files into a module component. */
|
|
358
407
|
function deriveComponents(symbols) {
|
|
@@ -125,7 +125,55 @@ const PYTHON = {
|
|
|
125
125
|
nameToDef: { "fn.name": "fn.def", "method.name": "method.def", "class.name": "class.def" },
|
|
126
126
|
builtinMethods: PY_BUILTIN_METHODS,
|
|
127
127
|
};
|
|
128
|
-
|
|
128
|
+
const GO_QUERY = `
|
|
129
|
+
(function_declaration name: (identifier) @fn.name) @fn.def
|
|
130
|
+
(method_declaration name: (field_identifier) @method.name) @method.def
|
|
131
|
+
;; Specific type_spec shapes FIRST: parse.ts keeps the first classification a
|
|
132
|
+
;; node id receives (same mechanism the Python class patterns rely on), so a
|
|
133
|
+
;; struct/interface matches its specific pattern before the generic @type.def.
|
|
134
|
+
(type_spec name: (type_identifier) @struct.name type: (struct_type)) @struct.def
|
|
135
|
+
(type_spec name: (type_identifier) @iface.name type: (interface_type)) @iface.def
|
|
136
|
+
(type_spec name: (type_identifier) @type.name) @type.def
|
|
137
|
+
;; \`type X = Y\` is a distinct type_alias node, not a type_spec.
|
|
138
|
+
(type_alias name: (type_identifier) @type.name) @type.def
|
|
139
|
+
(import_spec path: [(interpreted_string_literal) (raw_string_literal)] @import.src)
|
|
140
|
+
(call_expression function: (identifier) @call.id)
|
|
141
|
+
(call_expression function: (selector_expression field: (field_identifier) @call.member))
|
|
142
|
+
`;
|
|
143
|
+
/** In Go every package-qualified call (fmt.Println, strings.Split, t.Errorf) is a
|
|
144
|
+
* selector_expression and therefore lands in @call.member — the DOMINANT call form.
|
|
145
|
+
* This allowlist filters the highest-frequency stdlib/testing method+function names
|
|
146
|
+
* so they never create false edges to same-named repo symbols; repo-specific member
|
|
147
|
+
* calls (s.Run(), h.Handle()) pass through and resolve conservatively like TS/PY. */
|
|
148
|
+
const GO_BUILTIN_METHODS = new Set([
|
|
149
|
+
"Error", "String", "Read", "Write", "Close", "Len", "Cap", "Reset", "Bytes", "Text",
|
|
150
|
+
"Scan", "Next", "Err", "Lock", "Unlock", "RLock", "RUnlock", "Done", "Add", "Wait",
|
|
151
|
+
"Print", "Printf", "Println", "Sprintf", "Fprintf", "Errorf", "Fatal", "Fatalf", "Fatalln",
|
|
152
|
+
"Log", "Logf", "Helper", "Run", "Parallel", "Skip", "Skipf", "Cleanup",
|
|
153
|
+
"Get", "Set", "Delete", "Load", "Store", "Range", "Value", "Context", "Deadline",
|
|
154
|
+
"Marshal", "Unmarshal", "Encode", "Decode", "Parse", "Format", "Sub", "Before", "After",
|
|
155
|
+
"Join", "Split", "Contains", "Replace", "ReplaceAll", "TrimSpace", "ToLower", "ToUpper",
|
|
156
|
+
"HasPrefix", "HasSuffix", "WriteString", "ReadString", "ReadAll", "Copy", "New", "Now",
|
|
157
|
+
"Since", "Sleep", "Unix", "Exec", "Query", "QueryRow", "Begin", "Commit", "Rollback",
|
|
158
|
+
]);
|
|
159
|
+
const GO = {
|
|
160
|
+
id: "go",
|
|
161
|
+
extensions: [".go"],
|
|
162
|
+
grammarKey: "go",
|
|
163
|
+
loadGrammar: () => loadNativeTreeSitter().go,
|
|
164
|
+
query: GO_QUERY,
|
|
165
|
+
defNodeTypes: new Set(["function_declaration", "method_declaration", "type_spec", "type_alias"]),
|
|
166
|
+
defKindOf: {
|
|
167
|
+
"fn.def": "function", "method.def": "method", "struct.def": "class",
|
|
168
|
+
"iface.def": "interface", "type.def": "type",
|
|
169
|
+
},
|
|
170
|
+
nameToDef: {
|
|
171
|
+
"fn.name": "fn.def", "method.name": "method.def", "struct.name": "struct.def",
|
|
172
|
+
"iface.name": "iface.def", "type.name": "type.def",
|
|
173
|
+
},
|
|
174
|
+
builtinMethods: GO_BUILTIN_METHODS,
|
|
175
|
+
};
|
|
176
|
+
export const LANGUAGES = [TYPESCRIPT, TSX, PYTHON, GO];
|
|
129
177
|
export const CODE_EXTENSIONS = [...new Set(LANGUAGES.flatMap((l) => l.extensions))];
|
|
130
178
|
export function languageFor(file) {
|
|
131
179
|
for (const lang of LANGUAGES) {
|
|
@@ -4,7 +4,7 @@ import { tmpdir } from "node:os";
|
|
|
4
4
|
import { basename, dirname, join } from "node:path";
|
|
5
5
|
const runtimeRequire = createRequire(import.meta.url);
|
|
6
6
|
const COPY_PREFIX = "hunch-tree-sitter-";
|
|
7
|
-
const NATIVE_PACKAGES = ["tree-sitter", "tree-sitter-typescript", "tree-sitter-python"];
|
|
7
|
+
const NATIVE_PACKAGES = ["tree-sitter", "tree-sitter-typescript", "tree-sitter-python", "tree-sitter-go"];
|
|
8
8
|
let runtime = null;
|
|
9
9
|
function processIsAlive(pid) {
|
|
10
10
|
if (pid === process.pid)
|
|
@@ -69,7 +69,7 @@ export function loadNativeTreeSitter() {
|
|
|
69
69
|
// underscores (tree_sitter_runtime_binding.node, tree_sitter_python_binding.node,
|
|
70
70
|
// …). Missing the underscore names let an already-loaded source-built addon slip
|
|
71
71
|
// past this guard and defeat the file-lock isolation entirely (issue #52).
|
|
72
|
-
const preloaded = Object.keys(runtimeRequire.cache).filter((path) => /(?:tree-sitter(?:-typescript|-python)?|tree_sitter(?:_[a-z]+)*_binding)\.node$/.test(path)
|
|
72
|
+
const preloaded = Object.keys(runtimeRequire.cache).filter((path) => /(?:tree-sitter(?:-typescript|-python|-go)?|tree_sitter(?:_[a-z]+)*_binding)\.node$/.test(path)
|
|
73
73
|
&& !new RegExp(`(?:^|[\\\\/])${COPY_PREFIX}\\d+-`).test(path));
|
|
74
74
|
if (preloaded.length) {
|
|
75
75
|
throw new Error(`tree-sitter native addon was loaded before Hunch could isolate it: ${preloaded.join(", ")}`);
|
|
@@ -87,7 +87,8 @@ export function loadNativeTreeSitter() {
|
|
|
87
87
|
const Parser = runtimeRequire("tree-sitter");
|
|
88
88
|
const languages = runtimeRequire("tree-sitter-typescript");
|
|
89
89
|
const python = runtimeRequire("tree-sitter-python");
|
|
90
|
-
|
|
90
|
+
const go = runtimeRequire("tree-sitter-go");
|
|
91
|
+
runtime = { Parser, typescript: languages.typescript, tsx: languages.tsx, python, go };
|
|
91
92
|
}
|
|
92
93
|
catch (error) {
|
|
93
94
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davesheffer/hunch",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.15.0",
|
|
4
4
|
"mcpName": "io.github.davesheffer/hunch",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Dave Sheffer <dave.sheffer1@gmail.com>",
|
|
@@ -74,6 +74,7 @@
|
|
|
74
74
|
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
75
75
|
"commander": "^15.0.0",
|
|
76
76
|
"tree-sitter": "0.21.1",
|
|
77
|
+
"tree-sitter-go": "^0.23.4",
|
|
77
78
|
"tree-sitter-python": "^0.23.2",
|
|
78
79
|
"tree-sitter-typescript": "^0.23.2",
|
|
79
80
|
"zod": "^4.4.3"
|
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://hunch-pi.vercel.app",
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.15.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.
|
|
16
|
+
"version": "1.15.0",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|