@cplieger/deadset-ts 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/analysis.js +77 -10
- package/dist/src/configuration-files.js +107 -3
- package/dist/src/conformance-record.js +3 -9
- package/dist/src/contextual-uses.js +240 -0
- package/dist/src/convention-rows.js +6 -0
- package/dist/src/conventions.js +123 -14
- package/dist/src/dependencies.js +8 -2
- package/dist/src/destructuring.js +6 -6
- package/dist/src/entry-points.js +8 -5
- package/dist/src/exempt-classes.js +6 -1
- package/dist/src/exempt.js +21 -17
- package/dist/src/findings/completion.js +27 -3
- package/dist/src/findings/interfaces.js +6 -2
- package/dist/src/findings/reads-and-writes.js +53 -9
- package/dist/src/findings/unused-declarations.js +40 -1
- package/dist/src/generated-file.js +24 -0
- package/dist/src/interface-satisfaction.js +2 -68
- package/dist/src/inventory.js +4 -1
- package/dist/src/kinds.js +4 -3
- package/dist/src/query.js +15 -0
- package/dist/src/references.js +199 -10
- package/dist/src/resolve.js +12 -11
- package/dist/src/roots.js +1 -1
- package/dist/src/stores.js +3 -0
- package/dist/src/value-flow.js +74 -0
- package/dist/src/version.js +1 -1
- package/jsr.json +1 -1
- package/package.json +2 -2
- package/src/analysis.ts +92 -13
- package/src/configuration-files.ts +136 -1
- package/src/conformance-record.ts +3 -10
- package/src/contextual-uses.ts +354 -0
- package/src/convention-rows.ts +8 -0
- package/src/conventions.ts +161 -13
- package/src/dependencies.ts +14 -1
- package/src/destructuring.ts +16 -6
- package/src/entry-points.ts +16 -3
- package/src/exempt-classes.ts +6 -1
- package/src/exempt.ts +48 -17
- package/src/file-facts.ts +2 -0
- package/src/findings/completion.ts +31 -3
- package/src/findings/interfaces.ts +9 -2
- package/src/findings/reads-and-writes.ts +70 -9
- package/src/findings/unused-declarations.ts +51 -1
- package/src/generated-file.ts +27 -0
- package/src/interface-satisfaction.ts +1 -92
- package/src/inventory.ts +4 -1
- package/src/kinds.ts +5 -3
- package/src/query.ts +26 -0
- package/src/references.ts +254 -21
- package/src/resolve.ts +14 -13
- package/src/roots.ts +11 -1
- package/src/stores.ts +9 -0
- package/src/value-flow.ts +104 -0
- package/src/version.ts +1 -1
package/src/dependencies.ts
CHANGED
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
declaredIn as declaredBeside,
|
|
28
28
|
dependenciesNamed,
|
|
29
29
|
moduleStrings,
|
|
30
|
+
type ParseFile,
|
|
30
31
|
} from "./configuration-files.ts";
|
|
31
32
|
import type { FindingPosition } from "./finding.ts";
|
|
32
33
|
import type { Host } from "./host.ts";
|
|
@@ -284,6 +285,7 @@ export function projectNeeds<Brand>(
|
|
|
284
285
|
held: Inventory,
|
|
285
286
|
host: Host,
|
|
286
287
|
targetRoot: string,
|
|
288
|
+
parse?: ParseFile,
|
|
287
289
|
): ProjectNeeds {
|
|
288
290
|
const programPackages = new Set<string>();
|
|
289
291
|
for (const name of project.program.getSourceFileNames()) {
|
|
@@ -329,7 +331,12 @@ export function projectNeeds<Brand>(
|
|
|
329
331
|
}
|
|
330
332
|
return names;
|
|
331
333
|
};
|
|
332
|
-
const configuration = configurationFiles(
|
|
334
|
+
const configuration = configurationFiles(
|
|
335
|
+
host,
|
|
336
|
+
targetRoot,
|
|
337
|
+
sourceFilesOf(project, targetRoot),
|
|
338
|
+
parse,
|
|
339
|
+
);
|
|
333
340
|
for (const { file } of configuration.modules) {
|
|
334
341
|
const names = declaredAt(file.fileName);
|
|
335
342
|
for (const { node, text } of moduleStrings(file)) {
|
|
@@ -342,6 +349,12 @@ export function projectNeeds<Brand>(
|
|
|
342
349
|
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
343
350
|
}
|
|
344
351
|
}
|
|
352
|
+
for (const { file, manifestDir } of configuration.outside) {
|
|
353
|
+
const names = declaredAt(joinPath(manifestDir, "package.json"));
|
|
354
|
+
for (const { text } of moduleStrings(file)) {
|
|
355
|
+
use([PROJECT_OWNER], dependenciesNamed(text, names));
|
|
356
|
+
}
|
|
357
|
+
}
|
|
345
358
|
return { packages, uses, unanswered };
|
|
346
359
|
}
|
|
347
360
|
|
package/src/destructuring.ts
CHANGED
|
@@ -25,7 +25,13 @@ import {
|
|
|
25
25
|
type Node,
|
|
26
26
|
} from "@typescript/native/unstable/ast";
|
|
27
27
|
import { TypeFlags, type Symbol as TSSymbol, type Type } from "@typescript/native/unstable/sync";
|
|
28
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
isAnswered,
|
|
30
|
+
propertyTables,
|
|
31
|
+
UNANSWERED,
|
|
32
|
+
type Answer,
|
|
33
|
+
type PropertyTables,
|
|
34
|
+
} from "./query.ts";
|
|
29
35
|
import type { ProjectView } from "./session.ts";
|
|
30
36
|
|
|
31
37
|
/** One property one object pattern reads. */
|
|
@@ -228,7 +234,11 @@ type Found = Answer<Type | undefined>;
|
|
|
228
234
|
* read once per type for the whole project, since one type is destructured in many
|
|
229
235
|
* places and a table answers every name of it.
|
|
230
236
|
*/
|
|
231
|
-
export function destructuring<Brand>(
|
|
237
|
+
export function destructuring<Brand>(
|
|
238
|
+
project: ProjectView<Brand>,
|
|
239
|
+
cap: number,
|
|
240
|
+
properties: PropertyTables = propertyTables(project.queries),
|
|
241
|
+
): Destructuring {
|
|
232
242
|
const queries = project.queries;
|
|
233
243
|
const tables = new Map<number, Answer<ReadonlyMap<string, TSSymbol>>>();
|
|
234
244
|
let patternBatches = 0;
|
|
@@ -241,10 +251,10 @@ export function destructuring<Brand>(project: ProjectView<Brand>, cap: number):
|
|
|
241
251
|
}
|
|
242
252
|
let table: Answer<ReadonlyMap<string, TSSymbol>> = new Map<string, TSSymbol>();
|
|
243
253
|
if ((type.flags & TypeFlags.AnyOrUnknown) === 0) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
table = isAnswered(properties)
|
|
247
|
-
? new Map(properties.map((property) => [property.name, property]))
|
|
254
|
+
const read = properties.of(type);
|
|
255
|
+
patternLookups += read.asked ? 1 : 0;
|
|
256
|
+
table = isAnswered(read.properties)
|
|
257
|
+
? new Map(read.properties.map((property) => [property.name, property]))
|
|
248
258
|
: UNANSWERED;
|
|
249
259
|
}
|
|
250
260
|
tables.set(type.id, table);
|
package/src/entry-points.ts
CHANGED
|
@@ -30,7 +30,12 @@ import {
|
|
|
30
30
|
type SourceFile,
|
|
31
31
|
} from "@typescript/native/unstable/ast";
|
|
32
32
|
import { SymbolFlags } from "@typescript/native/unstable/sync";
|
|
33
|
-
import {
|
|
33
|
+
import {
|
|
34
|
+
configurationFiles,
|
|
35
|
+
filesNamedBy,
|
|
36
|
+
moduleStrings,
|
|
37
|
+
type ParseFile,
|
|
38
|
+
} from "./configuration-files.ts";
|
|
34
39
|
import { globExpression } from "./glob.ts";
|
|
35
40
|
import type { Host } from "./host.ts";
|
|
36
41
|
import { dirnamePath, relativePath, resolvePath } from "./paths.ts";
|
|
@@ -260,8 +265,9 @@ function configurationEntries(
|
|
|
260
265
|
host: Host,
|
|
261
266
|
targetRoot: string,
|
|
262
267
|
files: SourceFiles,
|
|
268
|
+
parse: ParseFile | undefined,
|
|
263
269
|
): readonly EntryPoint[] {
|
|
264
|
-
const { modules, documents } = configurationFiles(host, targetRoot, files);
|
|
270
|
+
const { modules, documents, outside } = configurationFiles(host, targetRoot, files, parse);
|
|
265
271
|
const found: EntryPoint[] = [];
|
|
266
272
|
const strings = (dir: string, texts: readonly string[]): void => {
|
|
267
273
|
for (const text of texts) {
|
|
@@ -280,6 +286,12 @@ function configurationEntries(
|
|
|
280
286
|
for (const document of documents) {
|
|
281
287
|
strings(dirnamePath(document.path), document.strings);
|
|
282
288
|
}
|
|
289
|
+
for (const { file } of outside) {
|
|
290
|
+
strings(
|
|
291
|
+
dirnamePath(file.fileName),
|
|
292
|
+
moduleStrings(file).map((one) => one.text),
|
|
293
|
+
);
|
|
294
|
+
}
|
|
283
295
|
return found;
|
|
284
296
|
}
|
|
285
297
|
|
|
@@ -507,8 +519,9 @@ export function entryPoints<Brand>(
|
|
|
507
519
|
testFiles: readonly string[],
|
|
508
520
|
host: Host,
|
|
509
521
|
targetRoot: string,
|
|
522
|
+
parse?: ParseFile,
|
|
510
523
|
): readonly EntryPoint[] {
|
|
511
|
-
const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files)];
|
|
524
|
+
const found: EntryPoint[] = [...configurationEntries(host, targetRoot, files, parse)];
|
|
512
525
|
const tests = testFiles.map((pattern) => ({ pattern, expression: globExpression(pattern) }));
|
|
513
526
|
for (const [path, file] of files.byPath) {
|
|
514
527
|
const name = basename(path);
|
package/src/exempt-classes.ts
CHANGED
|
@@ -49,7 +49,12 @@ export const EXEMPTION_CLASSES = [
|
|
|
49
49
|
confidence: "certain",
|
|
50
50
|
typescriptVisibility: { private: false, privateName: false },
|
|
51
51
|
},
|
|
52
|
-
{
|
|
52
|
+
{
|
|
53
|
+
class: "generated-file",
|
|
54
|
+
languages: ["go", "ts"],
|
|
55
|
+
confidence: "certain",
|
|
56
|
+
typescriptVisibility: { private: true, privateName: false },
|
|
57
|
+
},
|
|
53
58
|
{ class: "linkname-cgo-asm-plugin", languages: ["go"], confidence: "certain" },
|
|
54
59
|
{
|
|
55
60
|
class: "template-field",
|
package/src/exempt.ts
CHANGED
|
@@ -48,6 +48,11 @@ export interface DetectorInput<Brand> {
|
|
|
48
48
|
readonly ts: TSSection;
|
|
49
49
|
/** The absolute path of each declared consumer, whose modules are part of the analysis. */
|
|
50
50
|
readonly consumers: readonly string[];
|
|
51
|
+
/**
|
|
52
|
+
* The project's generated files, by path below the target root, each with the
|
|
53
|
+
* convention row that names its directory as generated.
|
|
54
|
+
*/
|
|
55
|
+
readonly generated?: ReadonlyMap<string, string>;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
/**
|
|
@@ -162,8 +167,29 @@ export function computeExemptions<Brand>(
|
|
|
162
167
|
detectors: Detectors,
|
|
163
168
|
holding: Holding,
|
|
164
169
|
): readonly Exemption[] {
|
|
170
|
+
return detectExemptions(input, detectors, holding).records;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** One project's records, and the evidence a production mode found in a test file. */
|
|
174
|
+
export interface Detected {
|
|
175
|
+
readonly records: readonly Exemption[];
|
|
176
|
+
/**
|
|
177
|
+
* Under a production mode, each record whose evidence is written in a test file. It
|
|
178
|
+
* holds nothing, and it is a test reference to its declaration, as a reference the
|
|
179
|
+
* test file wrote would be.
|
|
180
|
+
*/
|
|
181
|
+
readonly inTestFiles: readonly Exemption[];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The records of {@link computeExemptions}, beside the test-file evidence it set aside. */
|
|
185
|
+
export function detectExemptions<Brand>(
|
|
186
|
+
input: DetectorInput<Brand>,
|
|
187
|
+
detectors: Detectors,
|
|
188
|
+
holding: Holding,
|
|
189
|
+
): Detected {
|
|
165
190
|
const visibility = new Map(input.held.symbols.map((symbol) => [symbol.id, symbol.visibility]));
|
|
166
191
|
const found: Exemption[] = [];
|
|
192
|
+
const inTestFiles: Exemption[] = [];
|
|
167
193
|
for (const exemptionClass of TS_EXEMPTION_CLASSES) {
|
|
168
194
|
const detect = detectors.get(exemptionClass);
|
|
169
195
|
if (detect === undefined || holding.disabled.has(exemptionClass)) {
|
|
@@ -174,19 +200,21 @@ export function computeExemptions<Brand>(
|
|
|
174
200
|
if (!retainable(allowed, visibility.get(evidence.id))) {
|
|
175
201
|
continue;
|
|
176
202
|
}
|
|
177
|
-
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
found.push({
|
|
203
|
+
const record = {
|
|
181
204
|
id: evidence.id,
|
|
182
205
|
class: exemptionClass,
|
|
183
206
|
detail: evidence.detail,
|
|
184
207
|
site: evidence.site,
|
|
185
208
|
...(evidence.whileLive === undefined ? {} : { whileLive: evidence.whileLive }),
|
|
186
|
-
}
|
|
209
|
+
};
|
|
210
|
+
if (holding.mode.production && holding.testFiles.has(evidence.site.path)) {
|
|
211
|
+
inTestFiles.push(record);
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
found.push(record);
|
|
187
215
|
}
|
|
188
216
|
}
|
|
189
|
-
return exemptionsOf(found);
|
|
217
|
+
return { records: exemptionsOf(found), inTestFiles };
|
|
190
218
|
}
|
|
191
219
|
|
|
192
220
|
/** Two strings ordered bytewise. */
|
|
@@ -247,17 +275,18 @@ function byDeclaration(records: readonly Exemption[]): ReadonlyMap<string, reado
|
|
|
247
275
|
}
|
|
248
276
|
|
|
249
277
|
/**
|
|
250
|
-
* The records that held back a declaration some configuration
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
254
|
-
*
|
|
255
|
-
*
|
|
256
|
-
* on a declaration that a relation or a mark holds live in every configuration held
|
|
257
|
-
* nothing back. The order is the declarations' site order, and for one declaration
|
|
258
|
-
* the input's.
|
|
278
|
+
* The records that held back a declaration: one some configuration's graph, swept
|
|
279
|
+
* again with no exemption and its marks, judges a candidate, whatever another
|
|
280
|
+
* configuration answers, or one in `writeOnly`, which the run would report as written
|
|
281
|
+
* and never read without the record's read. A declaration a relation or a mark holds
|
|
282
|
+
* live in every configuration is held back by no record. The order is the
|
|
283
|
+
* declarations' site order, and for one declaration the input's.
|
|
259
284
|
*/
|
|
260
|
-
export function retainedIn(
|
|
285
|
+
export function retainedIn(
|
|
286
|
+
matrix: Matrix,
|
|
287
|
+
input: SweepInput,
|
|
288
|
+
writeOnly: ReadonlySet<string> = new Set(),
|
|
289
|
+
): readonly Exemption[] {
|
|
261
290
|
const exempt = input.exempt ?? [];
|
|
262
291
|
if (exempt.length === 0) {
|
|
263
292
|
return [];
|
|
@@ -268,7 +297,9 @@ export function retainedIn(matrix: Matrix, input: SweepInput): readonly Exemptio
|
|
|
268
297
|
sweep(graph, bare).candidates.map((candidate) => candidate.id),
|
|
269
298
|
),
|
|
270
299
|
);
|
|
271
|
-
const held = byDeclaration(
|
|
300
|
+
const held = byDeclaration(
|
|
301
|
+
exempt.filter((record) => dead.has(record.id) || writeOnly.has(record.id)),
|
|
302
|
+
);
|
|
272
303
|
return matrix.union.symbols.flatMap((symbol) => held.get(symbol.id) ?? []);
|
|
273
304
|
}
|
|
274
305
|
|
package/src/file-facts.ts
CHANGED
|
@@ -47,6 +47,8 @@ export interface FileFacts {
|
|
|
47
47
|
* reference names is included by the file that names it.
|
|
48
48
|
*/
|
|
49
49
|
readonly heldByInclusion: ReadonlySet<string>;
|
|
50
|
+
/** The paths below the target root of the files below a directory an applied row names as generated. */
|
|
51
|
+
readonly generated?: ReadonlySet<string>;
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
/** The line of the last character of one text, counted from one; an empty text is one line. */
|
|
@@ -146,8 +146,9 @@ export function completed(
|
|
|
146
146
|
const classOf = reachabilityClasses(input);
|
|
147
147
|
const componentOf = findingComponents(swept, input.config.reporters.cascade);
|
|
148
148
|
let minted = swept.sweep.components.length;
|
|
149
|
+
const dead = new Set(swept.sweep.components.map((component) => component.id));
|
|
149
150
|
|
|
150
|
-
|
|
151
|
+
const done = findings.map((finding): CompletedFinding => {
|
|
151
152
|
const row = kinds.get(finding.code);
|
|
152
153
|
if (row === undefined) {
|
|
153
154
|
throw new Error(`${finding.code} names no live row of the issue-kind vocabulary`);
|
|
@@ -183,6 +184,7 @@ export function completed(
|
|
|
183
184
|
? union.made[at]
|
|
184
185
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
185
186
|
const reachabilityClass = judged === undefined ? "certain" : classOf(judged.id);
|
|
187
|
+
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
186
188
|
return {
|
|
187
189
|
code: finding.code,
|
|
188
190
|
kind: row.name,
|
|
@@ -193,7 +195,7 @@ export function completed(
|
|
|
193
195
|
confidence: capped(reachabilityClass, row.maxClass),
|
|
194
196
|
...(candidate === undefined ? {} : { livenessRelation: candidate.relation }),
|
|
195
197
|
testOnly: judged !== undefined && counts?.production === 0 && counts.test > 0,
|
|
196
|
-
generated
|
|
198
|
+
generated,
|
|
197
199
|
component,
|
|
198
200
|
retainedBy: [],
|
|
199
201
|
configurations:
|
|
@@ -202,12 +204,38 @@ export function completed(
|
|
|
202
204
|
? swept.matrix.configurations
|
|
203
205
|
: (swept.matrix.heldIn[at] ?? swept.matrix.configurations)),
|
|
204
206
|
consumersLoaded: input.boundary.consumers.loaded,
|
|
205
|
-
fixability: row.fixability,
|
|
207
|
+
fixability: generated ? "none" : row.fixability,
|
|
206
208
|
severity: severityOf(input, finding.code, row.defaultSeverity),
|
|
207
209
|
message: finding.message,
|
|
208
210
|
details: finding.details ?? {},
|
|
209
211
|
};
|
|
210
212
|
});
|
|
213
|
+
return componentCapped(done, dead);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The findings with every finding of a dead component capped by the lowest confidence
|
|
218
|
+
* among the component's root members, so the minimum confidence reports or withholds a
|
|
219
|
+
* component whole.
|
|
220
|
+
*/
|
|
221
|
+
function componentCapped(
|
|
222
|
+
findings: readonly CompletedFinding[],
|
|
223
|
+
dead: ReadonlySet<string>,
|
|
224
|
+
): readonly CompletedFinding[] {
|
|
225
|
+
const lowest = new Map<string, Confidence>();
|
|
226
|
+
for (const finding of findings) {
|
|
227
|
+
const { id, root } = finding.component;
|
|
228
|
+
if (root && dead.has(id)) {
|
|
229
|
+
const held = lowest.get(id);
|
|
230
|
+
lowest.set(id, held === undefined ? finding.confidence : capped(held, finding.confidence));
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return findings.map((finding) => {
|
|
234
|
+
const cap = lowest.get(finding.component.id);
|
|
235
|
+
return cap === undefined || RANK[cap] >= RANK[finding.confidence]
|
|
236
|
+
? finding
|
|
237
|
+
: { ...finding, confidence: cap };
|
|
238
|
+
});
|
|
211
239
|
}
|
|
212
240
|
|
|
213
241
|
/** Whether the dials withhold a finding by its own severity or confidence. */
|
|
@@ -31,6 +31,7 @@ function compare(a: string, b: string): number {
|
|
|
31
31
|
function indexed(input: EmitterInput): {
|
|
32
32
|
readonly symbolOf: (id: string) => InventorySymbol | undefined;
|
|
33
33
|
readonly inTest: (id: string) => boolean;
|
|
34
|
+
readonly inSupport: (id: string) => boolean;
|
|
34
35
|
readonly candidateOf: (id: string) => Candidate | undefined;
|
|
35
36
|
readonly implementationsOf: (id: string) => readonly PositionedSymbol[];
|
|
36
37
|
} {
|
|
@@ -43,6 +44,7 @@ function indexed(input: EmitterInput): {
|
|
|
43
44
|
return {
|
|
44
45
|
symbolOf,
|
|
45
46
|
inTest: (id) => union.test[union.at(id)] === true,
|
|
47
|
+
inSupport: (id) => union.support[union.at(id)] === true,
|
|
46
48
|
candidateOf: (id) => candidates.get(id),
|
|
47
49
|
implementationsOf: (id) =>
|
|
48
50
|
[...(input.implementations.classes.get(id) ?? [])]
|
|
@@ -80,14 +82,19 @@ function findingOf(
|
|
|
80
82
|
* interface, whose members fall with it unreported; and `DS1203`, a method of a live
|
|
81
83
|
* interface no reference names, unless it is a marker, a method whose every
|
|
82
84
|
* implementation has an empty body. A test file's declaration is neither kind's
|
|
83
|
-
* subject, because a production sweep counts none of the references it can have
|
|
85
|
+
* subject, because a production sweep counts none of the references it can have, and
|
|
86
|
+
* neither is a test-support declaration test code references, which that code keeps live.
|
|
84
87
|
*/
|
|
85
88
|
export const interfaces: Emitter = (input) => {
|
|
86
89
|
const run = indexed(input);
|
|
87
90
|
const found: Finding[] = [];
|
|
88
91
|
for (const candidate of input.swept.sweep.candidates) {
|
|
89
92
|
const symbol = run.symbolOf(candidate.id);
|
|
90
|
-
if (
|
|
93
|
+
if (
|
|
94
|
+
symbol === undefined ||
|
|
95
|
+
run.inTest(candidate.id) ||
|
|
96
|
+
(run.inSupport(candidate.id) && candidate.testRefs > 0)
|
|
97
|
+
) {
|
|
91
98
|
continue;
|
|
92
99
|
}
|
|
93
100
|
if (symbol.kind === "interface") {
|
|
@@ -62,23 +62,76 @@ function findingAbout(
|
|
|
62
62
|
interface Uses {
|
|
63
63
|
readonly writes: FindingPosition[];
|
|
64
64
|
reads: number;
|
|
65
|
+
/** The import bindings only the writes use. */
|
|
66
|
+
readonly alone: Set<string>;
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
/**
|
|
68
70
|
* Whether one declaration is a subject of the write-only kind: a module-level variable,
|
|
69
|
-
* or a class member that is state rather than code. A getter
|
|
70
|
-
* store into one runs it.
|
|
71
|
+
* a member of an object type, or a class member that is state rather than code. A getter
|
|
72
|
+
* or a setter is code, so a store into one runs it.
|
|
71
73
|
*/
|
|
72
74
|
function writable(run: Run, symbol: InventorySymbol): boolean {
|
|
73
|
-
if (symbol.kind === "variable") {
|
|
75
|
+
if (symbol.kind === "variable" || symbol.kind === "type-member") {
|
|
74
76
|
return true;
|
|
75
77
|
}
|
|
76
78
|
return symbol.kind === "class-member" && !run.input.stores.accessors.has(symbol.id);
|
|
77
79
|
}
|
|
78
80
|
|
|
81
|
+
/** The reader's word for each kind of the write-only kind's subject. */
|
|
82
|
+
const SUBJECT_WORDS: Readonly<Partial<Record<InventorySymbol["kind"], string>>> = {
|
|
83
|
+
variable: "variable",
|
|
84
|
+
"class-member": "member",
|
|
85
|
+
"type-member": "type member",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* The type parameters of one member's declaring type that only the member names, which
|
|
90
|
+
* deleting the member leaves unused.
|
|
91
|
+
*/
|
|
92
|
+
function parametersOnlyNamedBy(
|
|
93
|
+
run: Run,
|
|
94
|
+
namedBy: ReadonlyMap<string, ReadonlySet<string>>,
|
|
95
|
+
member: InventorySymbol,
|
|
96
|
+
): readonly string[] {
|
|
97
|
+
return [...run.symbols.values()]
|
|
98
|
+
.filter((symbol) => {
|
|
99
|
+
const from = namedBy.get(symbol.id);
|
|
100
|
+
return (
|
|
101
|
+
symbol.kind === "type-parameter" &&
|
|
102
|
+
symbol.parent === member.parent &&
|
|
103
|
+
from?.size === 1 &&
|
|
104
|
+
from.has(member.id)
|
|
105
|
+
);
|
|
106
|
+
})
|
|
107
|
+
.map((symbol) => symbol.name);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** What one write-only finding says, the declarations its deletion takes with it included. */
|
|
111
|
+
function writeOnlyMessage(
|
|
112
|
+
run: Run,
|
|
113
|
+
namedBy: ReadonlyMap<string, ReadonlySet<string>>,
|
|
114
|
+
symbol: InventorySymbol,
|
|
115
|
+
held: Uses,
|
|
116
|
+
): string {
|
|
117
|
+
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
118
|
+
const read = run.input.stores.readInTests.has(symbol.id)
|
|
119
|
+
? "read only from test files"
|
|
120
|
+
: "never read";
|
|
121
|
+
const cascade = [
|
|
122
|
+
...parametersOnlyNamedBy(run, namedBy, symbol).map((name) => `type parameter ${name}`),
|
|
123
|
+
...[...held.alone].sort().map((name) => `import ${name}`),
|
|
124
|
+
];
|
|
125
|
+
const word = SUBJECT_WORDS[symbol.kind] ?? "member";
|
|
126
|
+
const message = `${word} ${symbol.name} is written ${writes} and ${read}`;
|
|
127
|
+
return cascade.length === 0
|
|
128
|
+
? message
|
|
129
|
+
: `${message}, and deleting it with its writes deletes ${cascade.join(" and ")} too`;
|
|
130
|
+
}
|
|
131
|
+
|
|
79
132
|
/**
|
|
80
|
-
* `DS1301`: a module-level variable or a class member the counted
|
|
81
|
-
* and never read, named with every position written.
|
|
133
|
+
* `DS1301`: a module-level variable, a type member or a class member the counted
|
|
134
|
+
* references store into and never read, named with every position written.
|
|
82
135
|
*
|
|
83
136
|
* The subject is a live declaration: a store is a reference, so the sweep holds a
|
|
84
137
|
* written one live and its candidates are never this kind's. Every reference that is
|
|
@@ -91,7 +144,7 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
91
144
|
for (const reference of stores.references) {
|
|
92
145
|
let held = uses.get(reference.to);
|
|
93
146
|
if (held === undefined) {
|
|
94
|
-
held = { writes: [], reads: 0 };
|
|
147
|
+
held = { writes: [], reads: 0, alone: new Set() };
|
|
95
148
|
uses.set(reference.to, held);
|
|
96
149
|
}
|
|
97
150
|
// A consumer's store is written at a position no report of the target can name, so
|
|
@@ -100,6 +153,9 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
100
153
|
held.reads += 1;
|
|
101
154
|
continue;
|
|
102
155
|
}
|
|
156
|
+
for (const name of reference.alone ?? []) {
|
|
157
|
+
held.alone.add(name);
|
|
158
|
+
}
|
|
103
159
|
const at = reference.position;
|
|
104
160
|
const key = positionKey(at);
|
|
105
161
|
if (!held.writes.some((written) => positionKey(written) === key)) {
|
|
@@ -107,6 +163,13 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
107
163
|
}
|
|
108
164
|
}
|
|
109
165
|
|
|
166
|
+
const namedBy = new Map<string, Set<string>>();
|
|
167
|
+
for (const reference of stores.references) {
|
|
168
|
+
if (run.symbols.get(reference.to)?.kind === "type-parameter") {
|
|
169
|
+
namedBy.set(reference.to, (namedBy.get(reference.to) ?? new Set()).add(reference.from));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
110
173
|
const found: Finding[] = [];
|
|
111
174
|
for (const symbol of matrix.union.symbols) {
|
|
112
175
|
const held = uses.get(symbol.id);
|
|
@@ -120,10 +183,8 @@ function writeOnlySymbols(run: Run): readonly Finding[] {
|
|
|
120
183
|
) {
|
|
121
184
|
continue;
|
|
122
185
|
}
|
|
123
|
-
const writes = held.writes.length === 1 ? "once" : `at ${String(held.writes.length)} positions`;
|
|
124
|
-
const word = symbol.kind === "variable" ? "variable" : "member";
|
|
125
186
|
found.push(
|
|
126
|
-
findingAbout(symbol, "DS1301",
|
|
187
|
+
findingAbout(symbol, "DS1301", writeOnlyMessage(run, namedBy, symbol, held), {
|
|
127
188
|
writePositions: held.writes,
|
|
128
189
|
}),
|
|
129
190
|
);
|
|
@@ -142,6 +142,52 @@ function messageOf(code: string, judged: Judged): string {
|
|
|
142
142
|
return `${subject} has no reference in the target`;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
+
/**
|
|
146
|
+
* The members nothing writes or reads that are brands: each names a type parameter of
|
|
147
|
+
* its declaring type that no other part of the declaration names, other than another
|
|
148
|
+
* such member. They are what make two instantiations of the type distinct types, so
|
|
149
|
+
* deleting them would let one instantiation stand for another.
|
|
150
|
+
*/
|
|
151
|
+
function brandMembers(input: EmitterInput): ReadonlySet<string> {
|
|
152
|
+
const union = input.swept.matrix.union;
|
|
153
|
+
const unreferenced = new Map(
|
|
154
|
+
input.swept.sweep.candidates
|
|
155
|
+
.filter((candidate) => candidate.productionRefs + candidate.testRefs === 0)
|
|
156
|
+
.map((candidate) => [candidate.id, candidate]),
|
|
157
|
+
);
|
|
158
|
+
const namedBy = new Map<string, Set<string>>();
|
|
159
|
+
for (const reference of input.stores.references) {
|
|
160
|
+
const to = union.symbols[union.at(reference.to)];
|
|
161
|
+
if (to?.kind === "type-parameter") {
|
|
162
|
+
namedBy.set(to.id, (namedBy.get(to.id) ?? new Set()).add(reference.from));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const brands = new Set<string>();
|
|
166
|
+
for (const [parameter, from] of namedBy) {
|
|
167
|
+
const owner = union.symbols[union.at(parameter)]?.parent;
|
|
168
|
+
const members = [...from].map((id) => union.symbols[union.at(id)]);
|
|
169
|
+
if (
|
|
170
|
+
members.every(
|
|
171
|
+
(member) =>
|
|
172
|
+
member !== undefined &&
|
|
173
|
+
member.parent === owner &&
|
|
174
|
+
BRANDS.has(member.kind) &&
|
|
175
|
+
unreferenced.has(member.id),
|
|
176
|
+
)
|
|
177
|
+
) {
|
|
178
|
+
for (const member of members) {
|
|
179
|
+
if (member !== undefined) {
|
|
180
|
+
brands.add(member.id);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return brands;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/** The kinds of member a brand is: a data member of a type or of a class. */
|
|
189
|
+
const BRANDS: ReadonlySet<SymbolKind> = new Set(["type-member", "class-member"]);
|
|
190
|
+
|
|
145
191
|
/** The findings of the unused-declarations family, `DS1000` to `DS1099`. */
|
|
146
192
|
export const unusedDeclarations: Emitter = (input) => familyFindings(input, false);
|
|
147
193
|
|
|
@@ -157,6 +203,7 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
157
203
|
const union = input.swept.matrix.union;
|
|
158
204
|
const dead = new Set(input.swept.sweep.candidates.map((candidate) => candidate.id));
|
|
159
205
|
const unreachable = unreachableExports(input);
|
|
206
|
+
let brands: ReadonlySet<string> | undefined;
|
|
160
207
|
|
|
161
208
|
const found: Finding[] = [];
|
|
162
209
|
for (const candidate of input.swept.sweep.candidates) {
|
|
@@ -179,7 +226,10 @@ function familyFindings(input: EmitterInput, fallen: boolean): Finding[] {
|
|
|
179
226
|
unreachable: unreachable.has(symbol.id),
|
|
180
227
|
};
|
|
181
228
|
const code = codeOf(judged);
|
|
182
|
-
if (
|
|
229
|
+
if (
|
|
230
|
+
code === undefined ||
|
|
231
|
+
(code === "DS1003" && (brands ??= brandMembers(input)).has(symbol.id))
|
|
232
|
+
) {
|
|
183
233
|
continue;
|
|
184
234
|
}
|
|
185
235
|
const endLine = Math.max(symbol.position.line, symbol.endLine);
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The generated-file class on TypeScript: every declaration in a file below a directory
|
|
3
|
+
* an applied convention row names as generated is retained, because the framework owns
|
|
4
|
+
* the file and a deletion there is undone at the next generation.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DetectorInput, Evidence } from "./exempt.ts";
|
|
8
|
+
|
|
9
|
+
/** The generated-file detector, recording each file's first line. */
|
|
10
|
+
export function generatedFile<Brand>(input: DetectorInput<Brand>): readonly Evidence[] {
|
|
11
|
+
const generated = input.generated;
|
|
12
|
+
if (generated === undefined || generated.size === 0) {
|
|
13
|
+
return [];
|
|
14
|
+
}
|
|
15
|
+
return input.held.symbols.flatMap((symbol): Evidence[] => {
|
|
16
|
+
const row = generated.get(symbol.position.path);
|
|
17
|
+
return symbol.kind === "file" || row === undefined
|
|
18
|
+
? []
|
|
19
|
+
: [
|
|
20
|
+
{
|
|
21
|
+
id: symbol.id,
|
|
22
|
+
detail: `generated below a directory the convention row ${row} names`,
|
|
23
|
+
site: { path: symbol.position.path, line: 1, column: 1 },
|
|
24
|
+
},
|
|
25
|
+
];
|
|
26
|
+
});
|
|
27
|
+
}
|