@davesheffer/hunch 1.41.0 → 1.41.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.
|
@@ -42,12 +42,19 @@ export interface LanguageSpec {
|
|
|
42
42
|
/** Ancestor shapes in which an ERROR node is a known limitation of THIS grammar
|
|
43
43
|
* rather than a real syntax error. parse.ts forgives an error only when some
|
|
44
44
|
* ancestor has type `node` AND that ancestor's own parent has type `parentIs` —
|
|
45
|
-
* the pair is what keeps the tolerance narrow.
|
|
46
|
-
*
|
|
45
|
+
* the pair is what keeps the tolerance narrow. `textPattern`, when present,
|
|
46
|
+
* must also match that ancestor's text. Omit for a language with no
|
|
47
|
+
* known grammar false positives; that spec then stays strictly fail-closed. */
|
|
47
48
|
toleratedErrorScopes?: ReadonlyArray<{
|
|
48
49
|
readonly node: string;
|
|
49
50
|
readonly parentIs: string;
|
|
51
|
+
readonly textPattern?: RegExp;
|
|
50
52
|
}>;
|
|
53
|
+
/** Optional same-length recovery used only when the original syntax tree is
|
|
54
|
+
* not parseable. A recovered tree may classify structure, but parse.ts reads
|
|
55
|
+
* every captured value from the original source at the tree's unchanged
|
|
56
|
+
* offsets. Keep replacements syntax-equivalent outside the grammar defect. */
|
|
57
|
+
parseErrorRecovery?: (source: string) => string;
|
|
51
58
|
/** Patterns whose presence anywhere in the source mean "this isn't actually
|
|
52
59
|
* {id} text yet — it's a template that renders to {id} later" (Go/Jinja/Helm
|
|
53
60
|
* delimiters in a .yaml file, e.g.). parse.ts still runs the real parse — a
|
|
@@ -81,6 +81,19 @@ const TSX = {
|
|
|
81
81
|
extensions: [".tsx", ".jsx"],
|
|
82
82
|
grammarKey: "tsx",
|
|
83
83
|
loadGrammar: () => loadNativeTreeSitter().tsx,
|
|
84
|
+
// tree-sitter-javascript rejects a bare ampersand in JSX text even though JSX
|
|
85
|
+
// accepts it (tree-sitter-javascript#366). The ERROR is a direct jsx_element
|
|
86
|
+
// child and its text begins at the ampersand. Require both facts plus text that
|
|
87
|
+
// cannot cross into a tag or expression; real JSX errors beside it remain fail-closed.
|
|
88
|
+
toleratedErrorScopes: [
|
|
89
|
+
...TS_SHARED.toleratedErrorScopes,
|
|
90
|
+
{ node: "ERROR", parentIs: "jsx_element", textPattern: /^&[^<>{}]*$/u },
|
|
91
|
+
],
|
|
92
|
+
// More than one bare ampersand can make the grammar collapse the entire TSX
|
|
93
|
+
// tree instead of emitting the narrow ERROR above. `|` is accepted as JSX
|
|
94
|
+
// text and has the same length; in JS/TS syntax it preserves ampersand
|
|
95
|
+
// operators' arity, so malformed expressions remain malformed on the retry.
|
|
96
|
+
parseErrorRecovery: (source) => source.replaceAll("&", "|"),
|
|
84
97
|
};
|
|
85
98
|
const PY_QUERY = `
|
|
86
99
|
(class_definition
|
package/dist/extractors/parse.js
CHANGED
|
@@ -56,11 +56,35 @@ export function parseSource(file, source, opts = {}) {
|
|
|
56
56
|
throw error;
|
|
57
57
|
return null;
|
|
58
58
|
}
|
|
59
|
+
let parseable = isParseable(tree.rootNode, spec);
|
|
60
|
+
let usingRecoveredTree = false;
|
|
61
|
+
if (!parseable && spec.parseErrorRecovery) {
|
|
62
|
+
const recoveredSource = spec.parseErrorRecovery(source);
|
|
63
|
+
// Offsets from the recovery tree are used against the original source below.
|
|
64
|
+
// Refuse a misconfigured recovery rather than corrupt captured names/text.
|
|
65
|
+
if (recoveredSource.length === source.length) {
|
|
66
|
+
try {
|
|
67
|
+
const recoveredTree = parser.parse(recoveredSource, undefined, { bufferSize: Math.max(32 * 1024, recoveredSource.length * 2 + 1024) });
|
|
68
|
+
if (isParseable(recoveredTree.rootNode, spec)) {
|
|
69
|
+
tree = recoveredTree;
|
|
70
|
+
parseable = true;
|
|
71
|
+
usingRecoveredTree = true;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
if (opts.throwOnParseError)
|
|
76
|
+
throw error;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
59
80
|
const symbols = [];
|
|
60
81
|
const imports = [];
|
|
61
82
|
const calls = [];
|
|
62
83
|
const relations = [];
|
|
63
84
|
let namespace = null;
|
|
85
|
+
const originalText = (node) => usingRecoveredTree
|
|
86
|
+
? source.slice(node.startIndex, node.endIndex)
|
|
87
|
+
: node.text;
|
|
64
88
|
// group captures by their enclosing @*.def via a quick pass: we record names
|
|
65
89
|
// keyed by the def node, then emit a symbol per def.
|
|
66
90
|
const pendingDefs = new Map();
|
|
@@ -82,30 +106,32 @@ export function parseSource(file, source, opts = {}) {
|
|
|
82
106
|
if (defNode) {
|
|
83
107
|
const existing = pendingDefs.get(defNode.id);
|
|
84
108
|
if (existing)
|
|
85
|
-
existing.name = node
|
|
109
|
+
existing.name = originalText(node);
|
|
86
110
|
else
|
|
87
|
-
pendingDefs.set(defNode.id, { kind: spec.defKindOf[spec.nameToDef[cname]], def: defNode, name: node
|
|
111
|
+
pendingDefs.set(defNode.id, { kind: spec.defKindOf[spec.nameToDef[cname]], def: defNode, name: originalText(node) });
|
|
88
112
|
}
|
|
89
113
|
if (cname === "namespace.name")
|
|
90
|
-
namespace = node
|
|
114
|
+
namespace = originalText(node);
|
|
91
115
|
}
|
|
92
116
|
else if (cname === "import.src") {
|
|
93
|
-
imports.push(node.
|
|
117
|
+
imports.push(originalText(node).replace(STR_QUOTES, ""));
|
|
94
118
|
}
|
|
95
119
|
else if (cname === "call.id") {
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
const text = originalText(node);
|
|
121
|
+
if (!spec.builtinFunctions?.has(text)) {
|
|
122
|
+
calls.push({ callee: text, atByte: node.startIndex, endByte: node.endIndex, member: false });
|
|
98
123
|
}
|
|
99
124
|
}
|
|
100
125
|
else if (cname === "call.member") {
|
|
101
126
|
// skip builtin method names to avoid false edges to similarly-named symbols
|
|
102
|
-
|
|
103
|
-
|
|
127
|
+
const text = originalText(node);
|
|
128
|
+
if (!spec.builtinMethods.has(text))
|
|
129
|
+
calls.push({ callee: text, atByte: node.startIndex, endByte: node.endIndex, member: true });
|
|
104
130
|
}
|
|
105
131
|
else if (spec.relationKindOf?.[cname]) {
|
|
106
132
|
const relation = spec.relationKindOf[cname];
|
|
107
133
|
relations.push({
|
|
108
|
-
target: node
|
|
134
|
+
target: originalText(node),
|
|
109
135
|
atByte: node.startIndex,
|
|
110
136
|
endByte: node.endIndex,
|
|
111
137
|
edgeType: relation.edgeType,
|
|
@@ -121,7 +147,7 @@ export function parseSource(file, source, opts = {}) {
|
|
|
121
147
|
symbols.push({
|
|
122
148
|
name: resolvedName, kind,
|
|
123
149
|
startByte: def.startIndex, endByte: def.endIndex, loc,
|
|
124
|
-
bodyText: def.
|
|
150
|
+
bodyText: originalText(def).slice(0, MAX_BODY_TEXT_CHARS),
|
|
125
151
|
});
|
|
126
152
|
}
|
|
127
153
|
// Every other successfully-parsed YAML file gets at least a file-root symbol
|
|
@@ -141,7 +167,7 @@ export function parseSource(file, source, opts = {}) {
|
|
|
141
167
|
});
|
|
142
168
|
}
|
|
143
169
|
symbols.sort((a, b) => a.startByte - b.startByte);
|
|
144
|
-
return { symbols, imports, calls, relations, namespace, parseable: templated ||
|
|
170
|
+
return { symbols, imports, calls, relations, namespace, parseable: templated || parseable };
|
|
145
171
|
}
|
|
146
172
|
/** True when every ERROR/MISSING node in the tree sits in an ancestor shape this
|
|
147
173
|
* language declares as a known grammar limitation (LanguageSpec.toleratedErrorScopes).
|
|
@@ -177,9 +203,11 @@ function isParseable(root, spec) {
|
|
|
177
203
|
return ok;
|
|
178
204
|
}
|
|
179
205
|
function inToleratedScope(node, scopes) {
|
|
180
|
-
for (let ancestor = node
|
|
206
|
+
for (let ancestor = node; ancestor; ancestor = ancestor.parent) {
|
|
181
207
|
for (const scope of scopes) {
|
|
182
|
-
if (ancestor.type === scope.node
|
|
208
|
+
if (ancestor.type === scope.node
|
|
209
|
+
&& ancestor.parent?.type === scope.parentIs
|
|
210
|
+
&& (!scope.textPattern || ancestor.text.search(scope.textPattern) !== -1))
|
|
183
211
|
return true;
|
|
184
212
|
}
|
|
185
213
|
}
|
|
@@ -170,8 +170,14 @@ export function mergeLedgers(base, ours, theirs) {
|
|
|
170
170
|
seen.set(k, e);
|
|
171
171
|
order.push(e);
|
|
172
172
|
} };
|
|
173
|
-
|
|
174
|
-
|
|
173
|
+
const ourIdentities = new Set(ours.events.map(eventIdentity));
|
|
174
|
+
const theirIdentities = new Set(theirs.events.map(eventIdentity));
|
|
175
|
+
// Base is a merge ancestor, not a source of history both clones have compacted away.
|
|
176
|
+
for (const e of base?.events ?? []) {
|
|
177
|
+
const key = eventIdentity(e);
|
|
178
|
+
if (ourIdentities.has(key) || theirIdentities.has(key) || (e.seq > ours.floor_seq && e.seq > theirs.floor_seq))
|
|
179
|
+
add(e);
|
|
180
|
+
}
|
|
175
181
|
for (const e of ours.events)
|
|
176
182
|
add(e);
|
|
177
183
|
for (const e of theirs.events)
|
|
@@ -180,7 +186,6 @@ export function mergeLedgers(base, ours, theirs) {
|
|
|
180
186
|
// side 1 iff theirs holds it and neither ours nor base does. Deciding by object identity
|
|
181
187
|
// (`includes`) would instead put anything ours compacted away — base's object, which `add`
|
|
182
188
|
// keeps — after its same-`at` siblings and silently reorder a batch both sides already had.
|
|
183
|
-
const ourIdentities = new Set(ours.events.map(eventIdentity));
|
|
184
189
|
const baseIdentities = new Set((base?.events ?? []).map(eventIdentity));
|
|
185
190
|
const theirsOnly = (e) => {
|
|
186
191
|
const k = eventIdentity(e);
|
package/package.json
CHANGED
package/server.json
CHANGED
|
@@ -7,13 +7,13 @@
|
|
|
7
7
|
"source": "github"
|
|
8
8
|
},
|
|
9
9
|
"websiteUrl": "https://www.hunchmemory.com",
|
|
10
|
-
"version": "1.41.
|
|
10
|
+
"version": "1.41.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "npm",
|
|
14
14
|
"registryBaseUrl": "https://registry.npmjs.org",
|
|
15
15
|
"identifier": "@davesheffer/hunch",
|
|
16
|
-
"version": "1.41.
|
|
16
|
+
"version": "1.41.2",
|
|
17
17
|
"runtimeHint": "npx",
|
|
18
18
|
"packageArguments": [
|
|
19
19
|
{
|