docsgov 0.1.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 +242 -0
- package/dist/apispec/apispec.js +401 -0
- package/dist/apispec/apispec.test.js +444 -0
- package/dist/apispec/errors.js +17 -0
- package/dist/apispec/index.js +2 -0
- package/dist/check/doclinks.js +167 -0
- package/dist/check/index.js +8 -0
- package/dist/check/run.js +391 -0
- package/dist/check/run.test.js +513 -0
- package/dist/check/suggest.js +134 -0
- package/dist/check/suggest.test.js +92 -0
- package/dist/check/tokens.js +125 -0
- package/dist/cmd/main.js +330 -0
- package/dist/cmd/main.test.js +422 -0
- package/dist/codeq/cache.js +71 -0
- package/dist/codeq/cache.test.js +67 -0
- package/dist/codeq/errors.js +52 -0
- package/dist/codeq/grammars/tree-sitter-go.wasm +0 -0
- package/dist/codeq/grammars/tree-sitter-java.wasm +0 -0
- package/dist/codeq/grammars/tree-sitter-javascript.wasm +0 -0
- package/dist/codeq/grammars/tree-sitter-tsx.wasm +0 -0
- package/dist/codeq/grammars/tree-sitter-typescript.wasm +0 -0
- package/dist/codeq/index.js +11 -0
- package/dist/codeq/resolve.test.js +109 -0
- package/dist/codeq/resolver.js +128 -0
- package/dist/codeq/resolver.test.js +124 -0
- package/dist/codeq/resolvers/go.js +242 -0
- package/dist/codeq/resolvers/go.test.js +143 -0
- package/dist/codeq/resolvers/java.js +349 -0
- package/dist/codeq/resolvers/java.test.js +138 -0
- package/dist/codeq/resolvers/java_queries.js +63 -0
- package/dist/codeq/resolvers/javascript.js +412 -0
- package/dist/codeq/resolvers/javascript.test.js +125 -0
- package/dist/codeq/resolvers/javascript_queries.js +46 -0
- package/dist/codeq/resolvers/typescript.js +366 -0
- package/dist/codeq/resolvers/typescript.test.js +180 -0
- package/dist/codeq/resolvers/typescript_queries.js +78 -0
- package/dist/codeq/signature.js +50 -0
- package/dist/codeq/signature.test.js +50 -0
- package/dist/codeq/suggest.js +96 -0
- package/dist/codeq/treesitter.js +122 -0
- package/dist/codeq/treesitter.test.js +118 -0
- package/dist/config/config.js +74 -0
- package/dist/config/config.test.js +98 -0
- package/dist/config/fs.js +116 -0
- package/dist/config/glob.js +82 -0
- package/dist/config/glob.test.js +61 -0
- package/dist/config/index.js +4 -0
- package/dist/dedup/analyzer/analyzer.js +533 -0
- package/dist/dedup/analyzer/analyzer.test.js +530 -0
- package/dist/dedup/analyzer/canonical.js +74 -0
- package/dist/dedup/analyzer/canonical.test.js +70 -0
- package/dist/dedup/analyzer/cosine_clusters.js +169 -0
- package/dist/dedup/analyzer/cosine_clusters.test.js +131 -0
- package/dist/dedup/analyzer/distinctive.js +85 -0
- package/dist/dedup/analyzer/distinctive.test.js +49 -0
- package/dist/dedup/analyzer/exact_clusters.js +63 -0
- package/dist/dedup/analyzer/exact_clusters.test.js +81 -0
- package/dist/dedup/analyzer/index.js +14 -0
- package/dist/dedup/analyzer/multiplicity.js +110 -0
- package/dist/dedup/analyzer/multiplicity.test.js +123 -0
- package/dist/dedup/analyzer/order.js +22 -0
- package/dist/dedup/analyzer/partial_overlaps.js +65 -0
- package/dist/dedup/analyzer/partial_overlaps.test.js +161 -0
- package/dist/dedup/analyzer/preview.js +84 -0
- package/dist/dedup/analyzer/preview.test.js +46 -0
- package/dist/dedup/analyzer/safety.js +27 -0
- package/dist/dedup/analyzer/safety.test.js +39 -0
- package/dist/dedup/config.js +18 -0
- package/dist/dedup/configload.js +299 -0
- package/dist/dedup/configload.test.js +410 -0
- package/dist/dedup/dedup.index.test.js +203 -0
- package/dist/dedup/dedup.js +143 -0
- package/dist/dedup/dedup.test.js +212 -0
- package/dist/dedup/dedupcfg/config.js +112 -0
- package/dist/dedup/dedupcfg/config.test.js +70 -0
- package/dist/dedup/dedupcfg/index.js +1 -0
- package/dist/dedup/deduptypes/index.js +1 -0
- package/dist/dedup/deduptypes/types.js +9 -0
- package/dist/dedup/deduptypes/types.test.js +34 -0
- package/dist/dedup/embedder/cache.js +23 -0
- package/dist/dedup/embedder/cache.test.js +50 -0
- package/dist/dedup/embedder/constants.js +10 -0
- package/dist/dedup/embedder/embedder.js +76 -0
- package/dist/dedup/embedder/embedder.mock.test.js +128 -0
- package/dist/dedup/embedder/embedder.test.js +96 -0
- package/dist/dedup/embedder/errors.js +20 -0
- package/dist/dedup/embedder/errors.test.js +35 -0
- package/dist/dedup/embedder/index.js +4 -0
- package/dist/dedup/embedder/session.js +78 -0
- package/dist/dedup/embedder/session.test.js +172 -0
- package/dist/dedup/gitignore.js +97 -0
- package/dist/dedup/gitignore.test.js +98 -0
- package/dist/dedup/index.js +11 -0
- package/dist/dedup/indexdb/errors.js +48 -0
- package/dist/dedup/indexdb/index.js +6 -0
- package/dist/dedup/indexdb/indexdb.js +302 -0
- package/dist/dedup/indexdb/indexdb.test.js +739 -0
- package/dist/dedup/indexdb/load.js +110 -0
- package/dist/dedup/indexdb/migrations.js +58 -0
- package/dist/dedup/indexdb/schema.js +83 -0
- package/dist/dedup/indexer/index.js +9 -0
- package/dist/dedup/indexer/indexer.js +501 -0
- package/dist/dedup/indexer/indexer.test.js +510 -0
- package/dist/dedup/indexer/links.js +89 -0
- package/dist/dedup/mdsection/anchor.js +60 -0
- package/dist/dedup/mdsection/anchor.test.js +39 -0
- package/dist/dedup/mdsection/blocks.js +409 -0
- package/dist/dedup/mdsection/blocks.test.js +359 -0
- package/dist/dedup/mdsection/index.js +4 -0
- package/dist/dedup/mdsection/parse.js +21 -0
- package/dist/dedup/mdsection/section.js +234 -0
- package/dist/dedup/mdsection/section.test.js +221 -0
- package/dist/dedup/report/floatfmt.js +71 -0
- package/dist/dedup/report/floatfmt.test.js +42 -0
- package/dist/dedup/report/index.js +8 -0
- package/dist/dedup/report/quote.js +77 -0
- package/dist/dedup/report/quote.test.js +67 -0
- package/dist/dedup/report/text.js +251 -0
- package/dist/dedup/report/text.test.js +420 -0
- package/dist/dedup/report_types.js +8 -0
- package/dist/dedup/sectionid/index.js +1 -0
- package/dist/dedup/sectionid/sectionid.js +16 -0
- package/dist/dedup/sectionid/sectionid.test.js +49 -0
- package/dist/guard/api/errors.js +12 -0
- package/dist/guard/api/index.js +2 -0
- package/dist/guard/api/parser.js +81 -0
- package/dist/guard/api/parser.test.js +58 -0
- package/dist/guard/api/types.js +1 -0
- package/dist/guard/code/errors.js +16 -0
- package/dist/guard/code/index.js +2 -0
- package/dist/guard/code/parser.js +54 -0
- package/dist/guard/code/parser.test.js +111 -0
- package/dist/guard/code/types.js +6 -0
- package/dist/index.js +1 -0
- package/dist/index.test.js +5 -0
- package/dist/repo/boundary.js +92 -0
- package/dist/repo/boundary.test.js +65 -0
- package/dist/repo/errors.js +56 -0
- package/dist/repo/errors.test.js +85 -0
- package/dist/repo/exists.test.js +72 -0
- package/dist/repo/filename.js +46 -0
- package/dist/repo/filename.test.js +39 -0
- package/dist/repo/fs.js +53 -0
- package/dist/repo/index.js +7 -0
- package/dist/repo/overlay.js +36 -0
- package/dist/repo/overlay.test.js +80 -0
- package/dist/repo/repo.js +353 -0
- package/dist/repo/repo.test.js +255 -0
- package/dist/repo/testutil.js +27 -0
- package/dist/repo/write.test.js +125 -0
- package/dist/report/color.js +73 -0
- package/dist/report/index.js +1 -0
- package/dist/report/report.js +112 -0
- package/dist/report/report.test.js +368 -0
- package/dist/violation/index.js +1 -0
- package/dist/violation/types.js +22 -0
- package/dist/violation/types.test.js +70 -0
- package/package.json +48 -0
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { parseCodeRef } from "../guard/code/index.js";
|
|
3
|
+
import { createDefaultResolver } from "./resolver.js";
|
|
4
|
+
// WHY: this is the end-to-end seam test for the WHOLE codeq dispatch. The unit
|
|
5
|
+
// tests (resolver.test.ts) prove routing with FAKE per-language resolvers; this
|
|
6
|
+
// proves the REAL wiring works across grammars: createDefaultResolver loads the
|
|
7
|
+
// vendored wasm, picks the right grammar by extension, and a {{code:…}} token —
|
|
8
|
+
// parsed by the guard/code parser, exactly as the check layer does — resolves
|
|
9
|
+
// true when the symbol exists and false when it does not. Covering both Go and
|
|
10
|
+
// TypeScript (two distinct grammars routed by extension) is what proves the
|
|
11
|
+
// dispatch is per-language and not accidentally a single hard-wired grammar.
|
|
12
|
+
const enc = new TextEncoder();
|
|
13
|
+
class MemFS {
|
|
14
|
+
files;
|
|
15
|
+
constructor(files) {
|
|
16
|
+
this.files = files;
|
|
17
|
+
}
|
|
18
|
+
async readFile(name) {
|
|
19
|
+
const content = this.files[name];
|
|
20
|
+
if (content === undefined) {
|
|
21
|
+
const err = new Error(`ENOENT: ${name}`);
|
|
22
|
+
err.code = "ENOENT";
|
|
23
|
+
throw err;
|
|
24
|
+
}
|
|
25
|
+
return enc.encode(content);
|
|
26
|
+
}
|
|
27
|
+
async readDir() {
|
|
28
|
+
return [];
|
|
29
|
+
}
|
|
30
|
+
sub() {
|
|
31
|
+
return this;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
describe("codeq end-to-end resolve (real grammars)", () => {
|
|
35
|
+
it("resolves a Go {{code}} ref against inline source", async () => {
|
|
36
|
+
const r = await createDefaultResolver();
|
|
37
|
+
const fs = new MemFS({
|
|
38
|
+
"pkg/foo.go": "package pkg\n\nfunc Bar() {}\n\ntype Baz struct{}\n",
|
|
39
|
+
});
|
|
40
|
+
// Present function and present type both resolve true.
|
|
41
|
+
expect(await r.resolve(fs, parseCodeRef("{{code:pkg/foo.go#Bar}}"))).toBe(true);
|
|
42
|
+
expect(await r.resolve(fs, parseCodeRef("{{code:pkg/foo.go#Baz}}"))).toBe(true);
|
|
43
|
+
// An absent symbol resolves false (not an error) — proving the real grammar
|
|
44
|
+
// ran and searched, rather than a stub blanket-returning true.
|
|
45
|
+
expect(await r.resolve(fs, parseCodeRef("{{code:pkg/foo.go#Missing}}"))).toBe(false);
|
|
46
|
+
});
|
|
47
|
+
it("resolves a TypeScript {{code}} ref against inline source", async () => {
|
|
48
|
+
const r = await createDefaultResolver();
|
|
49
|
+
const fs = new MemFS({
|
|
50
|
+
"src/api.ts": "export function fetchUser(id: string): void {}\n" +
|
|
51
|
+
"export class Client {}\n",
|
|
52
|
+
});
|
|
53
|
+
expect(await r.resolve(fs, parseCodeRef("{{code:src/api.ts#fetchUser}}"))).toBe(true);
|
|
54
|
+
expect(await r.resolve(fs, parseCodeRef("{{code:src/api.ts#Client}}"))).toBe(true);
|
|
55
|
+
expect(await r.resolve(fs, parseCodeRef("{{code:src/api.ts#nope}}"))).toBe(false);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
// WHY: suggest is the candidate oracle behind a not-found ref. These prove the
|
|
59
|
+
// REAL grammar enumerates the same symbol space resolve() searches — so a typo'd
|
|
60
|
+
// ref can be answered with the names that actually exist — and that the
|
|
61
|
+
// owner-missing signal distinguishes "wrong type" from "wrong member".
|
|
62
|
+
describe("codeq suggest (real grammars)", () => {
|
|
63
|
+
it("lists top-level symbols for a bare symbol ref", async () => {
|
|
64
|
+
const r = await createDefaultResolver();
|
|
65
|
+
const fs = new MemFS({
|
|
66
|
+
"pkg/foo.go": "package pkg\n\nfunc Bar() {}\n\ntype Baz struct{}\n",
|
|
67
|
+
});
|
|
68
|
+
const sug = await r.suggest(fs, parseCodeRef("{{code:pkg/foo.go#Batz}}"));
|
|
69
|
+
expect(sug.kind).toBe("symbol");
|
|
70
|
+
expect(sug.ownerMissing).toBeUndefined();
|
|
71
|
+
expect([...sug.candidates].sort()).toEqual(["Bar", "Baz"]);
|
|
72
|
+
});
|
|
73
|
+
it("lists a type's members for a member ref whose owner exists", async () => {
|
|
74
|
+
const r = await createDefaultResolver();
|
|
75
|
+
const fs = new MemFS({
|
|
76
|
+
"pkg/foo.go": "package pkg\n\ntype Ctl struct{}\n\nfunc (c Ctl) Start() {}\nfunc (c Ctl) Stop() {}\n",
|
|
77
|
+
});
|
|
78
|
+
const sug = await r.suggest(fs, parseCodeRef("{{code:pkg/foo.go#Ctl.Strt}}"));
|
|
79
|
+
expect(sug.kind).toBe("member");
|
|
80
|
+
expect(sug.ownerMissing).toBeUndefined();
|
|
81
|
+
expect([...sug.candidates].sort()).toEqual(["Start", "Stop"]);
|
|
82
|
+
});
|
|
83
|
+
it("signals owner-missing when a member ref's type is absent", async () => {
|
|
84
|
+
const r = await createDefaultResolver();
|
|
85
|
+
const fs = new MemFS({
|
|
86
|
+
"pkg/foo.go": "package pkg\n\ntype Ctl struct{}\n\nfunc (c Ctl) Start() {}\n",
|
|
87
|
+
});
|
|
88
|
+
const sug = await r.suggest(fs, parseCodeRef("{{code:pkg/foo.go#Ctlx.Start}}"));
|
|
89
|
+
expect(sug.kind).toBe("member");
|
|
90
|
+
expect(sug.ownerMissing).toBe("Ctlx");
|
|
91
|
+
expect(sug.candidates).toEqual([]);
|
|
92
|
+
});
|
|
93
|
+
it("lists a function's params for a param ref (cross-grammar: TS)", async () => {
|
|
94
|
+
const r = await createDefaultResolver();
|
|
95
|
+
const fs = new MemFS({
|
|
96
|
+
"src/api.ts": "export function fetchUser(id: string, token: string): void {}\n",
|
|
97
|
+
});
|
|
98
|
+
const sug = await r.suggest(fs, parseCodeRef("{{code:src/api.ts#fetchUser@params.tokn}}"));
|
|
99
|
+
expect(sug.kind).toBe("param");
|
|
100
|
+
expect(sug.ownerMissing).toBeUndefined();
|
|
101
|
+
expect([...sug.candidates].sort()).toEqual(["id", "token"]);
|
|
102
|
+
});
|
|
103
|
+
it("yields no candidates for an unsupported extension (graceful)", async () => {
|
|
104
|
+
const r = await createDefaultResolver();
|
|
105
|
+
const fs = new MemFS({ "notes/x.rb": "class Foo; end\n" });
|
|
106
|
+
const sug = await r.suggest(fs, parseCodeRef("{{code:notes/x.rb#Foo}}"));
|
|
107
|
+
expect(sug.candidates).toEqual([]);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { SourceCache } from "./cache.js";
|
|
2
|
+
import { UnsupportedLanguageError } from "./errors.js";
|
|
3
|
+
import { refKind } from "./suggest.js";
|
|
4
|
+
import { createGoResolver } from "./resolvers/go.js";
|
|
5
|
+
import { createJavaResolver } from "./resolvers/java.js";
|
|
6
|
+
import { createJSResolver } from "./resolvers/javascript.js";
|
|
7
|
+
import { createTSResolver } from "./resolvers/typescript.js";
|
|
8
|
+
import { loadGrammar } from "./treesitter.js";
|
|
9
|
+
/**
|
|
10
|
+
* DefaultResolver is the by-extension dispatcher (the analogue of Go's
|
|
11
|
+
* byExtResolver + DefaultResolver). Build it with the async factory
|
|
12
|
+
* createDefaultResolver(); then call resolve(root, ref) once per CodeRef.
|
|
13
|
+
*
|
|
14
|
+
* resolve(root, ref) is ASYNC because it reads the file from the async FS; the
|
|
15
|
+
* per-language Resolver it dispatches to is SYNC.
|
|
16
|
+
*/
|
|
17
|
+
export class DefaultResolver {
|
|
18
|
+
byExt;
|
|
19
|
+
cache = new SourceCache();
|
|
20
|
+
/**
|
|
21
|
+
* @param byExt maps a file extension (including the leading ".") to the
|
|
22
|
+
* per-language Resolver for that extension. The same TS Resolver instance is
|
|
23
|
+
* registered under both ".ts" and ".tsx" only if it was built from the
|
|
24
|
+
* matching grammar; in practice .ts and .tsx are DISTINCT grammars and get
|
|
25
|
+
* distinct resolvers (see createDefaultResolver).
|
|
26
|
+
*/
|
|
27
|
+
constructor(byExt) {
|
|
28
|
+
this.byExt = byExt;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* resolve looks up `ref` in `root`. Returns true when found, false when
|
|
32
|
+
* absent. Throws UnsupportedLanguageError when no resolver is registered for
|
|
33
|
+
* the path's extension, FileNotFoundError when the file is missing, and
|
|
34
|
+
* ParseFailedError when the file fails to parse.
|
|
35
|
+
*/
|
|
36
|
+
async resolve(root, ref) {
|
|
37
|
+
const ext = extname(ref.Path);
|
|
38
|
+
const sub = this.byExt.get(ext);
|
|
39
|
+
if (sub === undefined) {
|
|
40
|
+
throw new UnsupportedLanguageError(ext);
|
|
41
|
+
}
|
|
42
|
+
// SourceCache maps a missing file to FileNotFoundError; any other I/O fault
|
|
43
|
+
// propagates. On success we hand the decoded string to the sync resolver.
|
|
44
|
+
const source = await this.cache.read(root, ref.Path);
|
|
45
|
+
return sub.resolve(source, ref.Path, ref);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* suggest lists candidate symbols for a not-found `ref`, dispatching to the
|
|
49
|
+
* per-language resolver by extension. It NEVER throws (suggestions are an
|
|
50
|
+
* enhancement, not a check): an unsupported extension, a missing file, a
|
|
51
|
+
* read/parse fault, or a resolver without suggest support all degrade to an
|
|
52
|
+
* empty candidate set. Call it only after resolve(root, ref) returned false.
|
|
53
|
+
*/
|
|
54
|
+
async suggest(root, ref) {
|
|
55
|
+
const empty = { kind: refKind(ref), candidates: [] };
|
|
56
|
+
const sub = this.byExt.get(extname(ref.Path));
|
|
57
|
+
if (sub === undefined || sub.suggest === undefined) {
|
|
58
|
+
return empty;
|
|
59
|
+
}
|
|
60
|
+
let source;
|
|
61
|
+
try {
|
|
62
|
+
source = await this.cache.read(root, ref.Path);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return empty;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
return sub.suggest(source, ref.Path, ref);
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
return empty;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* extname returns the file extension of a slash-separated path, INCLUDING the
|
|
77
|
+
* leading ".", matching Go's path.Ext (which operates on the final element after
|
|
78
|
+
* the last "/" and returns "" when there is no dot). We avoid node:path here
|
|
79
|
+
* because ref.Path is always slash-separated (io/fs convention), and path.Ext's
|
|
80
|
+
* semantics are simple enough to mirror exactly.
|
|
81
|
+
*/
|
|
82
|
+
function extname(p) {
|
|
83
|
+
const slash = p.lastIndexOf("/");
|
|
84
|
+
const base = slash >= 0 ? p.slice(slash + 1) : p;
|
|
85
|
+
const dot = base.lastIndexOf(".");
|
|
86
|
+
// A leading-dot-only basename (".foo") has no extension in path.Ext terms only
|
|
87
|
+
// if the dot is at index 0 AND there's no later dot — but path.Ext(".foo")
|
|
88
|
+
// returns ".foo". Mirror path.Ext exactly: it returns base[dot:] for the LAST
|
|
89
|
+
// dot, or "" if none.
|
|
90
|
+
return dot >= 0 ? base.slice(dot) : "";
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* createDefaultResolver eagerly loads all 5 vendored grammars, builds one
|
|
94
|
+
* resolver per language, and wires the by-extension dispatch table — the async
|
|
95
|
+
* analogue of Go's DefaultResolver() (internal/codeq/codeq_on.go).
|
|
96
|
+
*
|
|
97
|
+
* The extension map mirrors Go exactly:
|
|
98
|
+
* .go → go, .java → java,
|
|
99
|
+
* .ts → typescript grammar, .tsx → tsx grammar (DISTINCT grammars/queries),
|
|
100
|
+
* .js → javascript, .jsx → javascript.
|
|
101
|
+
*
|
|
102
|
+
* Grammars load concurrently; per-language resolve() stays sync after this.
|
|
103
|
+
*/
|
|
104
|
+
export async function createDefaultResolver() {
|
|
105
|
+
// Annotate the tuple element type explicitly: a plain Promise.all over an array
|
|
106
|
+
// literal infers a tuple, so destructured elements stay Language (not
|
|
107
|
+
// Language | undefined as a Language[] would under noUncheckedIndexedAccess).
|
|
108
|
+
const [go, java, js, ts, tsx] = await Promise.all([
|
|
109
|
+
loadGrammar("go"),
|
|
110
|
+
loadGrammar("java"),
|
|
111
|
+
loadGrammar("javascript"),
|
|
112
|
+
loadGrammar("typescript"),
|
|
113
|
+
loadGrammar("tsx"),
|
|
114
|
+
]);
|
|
115
|
+
const goR = createGoResolver(go);
|
|
116
|
+
const javaR = createJavaResolver(java);
|
|
117
|
+
const jsR = createJSResolver(js);
|
|
118
|
+
const tsR = createTSResolver(ts);
|
|
119
|
+
const tsxR = createTSResolver(tsx);
|
|
120
|
+
return new DefaultResolver(new Map([
|
|
121
|
+
[".go", goR],
|
|
122
|
+
[".java", javaR],
|
|
123
|
+
[".ts", tsR],
|
|
124
|
+
[".tsx", tsxR],
|
|
125
|
+
[".js", jsR],
|
|
126
|
+
[".jsx", jsR],
|
|
127
|
+
]));
|
|
128
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { DefaultResolver, createDefaultResolver, } from "./resolver.js";
|
|
3
|
+
import { FileNotFoundError, UnsupportedLanguageError } from "./errors.js";
|
|
4
|
+
// WHY: DefaultResolver is the top-level routing seam (the analogue of Go's
|
|
5
|
+
// byExtResolver). If it routes by the wrong extension, fails to surface
|
|
6
|
+
// UnsupportedLanguage, or swallows FileNotFound, every code-ref check is
|
|
7
|
+
// silently miscategorised. The per-language resolvers don't exist yet
|
|
8
|
+
// (WireGate), so we inject FAKE resolvers via the constructor to test the
|
|
9
|
+
// routing + error contract in isolation, exactly as Go's NewResolver(byExt) is
|
|
10
|
+
// testable without the real tree-sitter resolvers.
|
|
11
|
+
const enc = new TextEncoder();
|
|
12
|
+
class MemFS {
|
|
13
|
+
files;
|
|
14
|
+
constructor(files) {
|
|
15
|
+
this.files = files;
|
|
16
|
+
}
|
|
17
|
+
async readFile(name) {
|
|
18
|
+
const content = this.files[name];
|
|
19
|
+
if (content === undefined) {
|
|
20
|
+
const err = new Error(`ENOENT: ${name}`);
|
|
21
|
+
err.code = "ENOENT";
|
|
22
|
+
throw err;
|
|
23
|
+
}
|
|
24
|
+
return enc.encode(content);
|
|
25
|
+
}
|
|
26
|
+
async readDir() {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
sub() {
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** spyResolver records the (source, path) it was handed and returns `found`. */
|
|
34
|
+
function spyResolver(found) {
|
|
35
|
+
const calls = [];
|
|
36
|
+
return {
|
|
37
|
+
calls,
|
|
38
|
+
resolve(source, path, ref) {
|
|
39
|
+
calls.push({ source, path, ref });
|
|
40
|
+
return found;
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
function ref(path, symbol) {
|
|
45
|
+
return { Path: path, Symbol: symbol, Member: "", Param: "" };
|
|
46
|
+
}
|
|
47
|
+
describe("DefaultResolver dispatch", () => {
|
|
48
|
+
it("routes by extension and hands the decoded source to the sub-resolver", async () => {
|
|
49
|
+
const go = spyResolver(true);
|
|
50
|
+
const r = new DefaultResolver(new Map([[".go", go]]));
|
|
51
|
+
const fs = new MemFS({ "pkg/foo.go": "package pkg\nfunc Bar(){}\n" });
|
|
52
|
+
const found = await r.resolve(fs, ref("pkg/foo.go", "Bar"));
|
|
53
|
+
expect(found).toBe(true);
|
|
54
|
+
expect(go.calls).toHaveLength(1);
|
|
55
|
+
// The sub-resolver receives the ALREADY-DECODED string (the async/sync split)
|
|
56
|
+
// and the path (for ParseFailedError construction), not an FS.
|
|
57
|
+
expect(go.calls[0].source).toBe("package pkg\nfunc Bar(){}\n");
|
|
58
|
+
expect(go.calls[0].path).toBe("pkg/foo.go");
|
|
59
|
+
});
|
|
60
|
+
it("returns the sub-resolver's false (absent symbol) verbatim", async () => {
|
|
61
|
+
const go = spyResolver(false);
|
|
62
|
+
const r = new DefaultResolver(new Map([[".go", go]]));
|
|
63
|
+
const fs = new MemFS({ "pkg/foo.go": "package pkg\n" });
|
|
64
|
+
expect(await r.resolve(fs, ref("pkg/foo.go", "Nope"))).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
it("throws UnsupportedLanguageError for an unregistered extension", async () => {
|
|
67
|
+
// WHY: an unknown extension is NOT "symbol absent" — it means docgov has no
|
|
68
|
+
// grammar for it; the caller reports it differently. Mirrors Go returning
|
|
69
|
+
// ErrUnsupportedLanguage (wrapped with the ext).
|
|
70
|
+
const r = new DefaultResolver(new Map([[".go", spyResolver(true)]]));
|
|
71
|
+
const fs = new MemFS({ "other/baz.xyz": "whatever" });
|
|
72
|
+
await expect(r.resolve(fs, ref("other/baz.xyz", "Q"))).rejects.toBeInstanceOf(UnsupportedLanguageError);
|
|
73
|
+
});
|
|
74
|
+
it("propagates FileNotFoundError when the file is missing", async () => {
|
|
75
|
+
// WHY: a missing file is an operational failure distinct from absence; the
|
|
76
|
+
// dispatch must surface it rather than treating it as Found=false.
|
|
77
|
+
const r = new DefaultResolver(new Map([[".go", spyResolver(true)]]));
|
|
78
|
+
const fs = new MemFS({});
|
|
79
|
+
await expect(r.resolve(fs, ref("pkg/gone.go", "X"))).rejects.toBeInstanceOf(FileNotFoundError);
|
|
80
|
+
});
|
|
81
|
+
it("routes .ts and .tsx to their own (distinct) resolvers", async () => {
|
|
82
|
+
const ts = spyResolver(true);
|
|
83
|
+
const tsx = spyResolver(true);
|
|
84
|
+
const r = new DefaultResolver(new Map([
|
|
85
|
+
[".ts", ts],
|
|
86
|
+
[".tsx", tsx],
|
|
87
|
+
]));
|
|
88
|
+
const fs = new MemFS({ "a.ts": "x", "b.tsx": "y" });
|
|
89
|
+
await r.resolve(fs, ref("a.ts", "X"));
|
|
90
|
+
await r.resolve(fs, ref("b.tsx", "Y"));
|
|
91
|
+
expect(ts.calls).toHaveLength(1);
|
|
92
|
+
expect(tsx.calls).toHaveLength(1);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
describe("createDefaultResolver (wired)", () => {
|
|
96
|
+
it("loads all 5 grammars and returns a working DefaultResolver", async () => {
|
|
97
|
+
// WHY: the factory is the only place the 5 vendored wasm grammars are loaded
|
|
98
|
+
// and bound to per-language resolvers. If any grammar fails to load or a
|
|
99
|
+
// factory throws, this rejects — pinning that the wasm/load path stays on the
|
|
100
|
+
// critical path and that wiring produces a real (not half-built) resolver.
|
|
101
|
+
const r = await createDefaultResolver();
|
|
102
|
+
expect(r).toBeInstanceOf(DefaultResolver);
|
|
103
|
+
});
|
|
104
|
+
it("routes every Go-supported extension and rejects an unknown one", async () => {
|
|
105
|
+
// WHY: the extension map must mirror Go's DefaultResolver exactly
|
|
106
|
+
// (.go/.java/.ts/.tsx/.js/.jsx). A registered extension resolves (here:
|
|
107
|
+
// absent symbol -> false, NOT UnsupportedLanguage); an unmapped one throws
|
|
108
|
+
// UnsupportedLanguageError. This catches a dropped or mis-spelled map entry.
|
|
109
|
+
const r = await createDefaultResolver();
|
|
110
|
+
const fs = new MemFS({
|
|
111
|
+
"a.go": "package a\n",
|
|
112
|
+
"B.java": "class B {}\n",
|
|
113
|
+
"c.ts": "export const c = 1;\n",
|
|
114
|
+
"d.tsx": "export const d = 1;\n",
|
|
115
|
+
"e.js": "export const e = 1;\n",
|
|
116
|
+
"f.jsx": "export const f = 1;\n",
|
|
117
|
+
});
|
|
118
|
+
for (const path of ["a.go", "B.java", "c.ts", "d.tsx", "e.js", "f.jsx"]) {
|
|
119
|
+
// A missing symbol must come back false (extension WAS routed), not throw.
|
|
120
|
+
expect(await r.resolve(fs, ref(path, "Absent"))).toBe(false);
|
|
121
|
+
}
|
|
122
|
+
await expect(r.resolve(new MemFS({ "g.rb": "x" }), ref("g.rb", "X"))).rejects.toBeInstanceOf(UnsupportedLanguageError);
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
// go.ts ports internal/codeq/go_lang.go + go_queries.go: the Go per-language
|
|
2
|
+
// Resolver. It is a faithful port of the *goResolver Resolve body MINUS the
|
|
3
|
+
// cache.Read + ErrFileNotFound prelude (the dispatch layer owns I/O), per the
|
|
4
|
+
// async/sync split recorded in resolver.ts.
|
|
5
|
+
//
|
|
6
|
+
// The query S-expressions below are VERBATIM from go_queries.go — the upstream
|
|
7
|
+
// tree-sitter-go node-type names match the vendored wasm grammar, so the Go
|
|
8
|
+
// queries port unchanged. Queries are compiled ONCE at factory time (the
|
|
9
|
+
// analogue of Go's package-level mustQuery vars) and reused across parses.
|
|
10
|
+
import { ParseFailedError } from "../errors.js";
|
|
11
|
+
import { signaturesMatch } from "../signature.js";
|
|
12
|
+
import { collectCapture, collectOwned, refKind, suggestFromExtractors, } from "../suggest.js";
|
|
13
|
+
import { Parser, compileQuery, nodeText, parseTree, runQuery, } from "../treesitter.js";
|
|
14
|
+
// goSymbolQuery matches top-level function, type, var, and const declarations.
|
|
15
|
+
// Each match has a single capture named "name" with the declared identifier.
|
|
16
|
+
const GO_SYMBOL_QUERY = `
|
|
17
|
+
(function_declaration name: (identifier) @name)
|
|
18
|
+
(type_declaration (type_spec name: (type_identifier) @name))
|
|
19
|
+
(var_declaration (var_spec name: (identifier) @name))
|
|
20
|
+
(const_declaration (const_spec name: (identifier) @name))
|
|
21
|
+
`;
|
|
22
|
+
// goMemberQuery matches struct fields and method declarations.
|
|
23
|
+
// Each match provides @owner (the type name) and @name (field or method name).
|
|
24
|
+
// Note: the method pattern binds the receiver TYPE via (type_identifier),
|
|
25
|
+
// i.e. a VALUE receiver "(b Baz)" — a POINTER receiver "(b *Baz)" parses the
|
|
26
|
+
// receiver type as (pointer_type ...), which (type_identifier) does not match,
|
|
27
|
+
// so pointer-receiver methods do NOT resolve as members. This is intentional
|
|
28
|
+
// and preserved from the Go original.
|
|
29
|
+
const GO_MEMBER_QUERY = `
|
|
30
|
+
(type_declaration (type_spec name: (type_identifier) @owner (struct_type (field_declaration_list (field_declaration name: (field_identifier) @name)))))
|
|
31
|
+
(method_declaration receiver: (parameter_list (parameter_declaration type: (type_identifier) @owner)) name: (field_identifier) @name)
|
|
32
|
+
`;
|
|
33
|
+
// goMemberDeclQuery matches method declarations on named receiver types.
|
|
34
|
+
// Each match provides @owner (receiver type name), @mname (method name), and
|
|
35
|
+
// @mdecl (the full method_declaration node — used to walk parameter types for
|
|
36
|
+
// overload disambiguation by signature). Same value-receiver caveat as above.
|
|
37
|
+
const GO_MEMBER_DECL_QUERY = `
|
|
38
|
+
(method_declaration receiver: (parameter_list (parameter_declaration type: (type_identifier) @owner)) name: (field_identifier) @mname) @mdecl
|
|
39
|
+
`;
|
|
40
|
+
// goParamQuery matches parameters of function and method declarations.
|
|
41
|
+
// Each match provides @fn (the function or method name) and @param (the
|
|
42
|
+
// parameter name).
|
|
43
|
+
const GO_PARAM_QUERY = `
|
|
44
|
+
(function_declaration name: (identifier) @fn parameters: (parameter_list (parameter_declaration name: (identifier) @param)))
|
|
45
|
+
(method_declaration name: (field_identifier) @fn parameters: (parameter_list (parameter_declaration name: (identifier) @param)))
|
|
46
|
+
`;
|
|
47
|
+
/**
|
|
48
|
+
* createGoResolver builds the Go Resolver from an already-loaded tree-sitter
|
|
49
|
+
* Language. It compiles all four queries eagerly (compileQuery throws on a bad
|
|
50
|
+
* query — the mustQuery-panic contract) and returns a sync Resolver. The
|
|
51
|
+
* compiled queries are captured in the closure and reused across every parse,
|
|
52
|
+
* mirroring Go's package-level mustQuery vars.
|
|
53
|
+
*/
|
|
54
|
+
export function createGoResolver(language) {
|
|
55
|
+
const symbolQuery = compileQuery(language, GO_SYMBOL_QUERY);
|
|
56
|
+
const memberQuery = compileQuery(language, GO_MEMBER_QUERY);
|
|
57
|
+
const memberDeclQuery = compileQuery(language, GO_MEMBER_DECL_QUERY);
|
|
58
|
+
const paramQuery = compileQuery(language, GO_PARAM_QUERY);
|
|
59
|
+
return {
|
|
60
|
+
resolve(source, path, ref) {
|
|
61
|
+
// Per-call parser — cheap to create. parse/matches are sync post-init.
|
|
62
|
+
const parser = new Parser();
|
|
63
|
+
parser.setLanguage(language);
|
|
64
|
+
const root = parseTree(parser, source);
|
|
65
|
+
// Invalid Go produces ERROR nodes, not a parse failure. This is what the
|
|
66
|
+
// resolver maps to ParseFailedError (Go's ErrParseFailed) so callers can
|
|
67
|
+
// distinguish "could not check" from "symbol genuinely absent".
|
|
68
|
+
if (root.hasError) {
|
|
69
|
+
throw new ParseFailedError(path);
|
|
70
|
+
}
|
|
71
|
+
// Dispatch on ref facets EXACTLY as go_lang.go's Resolve switch does:
|
|
72
|
+
// Member → findMember, else Param → findParam, else findSymbol.
|
|
73
|
+
if (ref.Member !== "") {
|
|
74
|
+
return findMember(root, memberQuery, memberDeclQuery, ref.Symbol, ref.Member, ref.Signature);
|
|
75
|
+
}
|
|
76
|
+
if (ref.Param !== "") {
|
|
77
|
+
return findParam(root, paramQuery, ref.Symbol, ref.Param);
|
|
78
|
+
}
|
|
79
|
+
return findSymbol(root, symbolQuery, ref.Symbol);
|
|
80
|
+
},
|
|
81
|
+
suggest(source, _path, ref) {
|
|
82
|
+
const parser = new Parser();
|
|
83
|
+
parser.setLanguage(language);
|
|
84
|
+
const root = parseTree(parser, source);
|
|
85
|
+
// Unlike resolve, a malformed tree is not an error here — just no candidates.
|
|
86
|
+
if (root.hasError) {
|
|
87
|
+
return { kind: refKind(ref), candidates: [] };
|
|
88
|
+
}
|
|
89
|
+
const ex = {
|
|
90
|
+
symbolNames: (r) => collectCapture(r, symbolQuery, "name"),
|
|
91
|
+
memberNames: (r, owner) => collectOwned(r, memberQuery, owner, "owner", "name"),
|
|
92
|
+
paramNames: (r, fn) => collectOwned(r, paramQuery, fn, "fn", "param"),
|
|
93
|
+
};
|
|
94
|
+
return suggestFromExtractors(root, ref, ex);
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* findSymbol searches for a top-level declaration with the given name. Returns
|
|
100
|
+
* true when found, false when absent.
|
|
101
|
+
*/
|
|
102
|
+
function findSymbol(root, symbolQuery, name) {
|
|
103
|
+
for (const m of runQuery(root, symbolQuery)) {
|
|
104
|
+
const n = m.captures["name"];
|
|
105
|
+
if (n !== undefined && nodeText(n) === name) {
|
|
106
|
+
return true;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* findMember searches for a struct field or method named member on symbol.
|
|
113
|
+
*
|
|
114
|
+
* When sig is undefined (name-only), the flat member query matches fields and
|
|
115
|
+
* methods by name. When sig is defined (including the zero-arg empty array),
|
|
116
|
+
* only method declarations whose parameter types satisfy signaturesMatch are
|
|
117
|
+
* considered — struct fields have no signature and are never matched in this
|
|
118
|
+
* branch. Returns true when found, false when absent.
|
|
119
|
+
*/
|
|
120
|
+
function findMember(root, memberQuery, memberDeclQuery, symbol, member, sig) {
|
|
121
|
+
if (sig === undefined) {
|
|
122
|
+
// Name-only: flat member query (matches fields and value-receiver methods).
|
|
123
|
+
for (const m of runQuery(root, memberQuery)) {
|
|
124
|
+
const owner = m.captures["owner"];
|
|
125
|
+
const name = m.captures["name"];
|
|
126
|
+
if (owner !== undefined &&
|
|
127
|
+
name !== undefined &&
|
|
128
|
+
nodeText(owner) === symbol &&
|
|
129
|
+
nodeText(name) === member) {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
// Signature-filtered: iterate method declarations and check param types.
|
|
136
|
+
for (const m of runQuery(root, memberDeclQuery)) {
|
|
137
|
+
const owner = m.captures["owner"];
|
|
138
|
+
const mname = m.captures["mname"];
|
|
139
|
+
const mdecl = m.captures["mdecl"];
|
|
140
|
+
if (owner !== undefined &&
|
|
141
|
+
mname !== undefined &&
|
|
142
|
+
mdecl !== undefined &&
|
|
143
|
+
nodeText(owner) === symbol &&
|
|
144
|
+
nodeText(mname) === member &&
|
|
145
|
+
signaturesMatch(sig, collectGoParamTypes(mdecl))) {
|
|
146
|
+
return true;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return false;
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* findParam searches for a parameter named param on a function or method named
|
|
153
|
+
* symbol. Returns true when found, false when absent.
|
|
154
|
+
*/
|
|
155
|
+
function findParam(root, paramQuery, symbol, param) {
|
|
156
|
+
for (const m of runQuery(root, paramQuery)) {
|
|
157
|
+
const fn = m.captures["fn"];
|
|
158
|
+
const pname = m.captures["param"];
|
|
159
|
+
if (fn !== undefined &&
|
|
160
|
+
pname !== undefined &&
|
|
161
|
+
nodeText(fn) === symbol &&
|
|
162
|
+
nodeText(pname) === param) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* collectGoParamTypes walks a method_declaration or function_declaration node
|
|
170
|
+
* and returns the declared parameter types in document order. Grouped params
|
|
171
|
+
* (a, b int) repeat the shared type once per declared name so the returned
|
|
172
|
+
* arity is correct. Variadic params (xs ...string) are collected as the Go
|
|
173
|
+
* slice form "[]string" — the effective type a doc author writes — since Go's
|
|
174
|
+
* ... is a prefix qualifier, not a suffix.
|
|
175
|
+
*
|
|
176
|
+
* A method_declaration has two parameter_list children: the first is the
|
|
177
|
+
* receiver list ("(c Ctl)"), the second is the parameter list. A
|
|
178
|
+
* function_declaration has only one parameter_list child. We take the LAST
|
|
179
|
+
* parameter_list to skip the receiver in both cases.
|
|
180
|
+
*/
|
|
181
|
+
function collectGoParamTypes(decl) {
|
|
182
|
+
// Collect all parameter_list children; the parameters list is the last one
|
|
183
|
+
// (for method_declaration the first is the receiver; function_declaration has
|
|
184
|
+
// only one).
|
|
185
|
+
const paramLists = [];
|
|
186
|
+
for (let i = 0; i < decl.childCount; i++) {
|
|
187
|
+
const c = decl.child(i);
|
|
188
|
+
if (c !== null && c.type === "parameter_list") {
|
|
189
|
+
paramLists.push(c);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (paramLists.length === 0) {
|
|
193
|
+
return [];
|
|
194
|
+
}
|
|
195
|
+
const pl = paramLists[paramLists.length - 1];
|
|
196
|
+
const types = [];
|
|
197
|
+
for (let i = 0; i < pl.childCount; i++) {
|
|
198
|
+
const pd = pl.child(i);
|
|
199
|
+
if (pd === null) {
|
|
200
|
+
continue;
|
|
201
|
+
}
|
|
202
|
+
if (pd.type === "parameter_declaration") {
|
|
203
|
+
// Count identifier children (names) to handle grouped params (a, b int).
|
|
204
|
+
// The type node is the last non-comma, non-identifier child.
|
|
205
|
+
let nameCount = 0;
|
|
206
|
+
let typeNode = null;
|
|
207
|
+
for (let j = 0; j < pd.childCount; j++) {
|
|
208
|
+
const c = pd.child(j);
|
|
209
|
+
if (c === null) {
|
|
210
|
+
continue;
|
|
211
|
+
}
|
|
212
|
+
if (c.type === "identifier") {
|
|
213
|
+
nameCount++;
|
|
214
|
+
}
|
|
215
|
+
else if (c.type !== ",") {
|
|
216
|
+
// Anything that isn't a name identifier or comma is the type.
|
|
217
|
+
typeNode = c;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (typeNode === null) {
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
223
|
+
// Clamp nameCount to at least 1 (unnamed params like "_ int" / "int").
|
|
224
|
+
if (nameCount === 0) {
|
|
225
|
+
nameCount = 1;
|
|
226
|
+
}
|
|
227
|
+
const typeText = nodeText(typeNode);
|
|
228
|
+
for (let k = 0; k < nameCount; k++) {
|
|
229
|
+
types.push(typeText);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else if (pd.type === "variadic_parameter_declaration") {
|
|
233
|
+
// xs ...string: the "type" field is the element type node. The effective
|
|
234
|
+
// Go type is []elementType (what a doc author writes).
|
|
235
|
+
const tn = pd.childForFieldName("type");
|
|
236
|
+
if (tn !== null) {
|
|
237
|
+
types.push("[]" + nodeText(tn));
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
return types;
|
|
242
|
+
}
|