@cplieger/deadset-ts 7.1.0-dev.2 → 7.2.0-dev.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/dist/src/alias-chain.js +6 -2
- package/dist/src/analysis.js +3 -1
- package/dist/src/config.js +4 -0
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/decorator.js +3 -2
- package/dist/src/findings/unused-declarations.js +3 -3
- package/dist/src/injection-container.js +28 -25
- package/dist/src/intra-function-parts.js +72 -11
- package/dist/src/inventory.js +18 -3
- package/dist/src/kinds.js +2 -2
- package/dist/src/ref.js +3 -2
- package/dist/src/references.js +38 -20
- package/dist/src/report.js +1 -1
- package/dist/src/resolve.js +37 -4
- package/dist/src/schema.js +4 -1
- package/dist/src/suppress-file.js +43 -9
- package/dist/src/suppress-inline.js +35 -12
- package/dist/src/suppress.js +3 -2
- package/dist/src/verbs/analyze.js +7 -3
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/alias-chain.ts +7 -2
- package/src/analysis.ts +3 -3
- package/src/config.ts +9 -1
- package/src/conformance-record.ts +2 -2
- package/src/decorator.ts +8 -2
- package/src/findings/unused-declarations.ts +3 -3
- package/src/injection-container.ts +31 -26
- package/src/intra-function-parts.ts +95 -11
- package/src/inventory.ts +26 -2
- package/src/kinds.ts +2 -2
- package/src/ref.ts +3 -2
- package/src/references.ts +55 -21
- package/src/report.ts +1 -1
- package/src/resolve.ts +56 -6
- package/src/schema.ts +10 -14
- package/src/signatures.ts +6 -1
- package/src/suppress-file.ts +48 -9
- package/src/suppress-inline.ts +57 -15
- package/src/suppress.ts +3 -2
- package/src/verbs/analyze.ts +7 -2
- package/src/version.ts +1 -1
package/dist/src/resolve.js
CHANGED
|
@@ -21,9 +21,9 @@ const PLATFORM_MEMBERS = ["os", "arch", "tags"];
|
|
|
21
21
|
const PROJECT_MEMBER = "project";
|
|
22
22
|
/**
|
|
23
23
|
* One segment of a project path: not empty, not `.` or `..`, and holding no
|
|
24
|
-
* solidus
|
|
24
|
+
* solidus or control character.
|
|
25
25
|
*/
|
|
26
|
-
const PROJECT_SEGMENT = String.raw `(?:[
|
|
26
|
+
const PROJECT_SEGMENT = String.raw `(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.\.[^/\x00-\x1f\x7f]+)`;
|
|
27
27
|
/**
|
|
28
28
|
* A project path: segments joined by a solidus, so the path is relative, below
|
|
29
29
|
* the root it is read against, and spelled one way on every machine.
|
|
@@ -151,6 +151,39 @@ function readStrings(parent, name, path, label, minItems, allowed) {
|
|
|
151
151
|
}
|
|
152
152
|
return value;
|
|
153
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* The target kind, a string or an object naming every language. The walk has
|
|
156
|
+
* already refused an undeclared member, so an object is refused for the first
|
|
157
|
+
* language it leaves out, then for the first language whose value is refused,
|
|
158
|
+
* each in the vocabulary's language order.
|
|
159
|
+
*/
|
|
160
|
+
function readTargetKind(target, label) {
|
|
161
|
+
const value = member(target, "kind");
|
|
162
|
+
const perLanguage = `an object naming the kind of each of ${spell(LANGUAGES)}`;
|
|
163
|
+
if (value === undefined) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
if (typeof value === "string") {
|
|
167
|
+
if (!TARGET_KINDS.includes(value)) {
|
|
168
|
+
throw malformed(label, "target.kind", `${JSON.stringify(value)} is not one of ${spell(TARGET_KINDS)}, nor ${perLanguage}`);
|
|
169
|
+
}
|
|
170
|
+
return value;
|
|
171
|
+
}
|
|
172
|
+
if (!isRecord(value)) {
|
|
173
|
+
throw malformed(label, "target.kind", `${JSON.stringify(value)} is neither one of ${spell(TARGET_KINDS)} nor ${perLanguage}`);
|
|
174
|
+
}
|
|
175
|
+
const missing = LANGUAGES.find((language) => !Object.hasOwn(value, language));
|
|
176
|
+
if (missing !== undefined) {
|
|
177
|
+
throw malformed(label, `target.kind.${missing}`, `is not set, and ${perLanguage} names each`);
|
|
178
|
+
}
|
|
179
|
+
for (const language of LANGUAGES) {
|
|
180
|
+
const kind = value[language];
|
|
181
|
+
if (typeof kind !== "string" || !TARGET_KINDS.includes(kind)) {
|
|
182
|
+
throw malformed(label, `target.kind.${language}`, `${JSON.stringify(kind)} is not one of ${spell(TARGET_KINDS)}`);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { go: value["go"], ts: value["ts"] };
|
|
186
|
+
}
|
|
154
187
|
/** A component file's extension: a full stop followed by letters and digits. */
|
|
155
188
|
const COMPONENT_EXTENSION = /^\.[A-Za-z0-9]+$/u;
|
|
156
189
|
function readComponentExtensions(ts, label) {
|
|
@@ -194,7 +227,7 @@ function readProject(entry, at, label) {
|
|
|
194
227
|
const project = requireMember(readString(entry, PROJECT_MEMBER, path, label), path, label);
|
|
195
228
|
if (!PROJECT_PATH.test(project)) {
|
|
196
229
|
throw malformed(label, path, `${JSON.stringify(project)} is not a path below the target root: segments joined by /, ` +
|
|
197
|
-
"none empty, . or .., and no
|
|
230
|
+
"none empty, . or .., and no control character");
|
|
198
231
|
}
|
|
199
232
|
return { shape: "project", id, project };
|
|
200
233
|
}
|
|
@@ -508,7 +541,7 @@ function readDoc(value, label) {
|
|
|
508
541
|
}
|
|
509
542
|
return {
|
|
510
543
|
contractVersion,
|
|
511
|
-
targetKind:
|
|
544
|
+
targetKind: readTargetKind(target, label),
|
|
512
545
|
languages: readStrings(analysis, "languages", "analysis.languages", label, 0, LANGUAGES),
|
|
513
546
|
minConfidence: readEnum(analysis, "min_confidence", "analysis.min_confidence", label, CONFIDENCES),
|
|
514
547
|
generatedFiles: readEnum(analysis, "generated_files", "analysis.generated_files", label, GENERATED_FILES),
|
package/dist/src/schema.js
CHANGED
|
@@ -15,7 +15,10 @@ export const SCHEMA_ROOT = {
|
|
|
15
15
|
kind: "section",
|
|
16
16
|
members: {
|
|
17
17
|
contract_version: leaf,
|
|
18
|
-
target: {
|
|
18
|
+
target: {
|
|
19
|
+
kind: "section",
|
|
20
|
+
members: { kind: { kind: "object", members: { go: leaf, ts: leaf } } },
|
|
21
|
+
},
|
|
19
22
|
analysis: {
|
|
20
23
|
kind: "section",
|
|
21
24
|
members: {
|
|
@@ -205,11 +205,12 @@ export function rowReason(provenance) {
|
|
|
205
205
|
return `recorded by ${provenance.analyzer} ${provenance.version}`;
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
208
|
-
* The rows of
|
|
209
|
-
* document order and as written, which a write keeps. A document that is
|
|
210
|
-
* or a row in a shape no record takes, is refused rather
|
|
208
|
+
* The rows of a scheme other than `own` that the baseline document `text` holds, in
|
|
209
|
+
* document order and as written, which a write of `own`'s rows keeps. A document that is
|
|
210
|
+
* not a baseline, or a row in a shape no record takes, of any scheme, is refused rather
|
|
211
|
+
* than dropped.
|
|
211
212
|
*/
|
|
212
|
-
export function otherSchemeRows(text) {
|
|
213
|
+
export function otherSchemeRows(text, own = OWN_SCHEME) {
|
|
213
214
|
const refuse = (detail) => {
|
|
214
215
|
throw new Error(`${BASELINE_FILE} cannot be rewritten keeping the other language's rows: ${detail}`);
|
|
215
216
|
};
|
|
@@ -233,7 +234,7 @@ export function otherSchemeRows(text) {
|
|
|
233
234
|
}
|
|
234
235
|
// readRecord has checked every member is a string and the code and symbol are present.
|
|
235
236
|
const { code, symbol, path, reason } = value;
|
|
236
|
-
if (code === undefined || symbol === undefined || symbol.startsWith(
|
|
237
|
+
if (code === undefined || symbol === undefined || symbol.startsWith(own)) {
|
|
237
238
|
return [];
|
|
238
239
|
}
|
|
239
240
|
return [
|
|
@@ -246,10 +247,32 @@ export function otherSchemeRows(text) {
|
|
|
246
247
|
];
|
|
247
248
|
});
|
|
248
249
|
}
|
|
250
|
+
/** The reference schemes in the order a baseline holds their rows. */
|
|
251
|
+
const SCHEME_ORDER = ["go://", "ts://"];
|
|
252
|
+
function schemeRank(row) {
|
|
253
|
+
return SCHEME_ORDER.findIndex((scheme) => row.symbol.startsWith(scheme));
|
|
254
|
+
}
|
|
255
|
+
/** A surrogate half no other completes, which Go's encoding/json decodes as U+FFFD. */
|
|
256
|
+
const LONE_SURROGATE = /[\ud800-\udfff]/gu;
|
|
257
|
+
/** One string with every lone surrogate written as U+FFFD. */
|
|
258
|
+
function wellFormed(text) {
|
|
259
|
+
return text.replace(LONE_SURROGATE, "\ufffd");
|
|
260
|
+
}
|
|
249
261
|
/**
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
|
|
262
|
+
* The pieces of a JSON document with U+2028 and U+2029 escaped, as Go's encoding/json
|
|
263
|
+
* writes them. Neither is JSON syntax, so each stands inside a string.
|
|
264
|
+
*/
|
|
265
|
+
function* separatorsEscaped(pieces) {
|
|
266
|
+
for (const piece of pieces) {
|
|
267
|
+
yield piece.replaceAll("\u2028", "\\u2028").replaceAll("\u2029", "\\u2029");
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* A baseline holding one row per finding, in the order given, which is the report's, and
|
|
272
|
+
* the other language's rows `kept`, Go's rows ahead of TypeScript's and each scheme's in
|
|
273
|
+
* the order given, so two analyzers writing one document in either order write the same
|
|
274
|
+
* bytes, in the form grammar/suppression.md fixes. Every recorded row carries the
|
|
275
|
+
* provenance as its reason. A finding
|
|
253
276
|
* missing a value a row needs, and an identity missing its name or version, are refused
|
|
254
277
|
* rather than written, because the grammar requires a reason on every row.
|
|
255
278
|
*/
|
|
@@ -264,5 +287,16 @@ export function writeBaseline(findings, provenance, kept = []) {
|
|
|
264
287
|
}
|
|
265
288
|
return { code: found.code, symbol: found.symbol, path: found.path, reason };
|
|
266
289
|
});
|
|
267
|
-
|
|
290
|
+
const rows = [...kept, ...baseline]
|
|
291
|
+
.sort((a, b) => schemeRank(a) - schemeRank(b))
|
|
292
|
+
.map((row) => {
|
|
293
|
+
const { code, symbol, path, reason } = row;
|
|
294
|
+
return {
|
|
295
|
+
code: wellFormed(code),
|
|
296
|
+
symbol: wellFormed(symbol),
|
|
297
|
+
...(path === undefined ? {} : { path: wellFormed(path) }),
|
|
298
|
+
...(reason === undefined ? {} : { reason: wellFormed(reason) }),
|
|
299
|
+
};
|
|
300
|
+
});
|
|
301
|
+
return separatorsEscaped(jsonChunks({ description: BASELINE_DESCRIPTION, baseline: rows }, 2));
|
|
268
302
|
}
|
|
@@ -3,11 +3,11 @@
|
|
|
3
3
|
* one or more codes and a reason, and binding to every declaration that begins on the line
|
|
4
4
|
* below it, or on its own line where it trails a token there.
|
|
5
5
|
*/
|
|
6
|
-
import { SyntaxKind } from "@typescript/native/ast";
|
|
7
|
-
import { nodeKey } from "./inventory.js";
|
|
6
|
+
import { isConstructorDeclaration, SyntaxKind, } from "@typescript/native/ast";
|
|
7
|
+
import { isParameterProperty, nodeKey } from "./inventory.js";
|
|
8
8
|
import { byPosition, positionKey, renderPosition } from "./position.js";
|
|
9
9
|
import { signatureOf } from "./signatures.js";
|
|
10
|
-
import { SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
|
|
10
|
+
import { isPartKind, SUPPRESSION_WITHOUT_REASON, SuppressionError, siteText, } from "./suppress.js";
|
|
11
11
|
/**
|
|
12
12
|
* The three expressions the grammar applies to the text of each line comment, in this
|
|
13
13
|
* order: a comment in the namespace, a well-formed directive, and a directive that lacks
|
|
@@ -132,37 +132,53 @@ function lineComments(file) {
|
|
|
132
132
|
}
|
|
133
133
|
return found;
|
|
134
134
|
}
|
|
135
|
+
function bindingKey(one) {
|
|
136
|
+
return `${one.declared?.id ?? ""}\u0000${one.part.id}`;
|
|
137
|
+
}
|
|
135
138
|
/**
|
|
136
139
|
* The declarations of one file by the line each begins on, the line of its first token: a
|
|
137
140
|
* decorator, a modifier or the keyword. A variable's first token is its statement's
|
|
138
141
|
* keyword, and a variable written on a later line of the statement also begins there. A
|
|
139
|
-
* function also begins, for a directive, on the line of each of its parameters
|
|
142
|
+
* function also begins, for a directive, on the line of each of its parameters, and a
|
|
143
|
+
* class, for a part kind, on the lines of its constructor and its constructor's parameters.
|
|
140
144
|
*/
|
|
141
145
|
function declarationLines(file, held, symbols) {
|
|
142
146
|
const below = new Map();
|
|
143
147
|
const add = (offset, one) => {
|
|
144
148
|
const line = file.getLineAndCharacterOfPosition(tokenStart(file.text, offset)).line + 1;
|
|
145
149
|
const at = below.get(line) ?? [];
|
|
146
|
-
if (!at.some((known) => known
|
|
150
|
+
if (!at.some((known) => bindingKey(known) === bindingKey(one))) {
|
|
147
151
|
at.push(one);
|
|
148
152
|
}
|
|
149
153
|
below.set(line, at);
|
|
150
154
|
};
|
|
151
|
-
const
|
|
155
|
+
const declaredAt = (node) => {
|
|
152
156
|
const id = held.declarations.get(nodeKey(file, node));
|
|
153
|
-
|
|
157
|
+
return id === undefined ? undefined : symbols.get(id);
|
|
158
|
+
};
|
|
159
|
+
const visit = (node) => {
|
|
160
|
+
const one = declaredAt(node);
|
|
161
|
+
const owner = isConstructorDeclaration(node) ? declaredAt(node.parent) : undefined;
|
|
162
|
+
if (owner !== undefined && isConstructorDeclaration(node)) {
|
|
163
|
+
for (const at of [node, ...node.parameters.filter((one) => !isParameterProperty(one))]) {
|
|
164
|
+
add(at.pos, { declared: undefined, part: owner });
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
const holder = one !== undefined && isParameterProperty(node) ? declaredAt(node.parent.parent) : one;
|
|
168
|
+
if (one !== undefined && holder !== undefined) {
|
|
169
|
+
add(node.pos, { declared: one, part: holder });
|
|
170
|
+
}
|
|
154
171
|
if (one !== undefined) {
|
|
155
|
-
add(node.pos, one);
|
|
156
172
|
// A parameter on a later line of a wrapped signature takes its directive on the
|
|
157
173
|
// line above it, inside the parameter list, and the directive binds the function.
|
|
158
174
|
for (const parameter of signatureOf(node)?.parameters ?? []) {
|
|
159
|
-
add(parameter.pos, one);
|
|
175
|
+
add(parameter.pos, { declared: one, part: one });
|
|
160
176
|
}
|
|
161
177
|
const list = node.parent;
|
|
162
178
|
if (node.kind === SyntaxKind.VariableDeclaration &&
|
|
163
179
|
list.kind === SyntaxKind.VariableDeclarationList &&
|
|
164
180
|
list.parent.kind === SyntaxKind.VariableStatement) {
|
|
165
|
-
add(list.parent.pos, one);
|
|
181
|
+
add(list.parent.pos, { declared: one, part: one });
|
|
166
182
|
}
|
|
167
183
|
}
|
|
168
184
|
node.forEachChild(visit);
|
|
@@ -223,7 +239,7 @@ export function inlineSuppressions(perProject) {
|
|
|
223
239
|
continue;
|
|
224
240
|
}
|
|
225
241
|
for (const one of written.bound) {
|
|
226
|
-
if (!known.bound.some((other) => other
|
|
242
|
+
if (!known.bound.some((other) => bindingKey(other) === bindingKey(one))) {
|
|
227
243
|
known.bound.push(one);
|
|
228
244
|
}
|
|
229
245
|
}
|
|
@@ -251,8 +267,15 @@ export function inlineSuppressions(perProject) {
|
|
|
251
267
|
}
|
|
252
268
|
break;
|
|
253
269
|
case "directive": {
|
|
254
|
-
const declared = [...bound].sort((a, b) => byPosition(a.position, b.position));
|
|
255
270
|
for (const code of directive.codes) {
|
|
271
|
+
const targets = new Map();
|
|
272
|
+
for (const one of bound) {
|
|
273
|
+
const target = isPartKind(code) ? one.part : one.declared;
|
|
274
|
+
if (target !== undefined) {
|
|
275
|
+
targets.set(target.id, target);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
const declared = [...targets.values()].sort((a, b) => byPosition(a.position, b.position));
|
|
256
279
|
const base = {
|
|
257
280
|
code,
|
|
258
281
|
path: site.path,
|
package/dist/src/suppress.js
CHANGED
|
@@ -34,9 +34,10 @@ export function siteText(site) {
|
|
|
34
34
|
export const CODE_FORM = /^DS[0-9]{4}$/u;
|
|
35
35
|
/**
|
|
36
36
|
* A path inside the target: relative to the target root, the solidus as separator, no
|
|
37
|
-
* leading `./`, no trailing solidus, no `.` or `..` segment, never absolute
|
|
37
|
+
* leading `./`, no trailing solidus, no `.` or `..` segment, never absolute, and no
|
|
38
|
+
* control character; a backslash is a character of a name.
|
|
38
39
|
*/
|
|
39
|
-
export const PATH_EXPRESSION = String.raw `^(?:[
|
|
40
|
+
export const PATH_EXPRESSION = String.raw `^(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.\.[^/\x00-\x1f\x7f]+)(?:/(?:[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.[^/.\x00-\x1f\x7f][^/\x00-\x1f\x7f]*|\.\.[^/\x00-\x1f\x7f]+))*$`;
|
|
40
41
|
const PATH_FORM = new RegExp(PATH_EXPRESSION, "u");
|
|
41
42
|
/** Whether one value is a path in the form an entry's `path` takes. */
|
|
42
43
|
export function isEntryPath(value) {
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { runAnalysis, runBaselineWrite, unansweredCounts, } from "../analysis.js";
|
|
2
|
-
import { ConfigError } from "../config.js";
|
|
2
|
+
import { ConfigError, kindOf } from "../config.js";
|
|
3
3
|
import { CONFORMANCE, DECLARED_GAPS } from "../conformance.js";
|
|
4
4
|
import { moduleIdentity } from "../consumers.js";
|
|
5
5
|
import { analyzerProvenance, verdictOf } from "../findings-pass.js";
|
|
6
6
|
import { joinPath, normalizePath, relativePath, resolvePath } from "../paths.js";
|
|
7
7
|
import { partialNotes } from "../partial.js";
|
|
8
|
-
import { buildReport, capFindings, sortFindings, } from "../report.js";
|
|
8
|
+
import { buildReport, LANGUAGE, capFindings, sortFindings, } from "../report.js";
|
|
9
9
|
import { json } from "../reporters/json.js";
|
|
10
10
|
import { RENDERINGS } from "../reporters/reporters.js";
|
|
11
11
|
import { parseTemplate, TemplateError } from "../reporters/template.js";
|
|
@@ -188,7 +188,11 @@ export const analyzeVerb = ({ err, host, inputs, scope, openClient, option, repe
|
|
|
188
188
|
version,
|
|
189
189
|
conformance: CONFORMANCE,
|
|
190
190
|
declaredGaps: DECLARED_GAPS,
|
|
191
|
-
target: {
|
|
191
|
+
target: {
|
|
192
|
+
kind: kindOf(kind, LANGUAGE),
|
|
193
|
+
root,
|
|
194
|
+
identity: moduleIdentity(host, scoped.target),
|
|
195
|
+
},
|
|
192
196
|
configurations: configurationsOf(run.projects, targetRoot),
|
|
193
197
|
notBuilt: notBuiltOf(run.notBuilt, targetRoot),
|
|
194
198
|
loaded: run.consumers.map((consumer) => ({
|
package/dist/src/version.js
CHANGED
package/jsr.json
CHANGED
package/package.json
CHANGED
package/src/alias-chain.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
isNamespaceImport,
|
|
15
15
|
} from "@typescript/native/ast";
|
|
16
16
|
import { SymbolFlags, type Symbol as TSSymbol } from "@typescript/native/sync";
|
|
17
|
-
import { declarationsByName, nodeKey, type Inventory } from "./inventory.ts";
|
|
17
|
+
import { declarationsByName, isParameterProperty, nodeKey, type Inventory } from "./inventory.ts";
|
|
18
18
|
import { UNANSWERED, type Answer } from "./query.ts";
|
|
19
19
|
import type { ProjectView } from "./session.ts";
|
|
20
20
|
|
|
@@ -76,7 +76,12 @@ export function aliasChains<Brand>(project: ProjectView<Brand>, held: Inventory)
|
|
|
76
76
|
const node = project.declarationAt(handle)?.node;
|
|
77
77
|
const id =
|
|
78
78
|
node === undefined ? undefined : held.declarations.get(nodeKey(node.getSourceFile(), node));
|
|
79
|
-
|
|
79
|
+
// A parameter property's node declares the parameter too, whose reads are no member's.
|
|
80
|
+
const parameter =
|
|
81
|
+
node !== undefined &&
|
|
82
|
+
isParameterProperty(node) &&
|
|
83
|
+
(symbol.flags & SymbolFlags.Property) === 0;
|
|
84
|
+
if (id !== undefined && !parameter && !ids.includes(id)) {
|
|
80
85
|
ids.push(id);
|
|
81
86
|
}
|
|
82
87
|
}
|
package/src/analysis.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
9
|
import { lineOf, readComponent } from "./component-files.ts";
|
|
10
|
-
import type
|
|
10
|
+
import { kindOf, type Config, type Provenance } from "./config.ts";
|
|
11
11
|
import {
|
|
12
12
|
consumerLoads,
|
|
13
13
|
consumerReferences,
|
|
@@ -83,7 +83,7 @@ import {
|
|
|
83
83
|
import { dirnamePath, joinPath, normalizePath, relativePath, resolvePath } from "./paths.ts";
|
|
84
84
|
import type { UnansweredQuestion } from "./query.ts";
|
|
85
85
|
import { byPosition, isComponentFile, type Position } from "./position.ts";
|
|
86
|
-
import type
|
|
86
|
+
import { LANGUAGE, type TypeErrorSkip, type UnansweredCount } from "./report.ts";
|
|
87
87
|
import { SetupError, setupLine, type SetupFailure } from "./setup-failure.ts";
|
|
88
88
|
import {
|
|
89
89
|
pathsOf,
|
|
@@ -377,7 +377,7 @@ function readProjects<Answer>(
|
|
|
377
377
|
conventions,
|
|
378
378
|
testFiles: config.ts.testFiles,
|
|
379
379
|
parse,
|
|
380
|
-
publishedAPI: config.targetKind === "library",
|
|
380
|
+
publishedAPI: config.targetKind !== "" && kindOf(config.targetKind, LANGUAGE) === "library",
|
|
381
381
|
publishedMembers: new Set(
|
|
382
382
|
(workspace?.entries ?? []).filter((entry) => entry.published).map((entry) => entry.dir),
|
|
383
383
|
),
|
package/src/config.ts
CHANGED
|
@@ -13,6 +13,14 @@ export const REPOSITORY_DOCUMENT = "deadset.json";
|
|
|
13
13
|
*/
|
|
14
14
|
export type TargetKind = "application" | "library";
|
|
15
15
|
|
|
16
|
+
/** The target kind as a source writes it: one kind for every language, or one per language. */
|
|
17
|
+
export type TargetKindSetting = TargetKind | { readonly go: TargetKind; readonly ts: TargetKind };
|
|
18
|
+
|
|
19
|
+
/** The kind a target kind setting gives one language's declarations. */
|
|
20
|
+
export function kindOf(setting: TargetKindSetting, language: Language): TargetKind {
|
|
21
|
+
return typeof setting === "string" ? setting : setting[language];
|
|
22
|
+
}
|
|
23
|
+
|
|
16
24
|
/** The lowest reachability class a finding must reach to be reported. */
|
|
17
25
|
export type Confidence = "certain" | "probable" | "possible";
|
|
18
26
|
|
|
@@ -183,7 +191,7 @@ export interface TSSection {
|
|
|
183
191
|
*/
|
|
184
192
|
export interface Config {
|
|
185
193
|
readonly contractVersion: string;
|
|
186
|
-
readonly targetKind:
|
|
194
|
+
readonly targetKind: TargetKindSetting | "";
|
|
187
195
|
readonly analysis: Analysis;
|
|
188
196
|
readonly consumersComplete: boolean;
|
|
189
197
|
readonly rootPatterns: readonly string[];
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// Regenerate it with `UPDATE_GOLDEN=1 npx vitest --run src/conformance.corpus.test.ts`.
|
|
3
3
|
export const RECORD = {
|
|
4
4
|
conformance: {
|
|
5
|
-
corpusVersion: "5.
|
|
5
|
+
corpusVersion: "5.2.0",
|
|
6
6
|
result: "pass",
|
|
7
|
-
digest: "sha256:
|
|
7
|
+
digest: "sha256:4050d1b67fb757bc12a60f8ece7317a3e8485d3383ec52919cd24b9dfd5509dc",
|
|
8
8
|
},
|
|
9
9
|
gaps: [],
|
|
10
10
|
} as const;
|
package/src/decorator.ts
CHANGED
|
@@ -14,7 +14,12 @@
|
|
|
14
14
|
import type { Node } from "@typescript/native/ast";
|
|
15
15
|
import { decoratorsOf, spelledDecorator } from "./calls.ts";
|
|
16
16
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
isParameterProperty,
|
|
19
|
+
nodeKey,
|
|
20
|
+
type InventorySymbol,
|
|
21
|
+
type SymbolKind,
|
|
22
|
+
} from "./inventory.ts";
|
|
18
23
|
import { renderPosition } from "./position.ts";
|
|
19
24
|
|
|
20
25
|
/** The kinds of declaration a decorator is attached to and holds back. */
|
|
@@ -44,7 +49,8 @@ export function decorator<Brand>(input: DetectorInput<Brand>): readonly Evidence
|
|
|
44
49
|
const visit = (node: Node): void => {
|
|
45
50
|
const id = held.declarations.get(nodeKey(file, node));
|
|
46
51
|
const symbol = id === undefined ? undefined : byId.get(id);
|
|
47
|
-
|
|
52
|
+
// A parameter property's decorator is attached to its parameter, and to no member.
|
|
53
|
+
if (symbol !== undefined && DECORATED.has(symbol.kind) && !isParameterProperty(node)) {
|
|
48
54
|
for (const attached of decoratorsOf(node)) {
|
|
49
55
|
const detail = `decorated by ${spelledDecorator(attached)}`;
|
|
50
56
|
const site = renderPosition(file, targetRoot, attached.getStart());
|
|
@@ -78,13 +78,13 @@ function codeOf(judged: Judged): string | undefined {
|
|
|
78
78
|
) {
|
|
79
79
|
return undefined;
|
|
80
80
|
}
|
|
81
|
-
if (candidate.unreferencedTest) {
|
|
82
|
-
return unreferencedCode(symbol);
|
|
83
|
-
}
|
|
84
81
|
const referenced = candidate.productionRefs + candidate.testRefs > 0;
|
|
85
82
|
if (symbol.kind === "enum-member" && !referenced) {
|
|
86
83
|
return undefined;
|
|
87
84
|
}
|
|
85
|
+
if (candidate.unreferencedTest) {
|
|
86
|
+
return unreferencedCode(symbol);
|
|
87
|
+
}
|
|
88
88
|
if (candidate.heldByTests === candidate.id) {
|
|
89
89
|
return "DS1004";
|
|
90
90
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The injection-container class: a container constructs a class passed to a call
|
|
3
3
|
* `ts.injection_registrations` names, or one a parameter of whose constructor carries a
|
|
4
|
-
* decorator,
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* decorator, passes every argument of its constructor, and populates each of its
|
|
5
|
+
* properties and accessors that carries a decorator, a base class's included. So each
|
|
6
|
+
* parameter property is injected, decorated or not. A constructor is no declaration of
|
|
7
|
+
* the inventory, so the class stands for it and the registration referencing it keeps it.
|
|
7
8
|
*/
|
|
8
9
|
|
|
9
10
|
import {
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
isPropertyDeclaration,
|
|
15
16
|
isSetAccessorDeclaration,
|
|
16
17
|
type CallExpression,
|
|
18
|
+
type Decorator,
|
|
17
19
|
type Node,
|
|
18
20
|
} from "@typescript/native/ast";
|
|
19
21
|
import { aliasChains } from "./alias-chain.ts";
|
|
@@ -27,7 +29,7 @@ import {
|
|
|
27
29
|
type ResolvedEntry,
|
|
28
30
|
} from "./configured-declarations.ts";
|
|
29
31
|
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
30
|
-
import { nodeKey } from "./inventory.ts";
|
|
32
|
+
import { isParameterProperty, nodeKey } from "./inventory.ts";
|
|
31
33
|
import { renderPosition } from "./position.ts";
|
|
32
34
|
|
|
33
35
|
/** One class a container constructs, by the registration that says so. */
|
|
@@ -37,13 +39,14 @@ interface Registration {
|
|
|
37
39
|
readonly detail: string;
|
|
38
40
|
}
|
|
39
41
|
|
|
40
|
-
/** Whether one member declaration is a property or
|
|
42
|
+
/** Whether one member declaration is a parameter property, or a property or accessor carrying a decorator. */
|
|
41
43
|
function isInjected(node: Node): boolean {
|
|
42
44
|
return (
|
|
43
|
-
(
|
|
45
|
+
isParameterProperty(node) ||
|
|
46
|
+
((isPropertyDeclaration(node) ||
|
|
44
47
|
isGetAccessorDeclaration(node) ||
|
|
45
48
|
isSetAccessorDeclaration(node)) &&
|
|
46
|
-
|
|
49
|
+
decoratorsOf(node).length > 0)
|
|
47
50
|
);
|
|
48
51
|
}
|
|
49
52
|
|
|
@@ -77,21 +80,24 @@ function registrations<Brand>(input: DetectorInput<Brand>): readonly Registratio
|
|
|
77
80
|
});
|
|
78
81
|
}
|
|
79
82
|
|
|
80
|
-
/**
|
|
81
|
-
|
|
83
|
+
/**
|
|
84
|
+
* The classes of the target a parameter of whose constructor carries a decorator, each
|
|
85
|
+
* with the first such decorator.
|
|
86
|
+
*/
|
|
87
|
+
function constructedForParameters<Brand>(
|
|
88
|
+
input: DetectorInput<Brand>,
|
|
89
|
+
): ReadonlyMap<string, Decorator> {
|
|
82
90
|
const { project, held } = input;
|
|
83
|
-
const found = new
|
|
91
|
+
const found = new Map<string, Decorator>();
|
|
84
92
|
for (const file of project.ownSourceFiles()) {
|
|
85
93
|
const visit = (node: Node): void => {
|
|
86
94
|
if (isClassDeclaration(node) || isClassExpression(node)) {
|
|
87
95
|
const id = held.declarations.get(nodeKey(file, node));
|
|
88
|
-
const asks = node.members.
|
|
89
|
-
(member)
|
|
90
|
-
isConstructorDeclaration(member) &&
|
|
91
|
-
member.parameters.some((parameter) => decoratorsOf(parameter).length > 0),
|
|
96
|
+
const [asks] = node.members.flatMap((member) =>
|
|
97
|
+
isConstructorDeclaration(member) ? member.parameters.flatMap(decoratorsOf) : [],
|
|
92
98
|
);
|
|
93
|
-
if (id !== undefined && asks) {
|
|
94
|
-
found.
|
|
99
|
+
if (id !== undefined && asks !== undefined) {
|
|
100
|
+
found.set(id, asks);
|
|
95
101
|
}
|
|
96
102
|
}
|
|
97
103
|
node.forEachChild(visit);
|
|
@@ -110,7 +116,7 @@ export function injectionContainer<Brand>(input: DetectorInput<Brand>): readonly
|
|
|
110
116
|
const { project, held, targetRoot } = input;
|
|
111
117
|
const registered = registrations(input);
|
|
112
118
|
const constructed = constructedForParameters(input);
|
|
113
|
-
const classes = new Set([...registered.map((one) => one.id), ...constructed]);
|
|
119
|
+
const classes = new Set([...registered.map((one) => one.id), ...constructed.keys()]);
|
|
114
120
|
if (classes.size === 0) {
|
|
115
121
|
return [];
|
|
116
122
|
}
|
|
@@ -125,16 +131,15 @@ export function injectionContainer<Brand>(input: DetectorInput<Brand>): readonly
|
|
|
125
131
|
found.push({ id: member.id, detail: registration.detail, site: at(registration.site) });
|
|
126
132
|
}
|
|
127
133
|
}
|
|
128
|
-
for (const id of constructed) {
|
|
134
|
+
for (const [id, asks] of constructed) {
|
|
129
135
|
for (const member of injected(id)) {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
}
|
|
136
|
+
// An undecorated parameter property is passed because a sibling parameter asks.
|
|
137
|
+
const [decorator = asks] = decoratorsOf(member.node);
|
|
138
|
+
found.push({
|
|
139
|
+
id: member.id,
|
|
140
|
+
detail: `injected by ${spelledDecorator(decorator)}`,
|
|
141
|
+
site: at(decorator),
|
|
142
|
+
});
|
|
138
143
|
}
|
|
139
144
|
}
|
|
140
145
|
return found;
|