homegraph 1.6.1 → 1.6.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.
- package/CHANGELOG.md +22 -0
- package/dist/bin/homegraph.js +2 -2
- package/dist/db/index.js +1 -0
- package/dist/db/migrations.d.ts +6 -1
- package/dist/db/migrations.js +26 -1
- package/dist/db/queries.d.ts +5 -0
- package/dist/db/queries.js +17 -7
- package/dist/db/schema.sql +4 -0
- package/dist/extraction/grammars.js +2 -1
- package/dist/extraction/languages/c-cpp.d.ts +8 -0
- package/dist/extraction/languages/c-cpp.js +44 -0
- package/dist/extraction/wasm-runtime-flags.d.ts +4 -0
- package/dist/extraction/wasm-runtime-flags.js +9 -0
- package/dist/index.js +7 -1
- package/dist/installer/targets/icode.d.ts +43 -0
- package/dist/installer/targets/icode.js +631 -0
- package/dist/installer/targets/registry.js +2 -0
- package/dist/installer/targets/types.d.ts +1 -1
- package/dist/mcp/evidence-rendering.js +1 -1
- package/dist/mcp/explore-repeat-guard.d.ts +19 -5
- package/dist/mcp/explore-repeat-guard.js +35 -14
- package/dist/mcp/explore-session-state.d.ts +5 -0
- package/dist/mcp/explore-session-state.js +1 -0
- package/dist/mcp/locate-contract.js +1 -1
- package/dist/mcp/server-instructions.d.ts +2 -2
- package/dist/mcp/server-instructions.js +10 -10
- package/dist/mcp/session.js +3 -2
- package/dist/mcp/tools.d.ts +21 -2
- package/dist/mcp/tools.js +109 -50
- package/dist/resolution/c-feature-variants.d.ts +40 -0
- package/dist/resolution/c-feature-variants.js +272 -0
- package/dist/resolution/cpp-macro-visibility.d.ts +21 -0
- package/dist/resolution/cpp-macro-visibility.js +393 -0
- package/dist/resolution/index.js +11 -0
- package/dist/resolution/name-matcher.js +49 -2
- package/dist/resolution/types.d.ts +8 -0
- package/dist/resolution/types.js +8 -0
- package/dist/sync/watcher.d.ts +30 -0
- package/dist/sync/watcher.js +45 -0
- package/package.json +1 -1
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.clearCFeatureVariantMemos = clearCFeatureVariantMemos;
|
|
4
|
+
exports.isCFeatureVariant = isCFeatureVariant;
|
|
5
|
+
exports.isOffByDefault = isOffByDefault;
|
|
6
|
+
const cpp_macro_visibility_1 = require("./cpp-macro-visibility");
|
|
7
|
+
const MEMO = new WeakMap();
|
|
8
|
+
function clearCFeatureVariantMemos(context) {
|
|
9
|
+
MEMO.delete(context);
|
|
10
|
+
}
|
|
11
|
+
function memoFor(context) {
|
|
12
|
+
let memo = MEMO.get(context);
|
|
13
|
+
if (!memo) {
|
|
14
|
+
memo = { files: new Map(), buildText: new Map(), components: new Map(), cFiles: null, defines: new Map(), named: new Map(), answers: new Map() };
|
|
15
|
+
MEMO.set(context, memo);
|
|
16
|
+
}
|
|
17
|
+
return memo;
|
|
18
|
+
}
|
|
19
|
+
const and = (a, b) => (a === false || b === false ? false : a === true && b === true ? true : undefined);
|
|
20
|
+
const or = (a, b) => (a === true || b === true ? true : a === false && b === false ? false : undefined);
|
|
21
|
+
const not = (a) => (a === undefined ? undefined : !a);
|
|
22
|
+
const C_FAMILY_FILE = /\.(c|h|cc|cpp|cxx|c\+\+|hpp|hh|hxx|h\+\+|inl|ipp|tcc|inc|m|mm)$/i;
|
|
23
|
+
const PLATFORM_FLAGS = new Set(['WIN32', 'WIN64', 'linux', 'unix']);
|
|
24
|
+
const BUILD_FILES = [
|
|
25
|
+
'Makefile', 'makefile', 'GNUmakefile', 'Makefile.am', 'Makefile.in', 'CMakeLists.txt', 'meson.build',
|
|
26
|
+
'meson_options.txt', 'configure.ac', 'configure.in', 'config.h.in', 'BUILD', 'BUILD.bazel', 'SConstruct', 'SConscript',
|
|
27
|
+
];
|
|
28
|
+
const dirOf = (file) => file.slice(0, Math.max(0, file.lastIndexOf('/')));
|
|
29
|
+
/** `a/b/c.h` → `a/b`, `a`, `` (the project root). */
|
|
30
|
+
function ancestors(file) {
|
|
31
|
+
const dirs = [];
|
|
32
|
+
for (let dir = dirOf(file);; dir = dirOf(dir)) {
|
|
33
|
+
dirs.push(dir);
|
|
34
|
+
if (dir === '')
|
|
35
|
+
return dirs;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Is `fn`'s definition compiled only by a build that sets a flag nothing in
|
|
40
|
+
* the project sets, for the call at `ref`? A branch around both the
|
|
41
|
+
* definition and the call, in the call's own file, decides nothing.
|
|
42
|
+
*/
|
|
43
|
+
function isCFeatureVariant(fn, ref, context) {
|
|
44
|
+
const memo = memoFor(context);
|
|
45
|
+
const sameFile = fn.filePath === ref.filePath;
|
|
46
|
+
const key = `v\0${fn.id}\0${sameFile ? ref.line : dirOf(ref.filePath)}`;
|
|
47
|
+
const hit = memo.answers.get(key);
|
|
48
|
+
if (hit !== undefined)
|
|
49
|
+
return hit;
|
|
50
|
+
const file = directivesOf(fn.filePath, context, memo);
|
|
51
|
+
let groups = enclosingTests(file.conditionals, fn.startLine);
|
|
52
|
+
if (sameFile) {
|
|
53
|
+
const around = enclosingTests(file.conditionals, ref.line);
|
|
54
|
+
let shared = 0;
|
|
55
|
+
while (shared < groups.length && shared < around.length &&
|
|
56
|
+
groups[shared][0] === around[shared][0] && groups[shared].length === around[shared].length)
|
|
57
|
+
shared++;
|
|
58
|
+
groups = groups.slice(shared);
|
|
59
|
+
}
|
|
60
|
+
const off = groups.length > 0 && isOff(groups, file, [fn.filePath, ref.filePath], context, memo);
|
|
61
|
+
memo.answers.set(key, off);
|
|
62
|
+
return off;
|
|
63
|
+
}
|
|
64
|
+
/** Is line `line` of `path` compiled only by a build that sets a flag nothing in the project sets? */
|
|
65
|
+
function isOffByDefault(path, line, context) {
|
|
66
|
+
const memo = memoFor(context);
|
|
67
|
+
const key = `o\0${path}\0${line}`;
|
|
68
|
+
const hit = memo.answers.get(key);
|
|
69
|
+
if (hit !== undefined)
|
|
70
|
+
return hit;
|
|
71
|
+
const file = directivesOf(path, context, memo);
|
|
72
|
+
const groups = enclosingTests(file.conditionals, line);
|
|
73
|
+
const off = groups.length > 0 && isOff(groups, file, [path], context, memo);
|
|
74
|
+
memo.answers.set(key, off);
|
|
75
|
+
return off;
|
|
76
|
+
}
|
|
77
|
+
/** Is a branch of `groups` off when no flag `scope`'s files see is set? */
|
|
78
|
+
function isOff(groups, file, scope, context, memo) {
|
|
79
|
+
const dirs = [...new Set(scope.flatMap(ancestors))];
|
|
80
|
+
const flag = (name) => {
|
|
81
|
+
if (name.startsWith('_') || PLATFORM_FLAGS.has(name) || file.defines.has(name))
|
|
82
|
+
return undefined;
|
|
83
|
+
let named = memo.named.get(name);
|
|
84
|
+
// A word of its own, or a compiler flag's `-DX` / `/DX`.
|
|
85
|
+
if (!named)
|
|
86
|
+
memo.named.set(name, (named = new RegExp(`(?:^|[^\\w]|[-/]D)${name}\\b`, 'm')));
|
|
87
|
+
if (dirs.some((dir) => named.test(buildTextOf(dir, context, memo))))
|
|
88
|
+
return undefined;
|
|
89
|
+
const components = new Set(scope.map((f) => componentOf(dirOf(f), context, memo)));
|
|
90
|
+
return [...components].some((component) => isDefinedIn(name, component, context, memo)) ? undefined : false;
|
|
91
|
+
};
|
|
92
|
+
return groups.some((tests) => branchTruth(tests, flag) === false);
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* The branches around `line`, innermost last: for each, the tests of its
|
|
96
|
+
* group's branches up to and including the one `line` is in (`#else` has no
|
|
97
|
+
* test).
|
|
98
|
+
*/
|
|
99
|
+
function enclosingTests(conditionals, line) {
|
|
100
|
+
const groups = [];
|
|
101
|
+
for (const d of conditionals) {
|
|
102
|
+
if (d.line >= line)
|
|
103
|
+
break;
|
|
104
|
+
if (d.op === 'if' || d.op === 'ifdef' || d.op === 'ifndef')
|
|
105
|
+
groups.push([d]);
|
|
106
|
+
else if (d.op === 'endif')
|
|
107
|
+
groups.pop();
|
|
108
|
+
else
|
|
109
|
+
groups[groups.length - 1]?.push(d);
|
|
110
|
+
}
|
|
111
|
+
return groups;
|
|
112
|
+
}
|
|
113
|
+
/** Whether the last branch of `tests` is taken: no earlier test held, and its own does. */
|
|
114
|
+
function branchTruth(tests, flag) {
|
|
115
|
+
let earlier = false;
|
|
116
|
+
for (let i = 0; i < tests.length - 1; i++) {
|
|
117
|
+
earlier = or(earlier, testTruth(tests[i], flag));
|
|
118
|
+
if (earlier === true)
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
return and(not(earlier), testTruth(tests[tests.length - 1], flag));
|
|
122
|
+
}
|
|
123
|
+
function testTruth(d, flag) {
|
|
124
|
+
const name = d.expression.trim().match(/^\w+$/)?.[0];
|
|
125
|
+
switch (d.op) {
|
|
126
|
+
case 'ifdef':
|
|
127
|
+
case 'elifdef':
|
|
128
|
+
return name ? flag(name) : undefined;
|
|
129
|
+
case 'ifndef':
|
|
130
|
+
case 'elifndef':
|
|
131
|
+
return name ? not(flag(name)) : undefined;
|
|
132
|
+
case 'else':
|
|
133
|
+
return true;
|
|
134
|
+
default:
|
|
135
|
+
return evaluate(d.expression, flag);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* A `#if` expression of `defined`, flags, integers, `!`, `&&`, `||` and
|
|
140
|
+
* parentheses; anything else is unknown. A flag that is set nowhere reads as
|
|
141
|
+
* `0`, as the preprocessor reads a name no `#define` gave a value.
|
|
142
|
+
*/
|
|
143
|
+
function evaluate(expression, flag) {
|
|
144
|
+
const tokens = expression.match(/0x[\da-f]+\w*|\d\w*|[A-Za-z_]\w*|&&|\|\||\S/gi) ?? [];
|
|
145
|
+
// Check the shape first, so an unsupported test reads no flag.
|
|
146
|
+
let i = 0;
|
|
147
|
+
const shape = () => {
|
|
148
|
+
const term = () => {
|
|
149
|
+
const t = tokens[i++];
|
|
150
|
+
if (t === '!')
|
|
151
|
+
return term();
|
|
152
|
+
if (t === '(')
|
|
153
|
+
return shape() && tokens[i++] === ')';
|
|
154
|
+
if (t === 'defined') {
|
|
155
|
+
const paren = tokens[i] === '(';
|
|
156
|
+
if (paren)
|
|
157
|
+
i++;
|
|
158
|
+
return /^[A-Za-z_]\w*$/.test(tokens[i++] ?? '') && (!paren || tokens[i++] === ')');
|
|
159
|
+
}
|
|
160
|
+
return t !== undefined && (/^(0x[\da-f]+|\d+)[ul]*$/i.test(t) || /^[A-Za-z_]\w*$/.test(t));
|
|
161
|
+
};
|
|
162
|
+
if (!term())
|
|
163
|
+
return false;
|
|
164
|
+
while (tokens[i] === '&&' || tokens[i] === '||') {
|
|
165
|
+
i++;
|
|
166
|
+
if (!term())
|
|
167
|
+
return false;
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
};
|
|
171
|
+
if (tokens.length === 0 || !shape() || i !== tokens.length)
|
|
172
|
+
return undefined;
|
|
173
|
+
i = 0;
|
|
174
|
+
const disjunction = () => {
|
|
175
|
+
let value = conjunction();
|
|
176
|
+
while (tokens[i] === '||') {
|
|
177
|
+
i++;
|
|
178
|
+
value = or(value, conjunction());
|
|
179
|
+
}
|
|
180
|
+
return value;
|
|
181
|
+
};
|
|
182
|
+
const conjunction = () => {
|
|
183
|
+
let value = term();
|
|
184
|
+
while (tokens[i] === '&&') {
|
|
185
|
+
i++;
|
|
186
|
+
value = and(value, term());
|
|
187
|
+
}
|
|
188
|
+
return value;
|
|
189
|
+
};
|
|
190
|
+
const term = () => {
|
|
191
|
+
const t = tokens[i++];
|
|
192
|
+
if (t === '!')
|
|
193
|
+
return not(term());
|
|
194
|
+
if (t === '(') {
|
|
195
|
+
const value = disjunction();
|
|
196
|
+
i++;
|
|
197
|
+
return value;
|
|
198
|
+
}
|
|
199
|
+
if (t === 'defined') {
|
|
200
|
+
if (tokens[i] === '(') {
|
|
201
|
+
const value = flag(tokens[i + 1]);
|
|
202
|
+
i += 3;
|
|
203
|
+
return value;
|
|
204
|
+
}
|
|
205
|
+
return flag(tokens[i++]);
|
|
206
|
+
}
|
|
207
|
+
if (/^\d|^0x/i.test(t))
|
|
208
|
+
return Number.parseInt(t.replace(/[ul]+$/i, ''), /^0x/i.test(t) ? 16 : 10) !== 0;
|
|
209
|
+
return flag(t) === false ? false : undefined;
|
|
210
|
+
};
|
|
211
|
+
return disjunction();
|
|
212
|
+
}
|
|
213
|
+
function directivesOf(file, context, memo) {
|
|
214
|
+
const hit = memo.files.get(file);
|
|
215
|
+
if (hit)
|
|
216
|
+
return hit;
|
|
217
|
+
const lines = (0, cpp_macro_visibility_1.directiveLines)(context.readFile(file) ?? '');
|
|
218
|
+
const conditionals = [];
|
|
219
|
+
const defines = new Set();
|
|
220
|
+
for (let i = 0; i < lines.length; i++) {
|
|
221
|
+
const define = lines[i].match(/^\s*#\s*define\s+(\w+)/);
|
|
222
|
+
if (define) {
|
|
223
|
+
defines.add(define[1]);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
const m = lines[i].match(/^\s*#\s*(ifdef|ifndef|if|elifdef|elifndef|elif|else|endif)\b(.*)$/);
|
|
227
|
+
if (!m)
|
|
228
|
+
continue;
|
|
229
|
+
// A test continued with a backslash goes on over the next lines.
|
|
230
|
+
let expression = m[2];
|
|
231
|
+
for (let j = i; /\\\s*$/.test(lines[j]) && j + 1 < lines.length; j++) {
|
|
232
|
+
expression = expression.replace(/\\\s*$/, ' ') + lines[j + 1];
|
|
233
|
+
}
|
|
234
|
+
conditionals.push({ op: m[1], expression, line: i + 1 });
|
|
235
|
+
}
|
|
236
|
+
const directives = { conditionals, defines };
|
|
237
|
+
memo.files.set(file, directives);
|
|
238
|
+
return directives;
|
|
239
|
+
}
|
|
240
|
+
function buildTextOf(dir, context, memo) {
|
|
241
|
+
const hit = memo.buildText.get(dir);
|
|
242
|
+
if (hit !== undefined)
|
|
243
|
+
return hit;
|
|
244
|
+
const text = BUILD_FILES.map((name) => context.readFile(dir ? `${dir}/${name}` : name) ?? '').join('\n');
|
|
245
|
+
memo.buildText.set(dir, text);
|
|
246
|
+
return text;
|
|
247
|
+
}
|
|
248
|
+
/** The nearest directory at or above `dir` with a build file, else the project root. */
|
|
249
|
+
function componentOf(dir, context, memo) {
|
|
250
|
+
const hit = memo.components.get(dir);
|
|
251
|
+
if (hit !== undefined)
|
|
252
|
+
return hit;
|
|
253
|
+
const component = dir === '' || buildTextOf(dir, context, memo).trim() ? dir : componentOf(dirOf(dir), context, memo);
|
|
254
|
+
memo.components.set(dir, component);
|
|
255
|
+
return component;
|
|
256
|
+
}
|
|
257
|
+
/** Does a C or C++ file under `component` `#define` `name`? Read only from files whose bytes hold the name. */
|
|
258
|
+
function isDefinedIn(name, component, context, memo) {
|
|
259
|
+
const key = `${name}\0${component}`;
|
|
260
|
+
const hit = memo.defines.get(key);
|
|
261
|
+
if (hit !== undefined)
|
|
262
|
+
return hit;
|
|
263
|
+
memo.cFiles ??= context.getAllFiles().filter((file) => C_FAMILY_FILE.test(file));
|
|
264
|
+
const prefix = component ? `${component}/` : '';
|
|
265
|
+
const define = new RegExp(`^[ \\t]*#[ \\t]*define[ \\t]+${name}\\b`, 'm');
|
|
266
|
+
const defined = memo.cFiles.some((file) => file.startsWith(prefix) &&
|
|
267
|
+
(context.fileContains ? context.fileContains(file, name) : true) &&
|
|
268
|
+
define.test(context.readFile(file) ?? ''));
|
|
269
|
+
memo.defines.set(key, defined);
|
|
270
|
+
return defined;
|
|
271
|
+
}
|
|
272
|
+
//# sourceMappingURL=c-feature-variants.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { type ResolutionContext, type UnresolvedRef } from './types';
|
|
2
|
+
export declare function clearCppMacroVisibility(context: ResolutionContext): void;
|
|
3
|
+
/**
|
|
4
|
+
* Is this C/C++ `calls` ref a macro expansion rather than a call? True when
|
|
5
|
+
* the index knows the name as a function-like macro (extraction mints a
|
|
6
|
+
* `constant` per `preproc_function_def`) and either nothing but macros bears
|
|
7
|
+
* the name — there is no function to call, and a case-insensitive fuzzy
|
|
8
|
+
* match (`SWAP` → `swap`) must not invent one — or the macro is definitely
|
|
9
|
+
* visible at the call site.
|
|
10
|
+
*/
|
|
11
|
+
export declare function isVisibleCppMacro(ref: UnresolvedRef, context: ResolutionContext): boolean;
|
|
12
|
+
/**
|
|
13
|
+
* The file's lines with comments removed — only as far as the preprocessor
|
|
14
|
+
* needs: a line inside a block comment is blank, a directive line loses its
|
|
15
|
+
* trailing `//` / `/* … *\/`, and every other line is kept verbatim (its
|
|
16
|
+
* content is never read, only whether a block comment opens on it). The
|
|
17
|
+
* generic comment stripper is a regex pass over the whole file and this walk
|
|
18
|
+
* touches every header of a translation unit, vendor trees included.
|
|
19
|
+
*/
|
|
20
|
+
export declare function directiveLines(source: string): string[];
|
|
21
|
+
//# sourceMappingURL=cpp-macro-visibility.d.ts.map
|
|
@@ -0,0 +1,393 @@
|
|
|
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
|
+
exports.clearCppMacroVisibility = clearCppMacroVisibility;
|
|
37
|
+
exports.isVisibleCppMacro = isVisibleCppMacro;
|
|
38
|
+
exports.directiveLines = directiveLines;
|
|
39
|
+
/**
|
|
40
|
+
* Is a C/C++ call's name a function-like macro visible at the call site?
|
|
41
|
+
*
|
|
42
|
+
* `TRACE_POINT(1)` parses as a call_expression, so extraction records a
|
|
43
|
+
* `calls` ref named `TRACE_POINT`. If the translation unit defines
|
|
44
|
+
* `#define TRACE_POINT(value) ...` (in the file itself or through an
|
|
45
|
+
* include), the "call" is a macro expansion — and binding it to a
|
|
46
|
+
* same-spelled free function in an unrelated file fabricates a caller and a
|
|
47
|
+
* callee that never existed (#1838). The resolver asks this before trying
|
|
48
|
+
* any strategy and drops such a ref.
|
|
49
|
+
*
|
|
50
|
+
* Directive summaries are cached per file, but evaluated in translation-unit
|
|
51
|
+
* order on every inclusion. Only #pragma once and actual guard state suppress
|
|
52
|
+
* reinclusion; an active recursion stack breaks include cycles. Unknown build
|
|
53
|
+
* flags remain possible, so only definite macro visibility suppresses a call.
|
|
54
|
+
* Object-like definitions participate in conditions but never enter the cached
|
|
55
|
+
* call-site timelines (vendor headers can contain tens of thousands of them).
|
|
56
|
+
*
|
|
57
|
+
* A guard the walk cannot decide (`#define X_H 1`, or any header reached under
|
|
58
|
+
* an unknown `#if`) re-enters its header on every inclusion path, which is
|
|
59
|
+
* exponential in the include graph's depth (#2127). A re-entry that provably
|
|
60
|
+
* repeats an earlier visit which changed nothing is skipped, and a hard budget
|
|
61
|
+
* bounds whatever is left: past it, nothing is known and nothing is suppressed.
|
|
62
|
+
*/
|
|
63
|
+
const path = __importStar(require("path"));
|
|
64
|
+
const c_cpp_1 = require("../extraction/languages/c-cpp");
|
|
65
|
+
const types_1 = require("./types");
|
|
66
|
+
const import_resolver_1 = require("./import-resolver");
|
|
67
|
+
const memo = new WeakMap();
|
|
68
|
+
const ROOT_TIMELINE_CAP = 32;
|
|
69
|
+
/** Directive events one translation-unit walk may evaluate (#2127). */
|
|
70
|
+
const WALK_EVENT_BUDGET = 1_000_000;
|
|
71
|
+
const NO_CUTS = [];
|
|
72
|
+
const and = (a, b) => a === false || b === false ? false : a === true && b === true ? true : undefined;
|
|
73
|
+
const or = (a, b) => a === true || b === true ? true : a === false && b === false ? false : undefined;
|
|
74
|
+
const not = (a) => (a === undefined ? undefined : !a);
|
|
75
|
+
function clearCppMacroVisibility(context) {
|
|
76
|
+
memo.delete(context);
|
|
77
|
+
}
|
|
78
|
+
const isDefine = (n) => n.kind === 'constant' && types_1.CPP_DEFINE_SIGNATURE.test(n.signature ?? '');
|
|
79
|
+
/**
|
|
80
|
+
* Is this C/C++ `calls` ref a macro expansion rather than a call? True when
|
|
81
|
+
* the index knows the name as a function-like macro (extraction mints a
|
|
82
|
+
* `constant` per `preproc_function_def`) and either nothing but macros bears
|
|
83
|
+
* the name — there is no function to call, and a case-insensitive fuzzy
|
|
84
|
+
* match (`SWAP` → `swap`) must not invent one — or the macro is definitely
|
|
85
|
+
* visible at the call site.
|
|
86
|
+
*/
|
|
87
|
+
function isVisibleCppMacro(ref, context) {
|
|
88
|
+
if (ref.language !== 'c' && ref.language !== 'cpp')
|
|
89
|
+
return false;
|
|
90
|
+
if (ref.referenceKind !== 'calls' || !/^\w+$/.test(ref.referenceName))
|
|
91
|
+
return false;
|
|
92
|
+
// The name cache is the index's truth, invalidated with the resolver's other
|
|
93
|
+
// caches; a per-context name set would go stale between indexing batches.
|
|
94
|
+
const sameName = context.getNodesByName(ref.referenceName);
|
|
95
|
+
if (!sameName.some(isDefine))
|
|
96
|
+
return false;
|
|
97
|
+
if (!sameName.some((n) => !isDefine(n)))
|
|
98
|
+
return true;
|
|
99
|
+
const cache = cacheFor(context);
|
|
100
|
+
const rootKey = `${ref.language}\0${ref.filePath}`;
|
|
101
|
+
let timeline = cache.roots.get(rootKey);
|
|
102
|
+
if (!timeline) {
|
|
103
|
+
timeline = walkTranslationUnit(ref.filePath, ref.language, context, cache);
|
|
104
|
+
if (cache.roots.size >= ROOT_TIMELINE_CAP)
|
|
105
|
+
cache.roots.delete(cache.roots.keys().next().value);
|
|
106
|
+
cache.roots.set(rootKey, timeline);
|
|
107
|
+
}
|
|
108
|
+
if (ref.line >= timeline.cutoff)
|
|
109
|
+
return false;
|
|
110
|
+
const before = (timeline.events.get(ref.referenceName) ?? []).filter((e) => e.line <= ref.line);
|
|
111
|
+
return before.length > 0 && before[before.length - 1].defined === true;
|
|
112
|
+
}
|
|
113
|
+
function cacheFor(context) {
|
|
114
|
+
let cache = memo.get(context);
|
|
115
|
+
if (!cache) {
|
|
116
|
+
cache = { summaries: new Map(), includes: new Map(), byBasename: null, roots: new Map() };
|
|
117
|
+
memo.set(context, cache);
|
|
118
|
+
}
|
|
119
|
+
return cache;
|
|
120
|
+
}
|
|
121
|
+
/** Cache syntax, never conditional truth: an included file can change its flags. */
|
|
122
|
+
function summarize(file, context, cache) {
|
|
123
|
+
const cached = cache.summaries.get(file);
|
|
124
|
+
if (cached)
|
|
125
|
+
return cached;
|
|
126
|
+
const events = [];
|
|
127
|
+
const lines = directiveLines(context.readFile(file) ?? '');
|
|
128
|
+
for (let i = 0; i < lines.length; i++) {
|
|
129
|
+
const text = lines[i];
|
|
130
|
+
const branch = text.match(/^\s*#\s*(ifdef|ifndef|if|elif|else|endif)\b(.*)$/);
|
|
131
|
+
if (branch) {
|
|
132
|
+
events.push({ kind: 'branch', op: branch[1], expression: branch[2],
|
|
133
|
+
guard: guardsItself(lines, i, branch[1], branch[2]), line: i + 1 });
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
const directive = text.match(/^\s*#\s*(define|undef)\s+(\w+)(\(?)/);
|
|
137
|
+
if (directive) {
|
|
138
|
+
events.push({ kind: directive[1], name: directive[2], line: i + 1,
|
|
139
|
+
functionLike: directive[3] === '(', value: text.slice(directive[0].length),
|
|
140
|
+
wrapsItself: directive[3] === '(' && callsItself(lines, i, directive[2]) });
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const include = text.match(/^\s*#\s*include\s*([<"])([^>"]+)[>"]/);
|
|
144
|
+
if (include)
|
|
145
|
+
events.push({ kind: 'include', quote: include[1], spec: include[2], line: i + 1 });
|
|
146
|
+
if (/^\s*#\s*pragma\s+once\b/.test(text))
|
|
147
|
+
events.push({ kind: 'once', line: i + 1 });
|
|
148
|
+
}
|
|
149
|
+
cache.summaries.set(file, events);
|
|
150
|
+
return events;
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* The file's lines with comments removed — only as far as the preprocessor
|
|
154
|
+
* needs: a line inside a block comment is blank, a directive line loses its
|
|
155
|
+
* trailing `//` / `/* … *\/`, and every other line is kept verbatim (its
|
|
156
|
+
* content is never read, only whether a block comment opens on it). The
|
|
157
|
+
* generic comment stripper is a regex pass over the whole file and this walk
|
|
158
|
+
* touches every header of a translation unit, vendor trees included.
|
|
159
|
+
*/
|
|
160
|
+
function directiveLines(source) {
|
|
161
|
+
const out = [];
|
|
162
|
+
let inBlock = false;
|
|
163
|
+
for (const raw of (0, c_cpp_1.maskCppRawStrings)(source).source.split(/\r?\n/)) {
|
|
164
|
+
let text = raw;
|
|
165
|
+
if (inBlock) {
|
|
166
|
+
const end = text.indexOf('*/');
|
|
167
|
+
if (end < 0) {
|
|
168
|
+
out.push('');
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
text = text.slice(end + 2);
|
|
172
|
+
inBlock = false;
|
|
173
|
+
}
|
|
174
|
+
const directive = /^\s*#/.test(text);
|
|
175
|
+
let kept = '';
|
|
176
|
+
let quote = '';
|
|
177
|
+
for (let i = 0; i < text.length; i++) {
|
|
178
|
+
const c = text[i];
|
|
179
|
+
if (quote) {
|
|
180
|
+
if (c === '\\')
|
|
181
|
+
i++;
|
|
182
|
+
else if (c === quote)
|
|
183
|
+
quote = '';
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (c === '"' || c === "'") {
|
|
187
|
+
quote = c;
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
if (c === '/' && text[i + 1] === '/') {
|
|
191
|
+
kept = text.slice(0, i);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
if (c === '/' && text[i + 1] === '*') {
|
|
195
|
+
const end = text.indexOf('*/', i + 2);
|
|
196
|
+
if (end < 0) {
|
|
197
|
+
inBlock = true;
|
|
198
|
+
kept = text.slice(0, i);
|
|
199
|
+
break;
|
|
200
|
+
}
|
|
201
|
+
text = text.slice(0, i) + ' ' + text.slice(end + 2);
|
|
202
|
+
i--;
|
|
203
|
+
continue;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
out.push(directive ? kept || text : raw);
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
|
210
|
+
/** Does the body of the `#define NAME(` at `index` (continuation lines included) call `NAME`? */
|
|
211
|
+
function callsItself(lines, index, name) {
|
|
212
|
+
let text = lines[index];
|
|
213
|
+
for (let j = index; /\\\s*$/.test(lines[j]) && j + 1 < lines.length; j++)
|
|
214
|
+
text += ' ' + lines[j + 1];
|
|
215
|
+
const body = text.slice(text.indexOf('(') + 1);
|
|
216
|
+
return new RegExp(`(?:\\b${name}\\s*\\(|\\(\\s*${name}\\s*\\)\\s*\\()`).test(body);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The include-guard idiom: `#ifndef X_H` (or `#if !defined(X_H)`) whose next
|
|
220
|
+
* directive is `#define X_H`. Nothing in the file defines the guard before
|
|
221
|
+
* the test, so this is the first inclusion and the guarded body is active.
|
|
222
|
+
* The same shape around a fallback function-like macro (`#ifndef MIN` /
|
|
223
|
+
* `#define MIN(a, b) …`) is read the same way. A default VALUE
|
|
224
|
+
* (`#ifndef ENABLE_X` / `#define ENABLE_X 0`) is not: that is the flag a
|
|
225
|
+
* build overrides on the command line, so it stays unknown.
|
|
226
|
+
*/
|
|
227
|
+
function guardsItself(lines, index, op, expression) {
|
|
228
|
+
const name = op === 'ifndef'
|
|
229
|
+
? expression.trim()
|
|
230
|
+
: expression.match(/^\s*!\s*defined\s*(?:\(\s*(\w+)\s*\)|(\w+))\s*$/)?.slice(1).find(Boolean);
|
|
231
|
+
if (!name || !/^\w+$/.test(name))
|
|
232
|
+
return false;
|
|
233
|
+
for (let j = index + 1; j < lines.length; j++) {
|
|
234
|
+
const text = lines[j];
|
|
235
|
+
if (!/^\s*#/.test(text))
|
|
236
|
+
continue;
|
|
237
|
+
return new RegExp(`^\\s*#\\s*define\\s+${name}(?:\\s*$|\\()`).test(text);
|
|
238
|
+
}
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
function resolveInclude(file, quote, spec, language, context, cache) {
|
|
242
|
+
const key = `${language}\0${file}\0${quote}${spec}`;
|
|
243
|
+
const cached = cache.includes.get(key);
|
|
244
|
+
if (cached !== undefined)
|
|
245
|
+
return cached;
|
|
246
|
+
const local = path.posix.normalize(path.posix.join(path.posix.dirname(file), spec.replace(/\\/g, '/')));
|
|
247
|
+
let target = quote === '"' && !local.startsWith('../') && !path.posix.isAbsolute(local) && context.fileExists(local)
|
|
248
|
+
? local
|
|
249
|
+
: (0, import_resolver_1.resolveImportPath)(spec, file, language, context);
|
|
250
|
+
if (!target) {
|
|
251
|
+
if (!cache.byBasename) {
|
|
252
|
+
cache.byBasename = new Map();
|
|
253
|
+
for (const f of context.getAllFiles()) {
|
|
254
|
+
const base = f.slice(f.lastIndexOf('/') + 1);
|
|
255
|
+
const list = cache.byBasename.get(base);
|
|
256
|
+
if (list)
|
|
257
|
+
list.push(f);
|
|
258
|
+
else
|
|
259
|
+
cache.byBasename.set(base, [f]);
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const normalized = spec.replace(/\\/g, '/');
|
|
263
|
+
const matches = (cache.byBasename.get(normalized.slice(normalized.lastIndexOf('/') + 1)) ?? []).filter((f) => f === normalized || f.endsWith('/' + normalized));
|
|
264
|
+
if (matches.length === 1)
|
|
265
|
+
target = matches[0];
|
|
266
|
+
}
|
|
267
|
+
cache.includes.set(key, target);
|
|
268
|
+
return target;
|
|
269
|
+
}
|
|
270
|
+
function walkTranslationUnit(rootFile, language, context, cache) {
|
|
271
|
+
const timeline = new Map();
|
|
272
|
+
const definitions = new Map();
|
|
273
|
+
const scanning = new Set();
|
|
274
|
+
const macroNames = new Set();
|
|
275
|
+
const once = new Map();
|
|
276
|
+
// Every change a later directive could observe (definitions, `#pragma once`,
|
|
277
|
+
// the macro-name set) bumps `version`. A visit that left it unchanged, entered
|
|
278
|
+
// again with the same inherited truth while it is still unchanged, starts from
|
|
279
|
+
// the same state, so it would take the same branches and change nothing again;
|
|
280
|
+
// the events it would push repeat each name's current state, which is already
|
|
281
|
+
// its last event. Include cycles cut by the recursion stack (`cuts`) must still
|
|
282
|
+
// be cut for the repeat to hold.
|
|
283
|
+
let version = 0;
|
|
284
|
+
const cuts = [];
|
|
285
|
+
const visits = new Map();
|
|
286
|
+
let budget = WALK_EVENT_BUDGET;
|
|
287
|
+
let cutoff = Infinity;
|
|
288
|
+
const condition = (expression) => {
|
|
289
|
+
const text = expression.trim();
|
|
290
|
+
if (/^(?:0x[\da-f]+|\d+)[uUlL]*$/i.test(text))
|
|
291
|
+
return Number(text.replace(/[uUlL]+$/, '')) !== 0;
|
|
292
|
+
const def = text.match(/^(!)?\s*defined\s*(?:\(\s*(\w+)\s*\)|(\w+))$/);
|
|
293
|
+
if (def) {
|
|
294
|
+
const known = definitions.get(def[2] ?? def[3])?.defined;
|
|
295
|
+
return def[1] ? not(known) : known;
|
|
296
|
+
}
|
|
297
|
+
return /^\w+$/.test(text) ? definitions.get(text)?.value : undefined;
|
|
298
|
+
};
|
|
299
|
+
const scan = (file, inherited, includeLine) => {
|
|
300
|
+
if (inherited === false || once.get(file) === true || cutoff !== Infinity)
|
|
301
|
+
return;
|
|
302
|
+
if (scanning.has(file)) {
|
|
303
|
+
cuts.push(file);
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
const seen = visits.get(file);
|
|
307
|
+
if (seen && seen.inherited === inherited && seen.start === seen.end && seen.end === version &&
|
|
308
|
+
seen.cuts.every((c) => scanning.has(c))) {
|
|
309
|
+
cuts.push(...seen.cuts);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
const start = version;
|
|
313
|
+
const firstCut = cuts.length;
|
|
314
|
+
scanning.add(file);
|
|
315
|
+
let active = inherited;
|
|
316
|
+
const frames = [];
|
|
317
|
+
for (const ev of summarize(file, context, cache)) {
|
|
318
|
+
const line = includeLine ?? ev.line;
|
|
319
|
+
if (cutoff === Infinity && --budget < 0)
|
|
320
|
+
cutoff = line;
|
|
321
|
+
if (cutoff !== Infinity)
|
|
322
|
+
break;
|
|
323
|
+
if (ev.kind === 'branch') {
|
|
324
|
+
if (ev.op === 'if' || ev.op === 'ifdef' || ev.op === 'ifndef') {
|
|
325
|
+
const known = definitions.get(ev.expression.trim())?.defined;
|
|
326
|
+
let selected = ev.op === 'if' ? condition(ev.expression) : ev.op === 'ifndef' ? not(known) : known;
|
|
327
|
+
if (selected === undefined && ev.guard)
|
|
328
|
+
selected = true;
|
|
329
|
+
frames.push({ parent: active, taken: selected });
|
|
330
|
+
active = and(active, selected);
|
|
331
|
+
}
|
|
332
|
+
else if (ev.op === 'endif') {
|
|
333
|
+
active = frames.pop()?.parent ?? inherited;
|
|
334
|
+
}
|
|
335
|
+
else {
|
|
336
|
+
const frame = frames[frames.length - 1];
|
|
337
|
+
if (frame) {
|
|
338
|
+
const test = ev.op === 'else' ? true : condition(ev.expression);
|
|
339
|
+
active = and(frame.parent, and(not(frame.taken), test));
|
|
340
|
+
frame.taken = or(frame.taken, test);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
continue;
|
|
344
|
+
}
|
|
345
|
+
if (active === false)
|
|
346
|
+
continue;
|
|
347
|
+
if (ev.kind === 'once') {
|
|
348
|
+
const next = or(once.get(file) ?? false, active);
|
|
349
|
+
if (next !== (once.get(file) ?? false))
|
|
350
|
+
version++;
|
|
351
|
+
once.set(file, next);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
if (ev.kind === 'include') {
|
|
355
|
+
const target = resolveInclude(file, ev.quote, ev.spec, language, context, cache);
|
|
356
|
+
if (target)
|
|
357
|
+
scan(target, active, line);
|
|
358
|
+
continue;
|
|
359
|
+
}
|
|
360
|
+
const prior = definitions.get(ev.name);
|
|
361
|
+
const defining = ev.kind === 'define';
|
|
362
|
+
const macro = defining && ev.functionLike && !ev.wrapsItself;
|
|
363
|
+
const now = active === true ? macro : prior?.macro === macro ? macro : undefined;
|
|
364
|
+
// A name no directive has touched is unknown, not undefined: the build
|
|
365
|
+
// can set it on the command line. So an `#undef` under an undecidable
|
|
366
|
+
// `#if` leaves it unknown (#2069); only a certain one clears it.
|
|
367
|
+
const next = {
|
|
368
|
+
defined: defining ? or(prior?.defined, active) : and(prior?.defined, not(active)),
|
|
369
|
+
value: defining && active === true ? condition(ev.value) : undefined,
|
|
370
|
+
macro: now,
|
|
371
|
+
};
|
|
372
|
+
if (next.defined !== prior?.defined || next.value !== prior?.value || next.macro !== prior?.macro)
|
|
373
|
+
version++;
|
|
374
|
+
definitions.set(ev.name, next);
|
|
375
|
+
if (ev.functionLike && !macroNames.has(ev.name)) {
|
|
376
|
+
macroNames.add(ev.name);
|
|
377
|
+
version++;
|
|
378
|
+
}
|
|
379
|
+
if (macroNames.has(ev.name)) {
|
|
380
|
+
const events = timeline.get(ev.name) ?? [];
|
|
381
|
+
events.push({ line, defined: now });
|
|
382
|
+
timeline.set(ev.name, events);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
scanning.delete(file);
|
|
386
|
+
const own = cuts.length > firstCut ? [...new Set(cuts.splice(firstCut))] : NO_CUTS;
|
|
387
|
+
cuts.push(...own);
|
|
388
|
+
visits.set(file, { inherited, start, end: version, cuts: own });
|
|
389
|
+
};
|
|
390
|
+
scan(rootFile, true);
|
|
391
|
+
return { events: timeline, cutoff };
|
|
392
|
+
}
|
|
393
|
+
//# sourceMappingURL=cpp-macro-visibility.js.map
|