fallow-type-aware 3.33.1 → 3.34.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -0
- package/package.json +1 -1
- package/src/cli.mjs +19 -1
- package/src/generated-protocol.mjs +2 -0
- package/src/mutant-typecheck.mjs +177 -0
- package/src/protocol.mjs +109 -2
- package/src/semantic.mjs +1 -1
package/README.md
CHANGED
|
@@ -14,6 +14,9 @@ zero-based UTF-8 byte column, and optional owner. Results keep their semantic
|
|
|
14
14
|
assertion separate from `complete`, `partial`, or `unavailable` status. Evidence
|
|
15
15
|
and every operation-specific array are deterministic and bounded, with totals,
|
|
16
16
|
omissions, reason codes, actions, and truncation reported explicitly.
|
|
17
|
+
The request `evidence_limit` is at most 40. A `type-coupling` query can set its
|
|
18
|
+
own `evidence_limit` of at most 10,000, because its result is one report for
|
|
19
|
+
the whole project.
|
|
17
20
|
|
|
18
21
|
Structural diagnostics,
|
|
19
22
|
unknown identities, missing projects, unsupported syntax, dynamic behavior,
|
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -2,12 +2,19 @@ import path from "node:path";
|
|
|
2
2
|
import { createInterface } from "node:readline";
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
+
MUTANT_TYPECHECK_OPERATION,
|
|
5
6
|
SESSION_ENVELOPE_TYPES,
|
|
6
7
|
STATUS_OPERATION,
|
|
7
8
|
WIRE_PROTOCOL_VERSION,
|
|
8
9
|
} from "./generated-protocol.mjs";
|
|
9
10
|
import { analyzeSemanticQueries, createSemanticSession } from "./semantic.mjs";
|
|
10
|
-
import {
|
|
11
|
+
import { checkMutants } from "./mutant-typecheck.mjs";
|
|
12
|
+
import {
|
|
13
|
+
createMutantTypecheckResponse,
|
|
14
|
+
createSemanticResponse,
|
|
15
|
+
createStatusResponse,
|
|
16
|
+
parseRequest,
|
|
17
|
+
} from "./protocol.mjs";
|
|
11
18
|
|
|
12
19
|
const MAX_REQUEST_BYTES = 8 * 1024 * 1024;
|
|
13
20
|
const [ANALYZE_ENVELOPE, SHUTDOWN_ENVELOPE] = SESSION_ENVELOPE_TYPES;
|
|
@@ -91,6 +98,9 @@ const runSession = async (input, output) => {
|
|
|
91
98
|
throw new Error("invalid semantic session envelope");
|
|
92
99
|
}
|
|
93
100
|
const request = parseRequest(envelope.request);
|
|
101
|
+
if (request.operation === MUTANT_TYPECHECK_OPERATION) {
|
|
102
|
+
throw new Error("a semantic session does not accept a mutant-typecheck request");
|
|
103
|
+
}
|
|
94
104
|
if (!session) {
|
|
95
105
|
root = request.root;
|
|
96
106
|
session = createSemanticSession(root);
|
|
@@ -145,6 +155,14 @@ export const run = async ({ input, output, args = [] }) => {
|
|
|
145
155
|
return;
|
|
146
156
|
}
|
|
147
157
|
const request = parseRequest(rawRequest);
|
|
158
|
+
if (request.operation === MUTANT_TYPECHECK_OPERATION) {
|
|
159
|
+
const results = checkMutants(request);
|
|
160
|
+
writeJson(
|
|
161
|
+
output,
|
|
162
|
+
createMutantTypecheckResponse({ results, elapsedMs: performance.now() - startedAt }),
|
|
163
|
+
);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
148
166
|
const result = analyzeSemanticQueries(request);
|
|
149
167
|
writeJson(output, responseFor(result, performance.now() - startedAt));
|
|
150
168
|
};
|
|
@@ -5,6 +5,7 @@ export const TYPE_AWARE_PROTOCOL = Object.freeze({
|
|
|
5
5
|
semantic_schema_version: 3,
|
|
6
6
|
analysis_operation: "semantic-queries",
|
|
7
7
|
status_operation: "status",
|
|
8
|
+
mutant_typecheck_operation: "mutant-typecheck",
|
|
8
9
|
query_operations: ["symbol-use", "symbol-trace", "api-surface", "symbol-impact", "type-coupling"],
|
|
9
10
|
session_envelope_types: ["analyze", "shutdown"],
|
|
10
11
|
backend: {
|
|
@@ -20,6 +21,7 @@ export const WIRE_PROTOCOL_VERSION = TYPE_AWARE_PROTOCOL.wire_protocol_version;
|
|
|
20
21
|
export const SEMANTIC_SCHEMA_VERSION = TYPE_AWARE_PROTOCOL.semantic_schema_version;
|
|
21
22
|
export const ANALYSIS_OPERATION = TYPE_AWARE_PROTOCOL.analysis_operation;
|
|
22
23
|
export const STATUS_OPERATION = TYPE_AWARE_PROTOCOL.status_operation;
|
|
24
|
+
export const MUTANT_TYPECHECK_OPERATION = TYPE_AWARE_PROTOCOL.mutant_typecheck_operation;
|
|
23
25
|
export const QUERY_OPERATIONS = Object.freeze(TYPE_AWARE_PROTOCOL.query_operations);
|
|
24
26
|
export const SESSION_ENVELOPE_TYPES = Object.freeze(TYPE_AWARE_PROTOCOL.session_envelope_types);
|
|
25
27
|
export const BACKEND_FAMILY = TYPE_AWARE_PROTOCOL.backend.family;
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// Type-checks mutated source files for `fallow mutation --typecheck`.
|
|
2
|
+
//
|
|
3
|
+
// The check applies one mutant at a time to an in-memory copy of its file and
|
|
4
|
+
// compares the error diagnostics of that file with the diagnostics of the
|
|
5
|
+
// original file. A new error makes the mutant `compile-error`. The check reads
|
|
6
|
+
// only the mutated file, so a mutant that breaks only another file stays
|
|
7
|
+
// `compiles`. The project files on disk never change.
|
|
8
|
+
|
|
9
|
+
import { createHash } from "node:crypto";
|
|
10
|
+
import { readFileSync } from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
|
|
13
|
+
import { API, DiagnosticCategory } from "typescript/unstable/sync";
|
|
14
|
+
|
|
15
|
+
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
16
|
+
|
|
17
|
+
const JAVASCRIPT_EXTENSIONS = new Set([".js", ".jsx", ".mjs", ".cjs"]);
|
|
18
|
+
const LEADING_COMMENTS = /^(?:\s|\/\/[^\n]*|\/\*[\s\S]*?\*\/)*/u;
|
|
19
|
+
const NO_CHECK_PRAGMA = /@ts-nocheck\b/u;
|
|
20
|
+
const MAX_CODES = 8;
|
|
21
|
+
|
|
22
|
+
const sha256 = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
23
|
+
|
|
24
|
+
const isInferredProject = (project) =>
|
|
25
|
+
project.configFileName.split(path.sep).join("/").endsWith("/dev/null/inferred");
|
|
26
|
+
|
|
27
|
+
const hasNoCheckPragma = (text) => NO_CHECK_PRAGMA.test(LEADING_COMMENTS.exec(text)?.[0] ?? "");
|
|
28
|
+
|
|
29
|
+
const isChecked = (project, absolutePath, text) => {
|
|
30
|
+
if (hasNoCheckPragma(text)) return false;
|
|
31
|
+
if (!JAVASCRIPT_EXTENSIONS.has(path.extname(absolutePath).toLowerCase())) return true;
|
|
32
|
+
return project.compilerOptions?.checkJs === true;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const errorKey = (diagnostic) => `${diagnostic.code}\u0000${diagnostic.text}`;
|
|
36
|
+
|
|
37
|
+
const fileErrors = (project, absolutePath) =>
|
|
38
|
+
[
|
|
39
|
+
...project.program.getSyntacticDiagnostics(absolutePath),
|
|
40
|
+
...project.program.getSemanticDiagnostics(absolutePath),
|
|
41
|
+
].filter((diagnostic) => diagnostic.category === DiagnosticCategory.Error);
|
|
42
|
+
|
|
43
|
+
const countKeys = (diagnostics) => {
|
|
44
|
+
const counts = new Map();
|
|
45
|
+
for (const diagnostic of diagnostics) {
|
|
46
|
+
const key = errorKey(diagnostic);
|
|
47
|
+
counts.set(key, (counts.get(key) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
return counts;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// The codes of the errors that the mutated file has and the original file
|
|
53
|
+
// does not have. Positions move with the mutation, so the comparison uses the
|
|
54
|
+
// code and the message text.
|
|
55
|
+
const newErrorCodes = (baseline, diagnostics) => {
|
|
56
|
+
const remaining = new Map(baseline);
|
|
57
|
+
const codes = new Set();
|
|
58
|
+
for (const diagnostic of diagnostics) {
|
|
59
|
+
const key = errorKey(diagnostic);
|
|
60
|
+
const count = remaining.get(key) ?? 0;
|
|
61
|
+
if (count > 0) remaining.set(key, count - 1);
|
|
62
|
+
else codes.add(diagnostic.code);
|
|
63
|
+
}
|
|
64
|
+
return [...codes].toSorted((left, right) => left - right).slice(0, MAX_CODES);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const mutatedText = (original, mutant) =>
|
|
68
|
+
Buffer.concat([
|
|
69
|
+
original.subarray(0, mutant.start),
|
|
70
|
+
Buffer.from(mutant.replacement, "utf8"),
|
|
71
|
+
original.subarray(mutant.end),
|
|
72
|
+
]).toString("utf8");
|
|
73
|
+
|
|
74
|
+
const unchecked = (file, reason) =>
|
|
75
|
+
file.mutants.map((mutant) => ({ id: mutant.id, outcome: "unchecked", reason }));
|
|
76
|
+
|
|
77
|
+
const readOriginal = (file) => {
|
|
78
|
+
try {
|
|
79
|
+
return readFileSync(file.absolutePath);
|
|
80
|
+
} catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
const defaultProjectConfig = (api, absolutePath) => {
|
|
86
|
+
const snapshot = api.updateSnapshot({ openFiles: [absolutePath] });
|
|
87
|
+
try {
|
|
88
|
+
const project = snapshot.getDefaultProjectForFile(absolutePath);
|
|
89
|
+
return project && !isInferredProject(project) ? project.configFileName : null;
|
|
90
|
+
} finally {
|
|
91
|
+
api.updateSnapshot({ closeFiles: [absolutePath] }).dispose();
|
|
92
|
+
snapshot.dispose();
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const createOverlayApi = (root, createApi) => {
|
|
97
|
+
const overlay = new Map();
|
|
98
|
+
const api = createApi({
|
|
99
|
+
cwd: root,
|
|
100
|
+
fs: {
|
|
101
|
+
readFile: (fileName) =>
|
|
102
|
+
overlay.size === 0 ? undefined : overlay.get(canonicalFileIdentity(fileName)),
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
return { api, overlay };
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const sourceSkipReason = (file, original) => {
|
|
109
|
+
if (original === null) return "file-missing";
|
|
110
|
+
const changed = file.sourceSha256 !== null && sha256(original) !== file.sourceSha256;
|
|
111
|
+
return changed ? "source-changed" : null;
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
const projectSkipReason = (project, file, original) => {
|
|
115
|
+
if (!project?.program.getSourceFile(file.absolutePath)) return "no-project";
|
|
116
|
+
return isChecked(project, file.absolutePath, original.toString("utf8"))
|
|
117
|
+
? null
|
|
118
|
+
: "not-type-checked";
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
const checkMutant = (context, check, mutant) => {
|
|
122
|
+
context.overlay.set(check.identity, mutatedText(check.original, mutant));
|
|
123
|
+
const next = context.api.updateSnapshot(check.changes);
|
|
124
|
+
check.snapshot.dispose();
|
|
125
|
+
check.snapshot = next;
|
|
126
|
+
const errors = fileErrors(next.getProject(check.config), check.file.absolutePath);
|
|
127
|
+
const codes = newErrorCodes(check.baseline, errors);
|
|
128
|
+
return codes.length > 0
|
|
129
|
+
? { id: mutant.id, outcome: "compile-error", codes }
|
|
130
|
+
: { id: mutant.id, outcome: "compiles" };
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const checkInProject = (context, file, original, config) => {
|
|
134
|
+
const check = {
|
|
135
|
+
file,
|
|
136
|
+
original,
|
|
137
|
+
config,
|
|
138
|
+
identity: canonicalFileIdentity(file.absolutePath),
|
|
139
|
+
changes: { fileChanges: { changed: [file.absolutePath] } },
|
|
140
|
+
snapshot: context.api.updateSnapshot({ openProjects: [config] }),
|
|
141
|
+
baseline: null,
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
const project = check.snapshot.getProject(config);
|
|
145
|
+
const reason = projectSkipReason(project, file, original);
|
|
146
|
+
if (reason !== null) return unchecked(file, reason);
|
|
147
|
+
check.baseline = countKeys(fileErrors(project, file.absolutePath));
|
|
148
|
+
return file.mutants.map((mutant) => checkMutant(context, check, mutant));
|
|
149
|
+
} finally {
|
|
150
|
+
context.overlay.delete(check.identity);
|
|
151
|
+
context.api.updateSnapshot({ ...check.changes, closeProjects: [config] }).dispose();
|
|
152
|
+
check.snapshot.dispose();
|
|
153
|
+
}
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const checkFile = (context, file) => {
|
|
157
|
+
const original = readOriginal(file);
|
|
158
|
+
const reason = sourceSkipReason(file, original);
|
|
159
|
+
if (reason !== null) return unchecked(file, reason);
|
|
160
|
+
const config = defaultProjectConfig(context.api, file.absolutePath);
|
|
161
|
+
if (config === null) return unchecked(file, "no-project");
|
|
162
|
+
return checkInProject(context, file, original, config);
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
/** Type-checks each mutant of a parsed `mutant-typecheck` request. */
|
|
166
|
+
export const checkMutants = (
|
|
167
|
+
{ root, files },
|
|
168
|
+
{ createApi = (options) => new API(options) } = {},
|
|
169
|
+
) => {
|
|
170
|
+
if (files.length === 0) return [];
|
|
171
|
+
const context = createOverlayApi(root, createApi);
|
|
172
|
+
try {
|
|
173
|
+
return files.flatMap((file) => checkFile(context, file));
|
|
174
|
+
} finally {
|
|
175
|
+
context.api.close();
|
|
176
|
+
}
|
|
177
|
+
};
|
package/src/protocol.mjs
CHANGED
|
@@ -7,9 +7,11 @@ import {
|
|
|
7
7
|
ANALYSIS_OPERATION,
|
|
8
8
|
BACKEND_FAMILY,
|
|
9
9
|
BACKEND_VERSION,
|
|
10
|
+
MUTANT_TYPECHECK_OPERATION,
|
|
10
11
|
QUERY_OPERATIONS as MANIFEST_QUERY_OPERATIONS,
|
|
11
12
|
WIRE_PROTOCOL_VERSION,
|
|
12
13
|
} from "./generated-protocol.mjs";
|
|
14
|
+
import { canonicalFileIdentity } from "./file-identity.mjs";
|
|
13
15
|
import { normalizePhaseTimings, normalizeSemanticResult } from "./response-normalization.mjs";
|
|
14
16
|
|
|
15
17
|
const require = createRequire(import.meta.url);
|
|
@@ -54,7 +56,13 @@ const API_SURFACE_QUERY_KEYS = new Set([
|
|
|
54
56
|
"include_cycles",
|
|
55
57
|
"private_leak_candidates",
|
|
56
58
|
]);
|
|
57
|
-
const TYPE_COUPLING_QUERY_KEYS = new Set([
|
|
59
|
+
const TYPE_COUPLING_QUERY_KEYS = new Set([
|
|
60
|
+
"id",
|
|
61
|
+
"operation",
|
|
62
|
+
"entry_points",
|
|
63
|
+
"include_cycles",
|
|
64
|
+
"evidence_limit",
|
|
65
|
+
]);
|
|
58
66
|
const PRIVATE_LEAK_CANDIDATE_KEYS = new Set(["id", "path", "export_name", "type_name"]);
|
|
59
67
|
const SYMBOL_KEYS = new Set([
|
|
60
68
|
"path",
|
|
@@ -75,6 +83,10 @@ const MAX_QUERIES = 25_000;
|
|
|
75
83
|
const MAX_GRAPH_QUERIES = 256;
|
|
76
84
|
const MAX_PRIVATE_LEAK_CANDIDATES = 25_000;
|
|
77
85
|
const MAX_EVIDENCE_PER_RESULT = 40;
|
|
86
|
+
// A type-coupling result is one project-wide report, so it can carry more
|
|
87
|
+
// evidence than a per-symbol result. This bound keeps the response well below
|
|
88
|
+
// the native client's semantic response byte limit.
|
|
89
|
+
const MAX_TYPE_COUPLING_EVIDENCE = 10_000;
|
|
78
90
|
const MAX_STRING_CHARS = 4_096;
|
|
79
91
|
|
|
80
92
|
const isObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
@@ -255,13 +267,31 @@ const parseFrameworkContracts = (value, field) =>
|
|
|
255
267
|
const parseIncludeCycles = (value, field) =>
|
|
256
268
|
value === undefined ? false : requireBoolean(value, field);
|
|
257
269
|
|
|
270
|
+
const parseTypeCouplingEvidenceLimit = (value, field) => {
|
|
271
|
+
const limit = requireInteger(value, field, 1);
|
|
272
|
+
if (limit > MAX_TYPE_COUPLING_EVIDENCE) {
|
|
273
|
+
throw new Error(`${field} exceeds the ${MAX_TYPE_COUPLING_EVIDENCE} item limit`);
|
|
274
|
+
}
|
|
275
|
+
return limit;
|
|
276
|
+
};
|
|
277
|
+
|
|
258
278
|
const parseGraphQuery = (value, field, root, query) => {
|
|
259
279
|
const graphQuery = {
|
|
260
280
|
...query,
|
|
261
281
|
entryPoints: parseEntryPoints(value.entry_points, `${field}.entry_points`, root),
|
|
262
282
|
includeCycles: parseIncludeCycles(value.include_cycles, `${field}.include_cycles`),
|
|
263
283
|
};
|
|
264
|
-
if (query.operation
|
|
284
|
+
if (query.operation === "type-coupling") {
|
|
285
|
+
return value.evidence_limit === undefined
|
|
286
|
+
? graphQuery
|
|
287
|
+
: {
|
|
288
|
+
...graphQuery,
|
|
289
|
+
evidenceLimit: parseTypeCouplingEvidenceLimit(
|
|
290
|
+
value.evidence_limit,
|
|
291
|
+
`${field}.evidence_limit`,
|
|
292
|
+
),
|
|
293
|
+
};
|
|
294
|
+
}
|
|
265
295
|
return {
|
|
266
296
|
...graphQuery,
|
|
267
297
|
privateLeakCandidates: parsePrivateLeakCandidates(
|
|
@@ -375,8 +405,75 @@ const parseBatchRequest = (value) => {
|
|
|
375
405
|
return { root, projects, queries, evidenceLimit };
|
|
376
406
|
};
|
|
377
407
|
|
|
408
|
+
const MUTANT_TYPECHECK_REQUEST_KEYS = new Set(["protocol_version", "operation", "root", "files"]);
|
|
409
|
+
const MUTANT_FILE_KEYS = new Set(["path", "source_sha256", "mutants"]);
|
|
410
|
+
const MUTANT_KEYS = new Set(["id", "start", "end", "replacement"]);
|
|
411
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/u;
|
|
412
|
+
const MAX_MUTANTS = 25_000;
|
|
413
|
+
const MAX_MUTANT_ID_CHARS = 64;
|
|
414
|
+
|
|
415
|
+
const requireText = (value, field) => {
|
|
416
|
+
if (typeof value !== "string") throw new Error(`${field} must be a string`);
|
|
417
|
+
if ([...value].length > MAX_STRING_CHARS) {
|
|
418
|
+
throw new Error(`${field} exceeds the ${MAX_STRING_CHARS} character limit`);
|
|
419
|
+
}
|
|
420
|
+
return value;
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const parseMutant = (value, field, ids) => {
|
|
424
|
+
requireObject(value, field);
|
|
425
|
+
requireExactKeys(value, MUTANT_KEYS, field);
|
|
426
|
+
const id = requireString(value.id, `${field}.id`);
|
|
427
|
+
if (id.length > MAX_MUTANT_ID_CHARS) {
|
|
428
|
+
throw new Error(`${field}.id exceeds the ${MAX_MUTANT_ID_CHARS} character limit`);
|
|
429
|
+
}
|
|
430
|
+
if (ids.has(id)) throw new Error(`duplicate mutant id ${id}`);
|
|
431
|
+
ids.add(id);
|
|
432
|
+
const start = requireInteger(value.start, `${field}.start`, 0);
|
|
433
|
+
const end = requireInteger(value.end, `${field}.end`, start);
|
|
434
|
+
return { id, start, end, replacement: requireText(value.replacement, `${field}.replacement`) };
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const parseMutantFile = (value, field, root, ids) => {
|
|
438
|
+
requireObject(value, field);
|
|
439
|
+
requireExactKeys(value, MUTANT_FILE_KEYS, field);
|
|
440
|
+
const { candidatePath, absolutePath } = parseCandidatePath(value.path, `${field}.path`, root);
|
|
441
|
+
const sourceSha256 =
|
|
442
|
+
value.source_sha256 === undefined
|
|
443
|
+
? null
|
|
444
|
+
: requireString(value.source_sha256, `${field}.source_sha256`);
|
|
445
|
+
if (sourceSha256 !== null && !SHA256_HEX.test(sourceSha256)) {
|
|
446
|
+
throw new Error(`${field}.source_sha256 must be a lowercase SHA-256 hex digest`);
|
|
447
|
+
}
|
|
448
|
+
return {
|
|
449
|
+
path: candidatePath,
|
|
450
|
+
absolutePath: canonicalFileIdentity(absolutePath),
|
|
451
|
+
sourceSha256,
|
|
452
|
+
mutants: requireBoundedArray(value.mutants, `${field}.mutants`, MAX_MUTANTS).map(
|
|
453
|
+
(mutant, index) => parseMutant(mutant, `${field}.mutants[${index}]`, ids),
|
|
454
|
+
),
|
|
455
|
+
};
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
const parseMutantTypecheckRequest = (value) => {
|
|
459
|
+
requireExactKeys(value, MUTANT_TYPECHECK_REQUEST_KEYS, "request");
|
|
460
|
+
const root = parseRoot(value.root);
|
|
461
|
+
const ids = new Set();
|
|
462
|
+
const files = requireBoundedArray(value.files, "files", MAX_MUTANTS).map((file, index) =>
|
|
463
|
+
parseMutantFile(file, `files[${index}]`, root, ids),
|
|
464
|
+
);
|
|
465
|
+
if (ids.size > MAX_MUTANTS) throw new Error(`mutants exceed the ${MAX_MUTANTS} item limit`);
|
|
466
|
+
return { operation: MUTANT_TYPECHECK_OPERATION, root, files };
|
|
467
|
+
};
|
|
468
|
+
|
|
378
469
|
export const parseRequest = (value) => {
|
|
379
470
|
requireObject(value, "request");
|
|
471
|
+
if (
|
|
472
|
+
value.protocol_version === WIRE_PROTOCOL_VERSION &&
|
|
473
|
+
value.operation === MUTANT_TYPECHECK_OPERATION
|
|
474
|
+
) {
|
|
475
|
+
return parseMutantTypecheckRequest(value);
|
|
476
|
+
}
|
|
380
477
|
if (value.protocol_version === WIRE_PROTOCOL_VERSION) {
|
|
381
478
|
return parseBatchRequest(value);
|
|
382
479
|
}
|
|
@@ -405,3 +502,13 @@ export const createSemanticResponse = ({
|
|
|
405
502
|
warnings: normalizeWarnings(warnings),
|
|
406
503
|
elapsed_ms: Math.max(0, Math.round(elapsedMs)),
|
|
407
504
|
});
|
|
505
|
+
|
|
506
|
+
export const createMutantTypecheckResponse = ({ results, elapsedMs }) => ({
|
|
507
|
+
protocol_version: WIRE_PROTOCOL_VERSION,
|
|
508
|
+
operation: MUTANT_TYPECHECK_OPERATION,
|
|
509
|
+
sidecar_version: SIDECAR_VERSION,
|
|
510
|
+
backend: BACKEND_FAMILY,
|
|
511
|
+
backend_version: BACKEND_VERSION,
|
|
512
|
+
results,
|
|
513
|
+
elapsed_ms: Math.max(0, Math.round(elapsedMs)),
|
|
514
|
+
});
|
package/src/semantic.mjs
CHANGED
|
@@ -1891,7 +1891,7 @@ const analyzeGraphSemanticQuery = (root, query, graphStates, evidenceLimit) => {
|
|
|
1891
1891
|
return analyzeApiSurface(root, query, graphStates, evidenceLimit);
|
|
1892
1892
|
}
|
|
1893
1893
|
return analyzeTypeCoupling(
|
|
1894
|
-
{ root, query, states: graphStates, evidenceLimit },
|
|
1894
|
+
{ root, query, states: graphStates, evidenceLimit: query.evidenceLimit ?? evidenceLimit },
|
|
1895
1895
|
{
|
|
1896
1896
|
boundedResult,
|
|
1897
1897
|
discoverEntryPoints,
|