@cplieger/deadset-ts 6.1.1-dev.1 → 7.0.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/analysis.js +39 -16
- package/dist/src/conformance-record.js +2 -2
- package/dist/src/contextual-uses.js +27 -3
- package/dist/src/convention-rows.js +42 -0
- package/dist/src/conventions.js +48 -0
- package/dist/src/dependencies.js +109 -38
- package/dist/src/findings/completion.js +6 -7
- package/dist/src/findings/dependencies-and-module-machinery.js +6 -26
- package/dist/src/html-entries.js +29 -14
- package/dist/src/indexed-access.js +371 -81
- package/dist/src/intra-function-parts.js +57 -11
- package/dist/src/kinds.js +6 -6
- package/dist/src/references.js +60 -21
- package/dist/src/roots.js +96 -6
- package/dist/src/session.js +6 -1
- package/dist/src/source-files.js +1 -0
- package/dist/src/sweep.js +29 -17
- package/dist/src/symbol-keys.js +114 -0
- package/dist/src/test-joins.js +101 -0
- package/dist/src/version.js +1 -1
- package/dist/src/workflow-steps.js +21 -15
- package/dist/src/workspace-programs.js +1 -1
- package/jsr.json +1 -1
- package/package.json +1 -1
- package/src/analysis.ts +45 -17
- package/src/conformance-record.ts +2 -2
- package/src/contextual-uses.ts +36 -4
- package/src/convention-rows.ts +57 -0
- package/src/conventions.ts +59 -0
- package/src/dependencies.ts +134 -46
- package/src/finding.ts +2 -0
- package/src/findings/completion.ts +7 -7
- package/src/findings/dependencies-and-module-machinery.ts +9 -28
- package/src/html-entries.ts +33 -15
- package/src/indexed-access.ts +468 -102
- package/src/intra-function-parts.ts +84 -12
- package/src/kinds.ts +7 -6
- package/src/references.ts +86 -27
- package/src/roots.ts +129 -6
- package/src/session.ts +7 -1
- package/src/source-files.ts +3 -0
- package/src/sweep.ts +29 -17
- package/src/symbol-keys.ts +172 -0
- package/src/test-joins.ts +120 -0
- package/src/version.ts +1 -1
- package/src/workflow-steps.ts +23 -16
- package/src/workspace-programs.ts +1 -1
package/src/conventions.ts
CHANGED
|
@@ -53,6 +53,7 @@ import {
|
|
|
53
53
|
type HelperReading,
|
|
54
54
|
type MoveReading,
|
|
55
55
|
type OptionsCall,
|
|
56
|
+
type PackageKey,
|
|
56
57
|
type ShortNameReading,
|
|
57
58
|
} from "./convention-rows.ts";
|
|
58
59
|
import { documentStrings } from "./configuration-files.ts";
|
|
@@ -873,6 +874,60 @@ function helperUses(
|
|
|
873
874
|
.map((reading) => reading.package);
|
|
874
875
|
}
|
|
875
876
|
|
|
877
|
+
/**
|
|
878
|
+
* The dependencies one manifest declares that an applied row's package keys use: each
|
|
879
|
+
* whose name a string under a member of a key's name equals, or begins followed by `/` or
|
|
880
|
+
* by `:` and a name. A member's own name is no such string.
|
|
881
|
+
*/
|
|
882
|
+
function packageKeyUses(
|
|
883
|
+
host: Host,
|
|
884
|
+
parse: ParseSource,
|
|
885
|
+
dir: string,
|
|
886
|
+
manifest: unknown,
|
|
887
|
+
keys: readonly PackageKey[],
|
|
888
|
+
): readonly string[] {
|
|
889
|
+
const declared = DEPENDENCY_SECTIONS.flatMap((section) => {
|
|
890
|
+
const held = isRecord(manifest) ? manifest[section] : undefined;
|
|
891
|
+
return isRecord(held) ? Object.keys(held) : [];
|
|
892
|
+
});
|
|
893
|
+
const strings: string[] = [];
|
|
894
|
+
const collect = (node: Node): void => {
|
|
895
|
+
const text = literalText(node);
|
|
896
|
+
if (text !== undefined) {
|
|
897
|
+
strings.push(text);
|
|
898
|
+
} else if (isPropertyAssignment(node)) {
|
|
899
|
+
collect(node.initializer);
|
|
900
|
+
} else {
|
|
901
|
+
node.forEachChild(collect);
|
|
902
|
+
}
|
|
903
|
+
};
|
|
904
|
+
for (const { key, files } of keys) {
|
|
905
|
+
const visit = (node: Node): void => {
|
|
906
|
+
if (isPropertyAssignment(node) && keyOf(node) === key) {
|
|
907
|
+
collect(node.initializer);
|
|
908
|
+
}
|
|
909
|
+
node.forEachChild(visit);
|
|
910
|
+
};
|
|
911
|
+
for (const { file } of readingFiles(host, parse, dir, files)) {
|
|
912
|
+
const values = declaredValues(file);
|
|
913
|
+
for (const one of defaultExports(file)) {
|
|
914
|
+
const object = objectOf(one, values, true);
|
|
915
|
+
if (object !== undefined) {
|
|
916
|
+
visit(object);
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
return declared.filter((name) =>
|
|
922
|
+
strings.some(
|
|
923
|
+
(text) =>
|
|
924
|
+
text === name ||
|
|
925
|
+
text.startsWith(`${name}/`) ||
|
|
926
|
+
(text.startsWith(`${name}:`) && text.length > name.length + 1),
|
|
927
|
+
),
|
|
928
|
+
);
|
|
929
|
+
}
|
|
930
|
+
|
|
876
931
|
/** Two strings ordered bytewise. */
|
|
877
932
|
function compare(a: string, b: string): number {
|
|
878
933
|
if (a === b) {
|
|
@@ -959,6 +1014,10 @@ export function readConventions(
|
|
|
959
1014
|
for (const name of shortNameUses(host, parse, manifest.dir, declared, row.shortNames ?? [])) {
|
|
960
1015
|
uses.add(name);
|
|
961
1016
|
}
|
|
1017
|
+
const keys = row.packageKeys ?? [];
|
|
1018
|
+
for (const name of packageKeyUses(host, parse, manifest.dir, declared, keys)) {
|
|
1019
|
+
uses.add(name);
|
|
1020
|
+
}
|
|
962
1021
|
for (const name of manifestKeyUses(declared, row.manifestKeys ?? [])) {
|
|
963
1022
|
uses.add(name);
|
|
964
1023
|
}
|
package/src/dependencies.ts
CHANGED
|
@@ -1,19 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The dependencies the target's manifest and its workspace members' manifests declare, and
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* file the specifier resolves to; a compiler configuration needs one it names, and a
|
|
6
|
-
*
|
|
7
|
-
*
|
|
2
|
+
* The dependencies the target's manifest and its workspace members' manifests declare, and what the
|
|
3
|
+
* projects' own files need from outside the target. A file needs a package it imports, re-exports
|
|
4
|
+
* from, augments, references the types of or asks the runtime to resolve, by name or through the
|
|
5
|
+
* file the specifier resolves to; a compiler configuration needs one it names, and a configuration
|
|
6
|
+
* file a tool loads needs each dependency its strings spell. A package one dependency pulls in for
|
|
7
|
+
* itself is that dependency's need, not the target's.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import {
|
|
11
|
+
isCallExpression,
|
|
11
12
|
isExportDeclaration,
|
|
12
13
|
isExternalModuleReference,
|
|
13
14
|
isIdentifier,
|
|
14
15
|
isImportDeclaration,
|
|
15
16
|
isImportEqualsDeclaration,
|
|
17
|
+
isMetaProperty,
|
|
16
18
|
isNamespaceExport,
|
|
19
|
+
isPropertyAccessExpression,
|
|
17
20
|
isShorthandPropertyAssignment,
|
|
18
21
|
isStringLiteral,
|
|
19
22
|
isNoSubstitutionTemplateLiteral,
|
|
@@ -32,11 +35,13 @@ import {
|
|
|
32
35
|
import type { FindingPosition } from "./finding.ts";
|
|
33
36
|
import type { Host } from "./host.ts";
|
|
34
37
|
import { nodeKey, packageScope, type Inventory } from "./inventory.ts";
|
|
38
|
+
import { scriptTokens } from "./manifest.ts";
|
|
35
39
|
import { dirnamePath, isAbsolutePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
36
40
|
import type { DependencySection, Module } from "./ref.ts";
|
|
37
41
|
import { UNANSWERED } from "./query.ts";
|
|
38
42
|
import type { Handle, ProjectView } from "./session.ts";
|
|
39
43
|
import { sourceFilesOf } from "./source-files.ts";
|
|
44
|
+
import { workflowTokens } from "./workflow-steps.ts";
|
|
40
45
|
|
|
41
46
|
const MANIFEST = "package.json";
|
|
42
47
|
const MODULES_DIR = "node_modules";
|
|
@@ -68,16 +73,14 @@ export function configurationNeeds(packages: readonly string[]): ProjectNeeds {
|
|
|
68
73
|
return {
|
|
69
74
|
packages: new Set(packages),
|
|
70
75
|
uses: new Map([[PROJECT_OWNER, new Set(packages)]]),
|
|
71
|
-
unanswered:
|
|
76
|
+
unanswered: [],
|
|
72
77
|
};
|
|
73
78
|
}
|
|
74
79
|
|
|
75
80
|
/** What the installed copy of one declared dependency states about itself. */
|
|
76
81
|
export interface InstalledDependency {
|
|
77
|
-
/**
|
|
78
|
-
readonly
|
|
79
|
-
/** The peer dependencies its manifest requires: every peer not marked optional. */
|
|
80
|
-
readonly peers: readonly string[];
|
|
82
|
+
/** The commands its manifest's `bin` declares, by name. */
|
|
83
|
+
readonly commands: readonly string[];
|
|
81
84
|
}
|
|
82
85
|
|
|
83
86
|
/** What one project's own files and configuration need from outside the target. */
|
|
@@ -91,20 +94,29 @@ export interface ProjectNeeds {
|
|
|
91
94
|
*/
|
|
92
95
|
readonly uses: ReadonlyMap<string, ReadonlySet<string>>;
|
|
93
96
|
/**
|
|
94
|
-
*
|
|
95
|
-
* dependency
|
|
97
|
+
* The files writing a module specifier the checker left unanswered, each of which may
|
|
98
|
+
* use any dependency a manifest at or above it declares.
|
|
96
99
|
*/
|
|
97
|
-
readonly unanswered:
|
|
100
|
+
readonly unanswered: readonly string[];
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
/** The run's dependency evidence. */
|
|
101
104
|
export interface Dependencies {
|
|
102
105
|
/** Every declared dependency, manifest by manifest, in the order each writes them. */
|
|
103
106
|
readonly declared: readonly DeclaredDependency[];
|
|
104
|
-
/** The installed copy of each declared dependency the run found, by name. */
|
|
105
|
-
readonly installed: ReadonlyMap<string, InstalledDependency>;
|
|
106
107
|
/** Every package some project needs. */
|
|
107
108
|
readonly needed: ReadonlySet<string>;
|
|
109
|
+
/**
|
|
110
|
+
* The declared dependencies whose command a script entry of the manifest declaring them,
|
|
111
|
+
* or a workflow step for the target's manifest, runs, each by {@link declarationKey}.
|
|
112
|
+
*/
|
|
113
|
+
readonly ran: ReadonlySet<string>;
|
|
114
|
+
/**
|
|
115
|
+
* The manifests, by their path below the target root, whose directory holds source only
|
|
116
|
+
* in configurations the run could not build, so a dependency they declare that nothing
|
|
117
|
+
* needs is reported at `possible`.
|
|
118
|
+
*/
|
|
119
|
+
readonly unbuilt: ReadonlySet<string>;
|
|
108
120
|
/**
|
|
109
121
|
* Per deletion candidate, the declared dependencies whose last use in the target is
|
|
110
122
|
* written inside it, sorted bytewise; a candidate holding no last use has no entry.
|
|
@@ -325,9 +337,11 @@ export function projectNeeds<Brand>(
|
|
|
325
337
|
};
|
|
326
338
|
use([PROJECT_OWNER], packages);
|
|
327
339
|
|
|
328
|
-
|
|
340
|
+
const unanswered: string[] = [];
|
|
329
341
|
for (const file of project.ownSourceFiles()) {
|
|
330
|
-
|
|
342
|
+
if (fileNeeds(project, held, file, programPackages, use)) {
|
|
343
|
+
unanswered.push(file.fileName);
|
|
344
|
+
}
|
|
331
345
|
}
|
|
332
346
|
|
|
333
347
|
const beside = new Map<string, readonly string[]>();
|
|
@@ -491,9 +505,43 @@ function fileNeeds<Brand>(
|
|
|
491
505
|
for (const reference of file.typeReferenceDirectives) {
|
|
492
506
|
use(ownersOf(file, held, file), typePackages(reference.fileName, programPackages));
|
|
493
507
|
}
|
|
508
|
+
for (const specifier of resolvedSpecifiers(file)) {
|
|
509
|
+
const named = packageOfSpecifier(specifier.text);
|
|
510
|
+
if (named !== undefined) {
|
|
511
|
+
use(ownersOf(file, held, specifier), [named]);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
494
514
|
return resolved.slice(0, specifiers.length).includes(UNANSWERED);
|
|
495
515
|
}
|
|
496
516
|
|
|
517
|
+
/**
|
|
518
|
+
* The literal specifiers one file asks the runtime to resolve, through `import.meta.resolve`
|
|
519
|
+
* or `require.resolve`, each of which needs the package it names installed.
|
|
520
|
+
*/
|
|
521
|
+
function resolvedSpecifiers(file: SourceFile): readonly Texted[] {
|
|
522
|
+
const found: Texted[] = [];
|
|
523
|
+
if (!file.text.includes(".resolve(")) {
|
|
524
|
+
return found;
|
|
525
|
+
}
|
|
526
|
+
const visit = (node: Node): void => {
|
|
527
|
+
if (isCallExpression(node) && isPropertyAccessExpression(node.expression)) {
|
|
528
|
+
const callee = node.expression;
|
|
529
|
+
const [argument] = node.arguments;
|
|
530
|
+
const receiver = callee.expression;
|
|
531
|
+
const resolving =
|
|
532
|
+
callee.name.text === "resolve" &&
|
|
533
|
+
((isMetaProperty(receiver) && receiver.name.text === "meta") ||
|
|
534
|
+
(isIdentifier(receiver) && receiver.text === "require"));
|
|
535
|
+
if (resolving && argument !== undefined && isLiteralText(argument)) {
|
|
536
|
+
found.push(argument);
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
node.forEachChild(visit);
|
|
540
|
+
};
|
|
541
|
+
visit(file);
|
|
542
|
+
return found;
|
|
543
|
+
}
|
|
544
|
+
|
|
497
545
|
/** The offset just past the JSON value that starts at `at` in text the parser has accepted. */
|
|
498
546
|
function valueEnd(text: string, at: number): number {
|
|
499
547
|
const first = text.charAt(at);
|
|
@@ -585,21 +633,17 @@ function installedOf(host: Host, root: string, name: string): InstalledDependenc
|
|
|
585
633
|
for (let dir = root; ; dir = dirnamePath(dir)) {
|
|
586
634
|
const read = readJSON(host, joinPath(dir, MODULES_DIR, name, MANIFEST));
|
|
587
635
|
if (read !== undefined && isRecord(read.value)) {
|
|
588
|
-
const
|
|
589
|
-
|
|
590
|
-
const peers = manifest["peerDependencies"];
|
|
591
|
-
const meta = isRecord(manifest["peerDependenciesMeta"])
|
|
592
|
-
? manifest["peerDependenciesMeta"]
|
|
593
|
-
: {};
|
|
636
|
+
const bin = read.value["bin"];
|
|
637
|
+
// A string `bin` names one command, the package's name without its scope.
|
|
594
638
|
return {
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
639
|
+
commands:
|
|
640
|
+
typeof bin === "string"
|
|
641
|
+
? bin === ""
|
|
642
|
+
? []
|
|
643
|
+
: [name.slice(name.indexOf("/") + 1)]
|
|
644
|
+
: isRecord(bin)
|
|
645
|
+
? Object.keys(bin)
|
|
646
|
+
: [],
|
|
603
647
|
};
|
|
604
648
|
}
|
|
605
649
|
if (dirnamePath(dir) === dir) {
|
|
@@ -639,12 +683,37 @@ function declaredIn(
|
|
|
639
683
|
return declared.sort((a, b) => a.at - b.at).map(({ dependency }) => dependency);
|
|
640
684
|
}
|
|
641
685
|
|
|
686
|
+
/** The key one declared dependency is known by: its manifest's path and its name. */
|
|
687
|
+
export function declarationKey(dependency: DeclaredDependency): string {
|
|
688
|
+
return `${dependency.position.path}\u0000${dependency.name}`;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/** Every token of every value of one manifest's `scripts` member. */
|
|
692
|
+
function scriptTokensOf(manifest: Record<string, unknown>): ReadonlySet<string> {
|
|
693
|
+
const scripts = manifest["scripts"];
|
|
694
|
+
const found = new Set<string>();
|
|
695
|
+
if (isRecord(scripts)) {
|
|
696
|
+
for (const command of Object.values(scripts)) {
|
|
697
|
+
if (typeof command === "string") {
|
|
698
|
+
scriptTokens(command).forEach((token) => found.add(token));
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
return found;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/** Whether a path below the target root lies in the directory `dir` below it, `""` being the root. */
|
|
706
|
+
function isInside(path: string, dir: string): boolean {
|
|
707
|
+
return dir === "" || path.startsWith(`${dir}/`);
|
|
708
|
+
}
|
|
709
|
+
|
|
642
710
|
/**
|
|
643
711
|
* The run's dependency evidence: the target's manifest and each workspace member's in
|
|
644
712
|
* `members` below the target root, read with the position of every dependency each
|
|
645
|
-
* declares, the
|
|
646
|
-
*
|
|
647
|
-
* program cannot read declares
|
|
713
|
+
* declares, what the projects and the commands its scripts and workflow steps run need,
|
|
714
|
+
* and the last uses each deletion candidate holds. A member in `unbuilt` holds source only
|
|
715
|
+
* in configurations the run could not build. A manifest the program cannot read declares
|
|
716
|
+
* nothing.
|
|
648
717
|
*/
|
|
649
718
|
export function dependenciesOf(
|
|
650
719
|
host: Host,
|
|
@@ -652,30 +721,43 @@ export function dependenciesOf(
|
|
|
652
721
|
perProject: readonly ProjectNeeds[],
|
|
653
722
|
candidates: readonly string[],
|
|
654
723
|
members: readonly string[] = [],
|
|
724
|
+
unbuiltMembers: readonly string[] = [],
|
|
655
725
|
): Dependencies {
|
|
656
726
|
const root = resolvePath(host.workingDirectory(), targetRoot);
|
|
657
727
|
const scopeOf = packageScope(host, targetRoot);
|
|
658
728
|
const manifests = new Map<string, string>([[root, MANIFEST]]);
|
|
659
|
-
|
|
729
|
+
const unbuilt = new Set<string>();
|
|
730
|
+
for (const dir of [...members, ...unbuiltMembers]) {
|
|
660
731
|
const below = relativePath(root, dir);
|
|
661
732
|
if (below !== undefined && !manifests.has(dir)) {
|
|
662
733
|
manifests.set(dir, `${below}/${MANIFEST}`);
|
|
663
734
|
}
|
|
664
735
|
}
|
|
736
|
+
for (const dir of unbuiltMembers) {
|
|
737
|
+
const path = manifests.get(dir);
|
|
738
|
+
if (path !== undefined && dir !== root) {
|
|
739
|
+
unbuilt.add(path);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
const workflow = new Set(workflowTokens(host, targetRoot));
|
|
665
743
|
const declared: DeclaredDependency[] = [];
|
|
666
|
-
const
|
|
744
|
+
const ran = new Set<string>();
|
|
667
745
|
for (const [dir, path] of manifests) {
|
|
668
746
|
const read = readJSON(host, joinPath(dir, MANIFEST));
|
|
669
747
|
if (read === undefined || !isRecord(read.value)) {
|
|
670
748
|
continue;
|
|
671
749
|
}
|
|
750
|
+
const tokens = scriptTokensOf(read.value);
|
|
672
751
|
for (const dependency of declaredIn(read.text, read.value, path, scopeOf(path))) {
|
|
673
752
|
declared.push(dependency);
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
753
|
+
const found = installedOf(host, dir, dependency.name);
|
|
754
|
+
if (found === undefined) {
|
|
755
|
+
continue;
|
|
756
|
+
}
|
|
757
|
+
const runs = (command: string): boolean =>
|
|
758
|
+
tokens.has(command) || (dir === root && workflow.has(command));
|
|
759
|
+
if (found.commands.some(runs)) {
|
|
760
|
+
ran.add(declarationKey(dependency));
|
|
679
761
|
}
|
|
680
762
|
}
|
|
681
763
|
}
|
|
@@ -685,9 +767,15 @@ export function dependenciesOf(
|
|
|
685
767
|
const needed = new Set<string>();
|
|
686
768
|
const uses = new Map<string, Set<string>>();
|
|
687
769
|
for (const project of perProject) {
|
|
688
|
-
// A specifier the checker left unanswered may resolve to any
|
|
689
|
-
// and what the configuration needs is never deleted
|
|
690
|
-
const
|
|
770
|
+
// A specifier the checker left unanswered may resolve to any dependency a manifest at
|
|
771
|
+
// or above its file declares, and what the configuration needs is never deleted.
|
|
772
|
+
const unanswered = project.unanswered.flatMap((file) => relativePath(root, file) ?? []);
|
|
773
|
+
const unknown = declared
|
|
774
|
+
.filter((dependency) => {
|
|
775
|
+
const dir = dirnamePath(dependency.position.path);
|
|
776
|
+
return unanswered.some((file) => isInside(file, dir === "." ? "" : dir));
|
|
777
|
+
})
|
|
778
|
+
.map((dependency) => dependency.name);
|
|
691
779
|
const held = unknown.length === 0 ? [] : [[PROJECT_OWNER, new Set(unknown)] as const];
|
|
692
780
|
for (const [id, used] of [...project.uses, ...held]) {
|
|
693
781
|
const merged = uses.get(id) ?? new Set<string>();
|
|
@@ -711,5 +799,5 @@ export function dependenciesOf(
|
|
|
711
799
|
);
|
|
712
800
|
}
|
|
713
801
|
}
|
|
714
|
-
return { declared,
|
|
802
|
+
return { declared, needed, ran, unbuilt, lastUses };
|
|
715
803
|
}
|
package/src/finding.ts
CHANGED
|
@@ -83,6 +83,8 @@ export interface Finding {
|
|
|
83
83
|
readonly message: string;
|
|
84
84
|
/** Absent where the code carries nothing. */
|
|
85
85
|
readonly details?: FindingDetails;
|
|
86
|
+
/** The reachability class of a subject that is no declaration, where it is not `certain`. */
|
|
87
|
+
readonly reachabilityClass?: Confidence;
|
|
86
88
|
}
|
|
87
89
|
|
|
88
90
|
/** The dead component a finding names, as the finding schema spells its members. */
|
|
@@ -136,12 +136,11 @@ function findingComponents(
|
|
|
136
136
|
|
|
137
137
|
/**
|
|
138
138
|
* Every finding of the run completed, in the order given. A dead declaration carries its
|
|
139
|
-
* candidate's relation, counts, configurations and component; a live one carries no
|
|
140
|
-
*
|
|
141
|
-
* holds in every configuration. A subject in no dead component gets one
|
|
142
|
-
* numbered past the computed ones in finding order
|
|
143
|
-
*
|
|
144
|
-
* row is read from, the shipped one unless a caller supplies its own.
|
|
139
|
+
* candidate's relation, counts, configurations and component; a live one carries no relation and
|
|
140
|
+
* holds where it is declared; a file or a manifest row carries the class its emitter states,
|
|
141
|
+
* `certain` by default, and holds in every configuration. A subject in no dead component gets one
|
|
142
|
+
* of its own, numbered past the computed ones in finding order so no two families mint one, whose
|
|
143
|
+
* deletion removes no line. `kinds` defaults to the shipped vocabulary.
|
|
145
144
|
*/
|
|
146
145
|
export function completed(
|
|
147
146
|
input: EmitterInput,
|
|
@@ -210,7 +209,8 @@ export function completed(
|
|
|
210
209
|
candidate === undefined
|
|
211
210
|
? union.made[at]
|
|
212
211
|
: { production: candidate.productionRefs, test: candidate.testRefs };
|
|
213
|
-
const reachabilityClass =
|
|
212
|
+
const reachabilityClass =
|
|
213
|
+
judged === undefined ? (finding.reachabilityClass ?? "certain") : classOf(judged.id);
|
|
214
214
|
const generated = input.files.generated?.has(finding.position.path) === true;
|
|
215
215
|
return {
|
|
216
216
|
code: finding.code,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { declarationKey, type Dependencies } from "../dependencies.ts";
|
|
2
2
|
import type { Finding } from "../finding.ts";
|
|
3
3
|
import { renderRef, type DependencySection } from "../ref.ts";
|
|
4
4
|
import type { Emitter } from "./emitter.ts";
|
|
@@ -11,39 +11,17 @@ const MESSAGES: Readonly<Record<DependencySection, string>> = {
|
|
|
11
11
|
"peer-dependency": "no import in the target needs the declared peer dependency",
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
/**
|
|
15
|
-
* The dependencies the target needs: every package the projects' uses name, every
|
|
16
|
-
* declared dependency that ships a command, which a script, a workflow or a shell
|
|
17
|
-
* runs outside anything the analysis reads, and every peer a needed package
|
|
18
|
-
* requires, which the target satisfies by declaring it.
|
|
19
|
-
*/
|
|
20
|
-
function neededBy(dependencies: Dependencies): ReadonlySet<string> {
|
|
21
|
-
const needed = new Set(dependencies.needed);
|
|
22
|
-
for (const [name, installed] of dependencies.installed) {
|
|
23
|
-
if (installed.command) {
|
|
24
|
-
needed.add(name);
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
const pending = [...needed];
|
|
28
|
-
for (let name = pending.pop(); name !== undefined; name = pending.pop()) {
|
|
29
|
-
for (const peer of dependencies.installed.get(name)?.peers ?? []) {
|
|
30
|
-
if (!needed.has(peer)) {
|
|
31
|
-
needed.add(peer);
|
|
32
|
-
pending.push(peer);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
return needed;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
14
|
/**
|
|
40
15
|
* A dependency, development dependency or peer dependency of a manifest the run reads
|
|
41
16
|
* that the target does not need, at the position of its key.
|
|
42
17
|
*/
|
|
43
18
|
function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
44
|
-
const needed = neededBy(dependencies);
|
|
45
19
|
return dependencies.declared
|
|
46
|
-
.filter(
|
|
20
|
+
.filter(
|
|
21
|
+
(dependency) =>
|
|
22
|
+
!dependencies.needed.has(dependency.name) &&
|
|
23
|
+
!dependencies.ran.has(declarationKey(dependency)),
|
|
24
|
+
)
|
|
47
25
|
.map((dependency) => ({
|
|
48
26
|
code: UNUSED_DEPENDENCY,
|
|
49
27
|
position: dependency.position,
|
|
@@ -59,6 +37,9 @@ function unusedDependencies(dependencies: Dependencies): Finding[] {
|
|
|
59
37
|
},
|
|
60
38
|
message: MESSAGES[dependency.section],
|
|
61
39
|
details: { dependencyClass: dependency.section },
|
|
40
|
+
...(dependencies.unbuilt.has(dependency.position.path)
|
|
41
|
+
? { reachabilityClass: "possible" as const }
|
|
42
|
+
: {}),
|
|
62
43
|
}));
|
|
63
44
|
}
|
|
64
45
|
|
package/src/html-entries.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The files the
|
|
3
|
-
*
|
|
2
|
+
* The files the scripts of the target's HTML files load: a module script's, and a
|
|
3
|
+
* classic script's that names its file by `src`. An HTML file is read as text and no
|
|
4
|
+
* markup is parsed, so a `<script>` inside a comment is read as well.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import {
|
|
@@ -14,7 +15,7 @@ import type { Host } from "./host.ts";
|
|
|
14
15
|
import { dirnamePath, joinPath, relativePath, resolvePath } from "./paths.ts";
|
|
15
16
|
import type { SourceFiles } from "./source-files.ts";
|
|
16
17
|
|
|
17
|
-
/** One file a
|
|
18
|
+
/** One file a script loads, and the value or specifier that named it. */
|
|
18
19
|
export interface HTMLEntry {
|
|
19
20
|
readonly file: SourceFile;
|
|
20
21
|
readonly source: string;
|
|
@@ -35,6 +36,9 @@ const ATTRIBUTE = /([^\s"'=<>/]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)
|
|
|
35
36
|
/** A value that names a resource on another origin, or by another scheme. */
|
|
36
37
|
const ELSEWHERE = /^(?:\/\/|[A-Za-z][A-Za-z\d+.-]*:)/u;
|
|
37
38
|
|
|
39
|
+
/** The `type` values a classic script carries, compared in lowercase. */
|
|
40
|
+
const CLASSIC_TYPES: ReadonlySet<string> = new Set(["text/javascript", "application/javascript"]);
|
|
41
|
+
|
|
38
42
|
/** The prefixes an inline script's specifier names a file of the target with. */
|
|
39
43
|
const FILE_PREFIXES = ["/", "./", "../"] as const;
|
|
40
44
|
|
|
@@ -85,18 +89,22 @@ function specifiersOf(parse: ParseFile, fileName: string, content: string): read
|
|
|
85
89
|
}
|
|
86
90
|
|
|
87
91
|
/**
|
|
88
|
-
* Every value a
|
|
89
|
-
*
|
|
90
|
-
* `./` or `../`.
|
|
92
|
+
* Every value a script of one HTML file names a file by: the `src` attribute's value of a
|
|
93
|
+
* module script or of a classic one, or each specifier an inline module script imports or
|
|
94
|
+
* re-exports that starts with `/`, `./` or `../`.
|
|
91
95
|
*/
|
|
92
96
|
function scriptValues(parse: ParseFile | undefined, path: string, text: string): readonly string[] {
|
|
93
97
|
const values: string[] = [];
|
|
94
98
|
for (const match of text.matchAll(SCRIPT_TAG)) {
|
|
95
99
|
const held = attributes(match[1] ?? "");
|
|
96
|
-
|
|
100
|
+
const type = held.get("type");
|
|
101
|
+
const src = held.get("src");
|
|
102
|
+
if (type !== "module") {
|
|
103
|
+
if (src !== undefined && (type === undefined || CLASSIC_TYPES.has(type.toLowerCase()))) {
|
|
104
|
+
values.push(src);
|
|
105
|
+
}
|
|
97
106
|
continue;
|
|
98
107
|
}
|
|
99
|
-
const src = held.get("src");
|
|
100
108
|
if (src !== undefined) {
|
|
101
109
|
values.push(src);
|
|
102
110
|
continue;
|
|
@@ -131,11 +139,11 @@ function manifestDirectory(host: Host, root: string, dir: string): string {
|
|
|
131
139
|
}
|
|
132
140
|
|
|
133
141
|
/**
|
|
134
|
-
* The own files the
|
|
142
|
+
* The own files the scripts of every HTML file below the target root load. A value
|
|
135
143
|
* starting with a single `/` is read against the nearest manifest's directory, any other
|
|
136
|
-
* against the HTML file's own,
|
|
137
|
-
*
|
|
138
|
-
*
|
|
144
|
+
* against the HTML file's own, as a configuration file's relative specifier. One naming no
|
|
145
|
+
* source file is read back through the project's emit mapping, and a `/` one also against
|
|
146
|
+
* an output directory that is the HTML file's own. Another origin or scheme loads nothing.
|
|
139
147
|
*/
|
|
140
148
|
export function htmlEntries(
|
|
141
149
|
host: Host,
|
|
@@ -157,12 +165,22 @@ export function htmlEntries(
|
|
|
157
165
|
if (ELSEWHERE.test(value)) {
|
|
158
166
|
continue;
|
|
159
167
|
}
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
168
|
+
const base = value.startsWith("/") ? manifestDirectory(host, root, dir) : dir;
|
|
169
|
+
const relative = value.startsWith("/") ? `.${value}` : value;
|
|
170
|
+
const reached = resolveRelativeSpecifier(host, base, relative);
|
|
163
171
|
const file = reached === undefined ? undefined : files.byName.get(reached);
|
|
164
172
|
if (file !== undefined) {
|
|
165
173
|
found.push({ file, source: value });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
const served = value.startsWith("/")
|
|
177
|
+
? files.outputDirs.filter((outDir) => outDir === dir)
|
|
178
|
+
: [];
|
|
179
|
+
const emitting = [base, ...served].flatMap((against) =>
|
|
180
|
+
files.named(resolvePath(against, relative)),
|
|
181
|
+
);
|
|
182
|
+
for (const one of new Set(emitting)) {
|
|
183
|
+
found.push({ file: one, source: value });
|
|
166
184
|
}
|
|
167
185
|
}
|
|
168
186
|
}
|