@cplieger/deadset-ts 1.1.0 → 2.0.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/README.md +46 -5
- package/{bin/deadset-ts.ts → dist/bin/deadset-ts.js} +2 -3
- package/dist/bin/node-host.js +74 -0
- package/dist/src/config.js +63 -0
- package/dist/src/discover.js +171 -0
- package/dist/src/host.js +1 -0
- package/dist/src/index.js +6 -0
- package/dist/src/inventory.js +676 -0
- package/dist/src/kinds.js +12 -0
- package/dist/src/paths.js +86 -0
- package/dist/src/position.js +52 -0
- package/dist/src/print.js +56 -0
- package/dist/src/ref.js +117 -0
- package/dist/src/references.js +420 -0
- package/dist/src/resolve.js +482 -0
- package/dist/src/run.js +249 -0
- package/dist/src/schema.js +335 -0
- package/dist/src/scope.js +116 -0
- package/dist/src/session.js +101 -0
- package/dist/src/version.js +7 -0
- package/jsr.json +1 -1
- package/package.json +10 -4
- package/src/host.ts +10 -2
- package/src/index.ts +39 -1
- package/src/inventory.ts +964 -0
- package/src/paths.ts +20 -0
- package/src/position.ts +75 -0
- package/src/ref.ts +179 -0
- package/src/references.ts +606 -0
- package/src/run.ts +4 -4
- package/src/session.ts +57 -22
- package/src/version.ts +3 -18
- package/bin/node-host.ts +0 -28
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem paths, as the supported platform spells them: a solidus separates
|
|
3
|
+
* segments and a leading solidus makes a path absolute.
|
|
4
|
+
*
|
|
5
|
+
* They are written here rather than taken from the platform's own library because
|
|
6
|
+
* every module below the command line stays free of the platform, and because the
|
|
7
|
+
* supported platform set is one, so a second spelling would be a rule nothing
|
|
8
|
+
* exercises.
|
|
9
|
+
*/
|
|
10
|
+
const SEPARATOR = "/";
|
|
11
|
+
/** Whether one path names a place without a directory to resolve it against. */
|
|
12
|
+
export function isAbsolutePath(path) {
|
|
13
|
+
return path.startsWith(SEPARATOR);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* One path with `.` removed, `..` applied to the segment before it, and repeated
|
|
17
|
+
* separators collapsed. A `..` that walks past the root of an absolute path is
|
|
18
|
+
* dropped, because the root has no parent; one at the head of a relative path is
|
|
19
|
+
* kept, because the path names a place above its own start.
|
|
20
|
+
*/
|
|
21
|
+
export function normalizePath(path) {
|
|
22
|
+
const absolute = isAbsolutePath(path);
|
|
23
|
+
const segments = [];
|
|
24
|
+
for (const segment of path.split(SEPARATOR)) {
|
|
25
|
+
if (segment === "" || segment === ".") {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (segment !== "..") {
|
|
29
|
+
segments.push(segment);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
const last = segments[segments.length - 1];
|
|
33
|
+
if (last !== undefined && last !== "..") {
|
|
34
|
+
segments.pop();
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
if (!absolute) {
|
|
38
|
+
segments.push("..");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
const joined = segments.join(SEPARATOR);
|
|
42
|
+
if (absolute) {
|
|
43
|
+
return SEPARATOR + joined;
|
|
44
|
+
}
|
|
45
|
+
return joined === "" ? "." : joined;
|
|
46
|
+
}
|
|
47
|
+
/** One path below another, normalized. */
|
|
48
|
+
export function joinPath(...parts) {
|
|
49
|
+
const joined = parts.filter((part) => part !== "").join(SEPARATOR);
|
|
50
|
+
return joined === "" ? "." : normalizePath(joined);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* The directory one path sits in. A path with one segment sits in the directory it
|
|
54
|
+
* is resolved against, which is `.`; the root sits in itself.
|
|
55
|
+
*/
|
|
56
|
+
export function dirnamePath(path) {
|
|
57
|
+
const normalized = normalizePath(path);
|
|
58
|
+
const cut = normalized.lastIndexOf(SEPARATOR);
|
|
59
|
+
if (cut < 0) {
|
|
60
|
+
return ".";
|
|
61
|
+
}
|
|
62
|
+
return cut === 0 ? SEPARATOR : normalized.slice(0, cut);
|
|
63
|
+
}
|
|
64
|
+
/** One path resolved against a directory, absolute where that directory is. */
|
|
65
|
+
export function resolvePath(against, path) {
|
|
66
|
+
return isAbsolutePath(path) ? normalizePath(path) : joinPath(against, path);
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* One path as it reads below a root, or undefined when it is not below the root.
|
|
70
|
+
* Both are normalized first, so a root written with a trailing separator or a `.`
|
|
71
|
+
* segment reaches the same answer, and the comparison is then lexical, so a root
|
|
72
|
+
* and a path spelled through different symbolic links name nothing in common. The
|
|
73
|
+
* root itself is not below itself.
|
|
74
|
+
*
|
|
75
|
+
* The caller resolves both against the same directory before calling: a relative
|
|
76
|
+
* root and an absolute path are two spellings this cannot reconcile.
|
|
77
|
+
*/
|
|
78
|
+
export function relativePath(root, path) {
|
|
79
|
+
const at = normalizePath(root);
|
|
80
|
+
const prefix = at.endsWith(SEPARATOR) ? at : at + SEPARATOR;
|
|
81
|
+
const under = normalizePath(path);
|
|
82
|
+
if (!under.startsWith(prefix) || under.length === prefix.length) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return under.slice(prefix.length);
|
|
86
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { relativePath } from "./paths.js";
|
|
2
|
+
/**
|
|
3
|
+
* A file the program holds whose path is not below the target root, which leaves
|
|
4
|
+
* the analysis unable to name a declaration it found. It ends the run rather than
|
|
5
|
+
* being passed over, so a position that does not render is never silently dropped.
|
|
6
|
+
*/
|
|
7
|
+
export class PositionError extends Error {
|
|
8
|
+
/** The file as the program named it. */
|
|
9
|
+
file;
|
|
10
|
+
constructor(file, root) {
|
|
11
|
+
super(`${file} is not below ${root}`);
|
|
12
|
+
this.name = "PositionError";
|
|
13
|
+
this.file = file;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The position of one offset of one file, rendered against the target root.
|
|
18
|
+
*
|
|
19
|
+
* `offset` is a UTF-16 code unit offset into the file's text, which is what every
|
|
20
|
+
* node of the tree carries.
|
|
21
|
+
*/
|
|
22
|
+
export function renderPosition(file, root, offset) {
|
|
23
|
+
const path = relativePath(root, file.fileName);
|
|
24
|
+
if (path === undefined) {
|
|
25
|
+
throw new PositionError(file.fileName, root);
|
|
26
|
+
}
|
|
27
|
+
const { line, character } = file.getLineAndCharacterOfPosition(offset);
|
|
28
|
+
return { path, line: line + 1, column: character + 1 };
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* The identifier of the declaration written at one position: the path, the line and
|
|
32
|
+
* the column joined by colons. It is what the graph keys on inside one run, and
|
|
33
|
+
* what the configuration intersection compares across projects, because a handle is
|
|
34
|
+
* meaningful only inside the program that produced it while a rendered position is
|
|
35
|
+
* meaningful anywhere.
|
|
36
|
+
*/
|
|
37
|
+
export function positionKey(position) {
|
|
38
|
+
return `${position.path}:${String(position.line)}:${String(position.column)}`;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Two positions ordered by file, line and column, which is the order the inventory
|
|
42
|
+
* reads in and the order every set of a run is reported in.
|
|
43
|
+
*/
|
|
44
|
+
export function byPosition(a, b) {
|
|
45
|
+
if (a.path !== b.path) {
|
|
46
|
+
return a.path < b.path ? -1 : 1;
|
|
47
|
+
}
|
|
48
|
+
if (a.line !== b.line) {
|
|
49
|
+
return a.line - b.line;
|
|
50
|
+
}
|
|
51
|
+
return a.column - b.column;
|
|
52
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { renderOrigin } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* The resolved configuration and its provenance as one JSON object, indented,
|
|
4
|
+
* ending in a newline.
|
|
5
|
+
*
|
|
6
|
+
* The output is an instance of the closed key list, so reading it back as a
|
|
7
|
+
* repository configuration resolves to the same configuration: the provenance
|
|
8
|
+
* object is a declared key resolution ignores. Keys are written in the schema's
|
|
9
|
+
* order and the severity and provenance objects in ascending key order, so one
|
|
10
|
+
* configuration renders to one string whatever order a source named its settings
|
|
11
|
+
* in.
|
|
12
|
+
*/
|
|
13
|
+
export function printConfig(config, provenance) {
|
|
14
|
+
const document = {
|
|
15
|
+
contract_version: config.contractVersion,
|
|
16
|
+
target: { kind: config.targetKind },
|
|
17
|
+
analysis: {
|
|
18
|
+
languages: config.analysis.languages,
|
|
19
|
+
min_confidence: config.analysis.minConfidence,
|
|
20
|
+
generated_files: config.analysis.generatedFiles,
|
|
21
|
+
consumer_tests: config.analysis.consumerTests,
|
|
22
|
+
configurations: config.analysis.configurations.map((entry) => ({
|
|
23
|
+
id: entry.id,
|
|
24
|
+
os: entry.os,
|
|
25
|
+
arch: entry.arch,
|
|
26
|
+
tags: entry.tags,
|
|
27
|
+
})),
|
|
28
|
+
matrix: { complete: config.analysis.matrixComplete },
|
|
29
|
+
template_dirs: config.analysis.templateDirs,
|
|
30
|
+
template_delimiters: {
|
|
31
|
+
left: config.analysis.templateDelimiters.left,
|
|
32
|
+
right: config.analysis.templateDelimiters.right,
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
consumers: { complete: config.consumersComplete },
|
|
36
|
+
roots: { patterns: config.rootPatterns },
|
|
37
|
+
severity: Object.fromEntries([...config.severity].sort(([a], [b]) => (a < b ? -1 : 1))),
|
|
38
|
+
exemptions: { disabled: config.exemptionsDisabled },
|
|
39
|
+
reporters: {
|
|
40
|
+
formats: config.reporters.formats,
|
|
41
|
+
sort: config.reporters.sort,
|
|
42
|
+
cascade: config.reporters.cascade,
|
|
43
|
+
max_findings: config.reporters.maxFindings,
|
|
44
|
+
fail_on: config.reporters.failOn,
|
|
45
|
+
},
|
|
46
|
+
go: {},
|
|
47
|
+
ts: {
|
|
48
|
+
test_files: config.ts.testFiles,
|
|
49
|
+
entry_files: config.ts.entryFiles,
|
|
50
|
+
},
|
|
51
|
+
provenance: Object.fromEntries([...provenance]
|
|
52
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
53
|
+
.map(([path, origin]) => [path, renderOrigin(origin)])),
|
|
54
|
+
};
|
|
55
|
+
return `${JSON.stringify(document, undefined, 2)}\n`;
|
|
56
|
+
}
|
package/dist/src/ref.js
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The stable symbol reference: the identifier that names one declaration across
|
|
3
|
+
* runs, across reports, in an ignore entry, in a configured root and in a
|
|
4
|
+
* cross-language edge. It is built from names and containers only, so an edit above
|
|
5
|
+
* a declaration leaves it unchanged, and it is compared as bytes, so there is one
|
|
6
|
+
* canonical spelling per declaration and this module is where that spelling is
|
|
7
|
+
* written.
|
|
8
|
+
*
|
|
9
|
+
* The position key of {@link ./position.ts} is the other identifier and has the
|
|
10
|
+
* other job: it locates a finding inside one run, and the two never mix.
|
|
11
|
+
*/
|
|
12
|
+
/** The language prefix, which namespaces every reference this analyzer emits. */
|
|
13
|
+
const PREFIX = "ts://";
|
|
14
|
+
/**
|
|
15
|
+
* The building blocks of the reference grammar, spelled in the intersection of the
|
|
16
|
+
* two regular-expression dialects a reference is checked in: Go's `regexp` and an
|
|
17
|
+
* ECMAScript `RegExp` with no flags. Neither dialect offers a Unicode identifier
|
|
18
|
+
* property without a flag, so a character outside ASCII is admitted as a class;
|
|
19
|
+
* the expressions check the form of a reference and never validate a name, because
|
|
20
|
+
* the compiler that accepted the declaration has already validated it.
|
|
21
|
+
*/
|
|
22
|
+
const TS_IDENT = String.raw `(?:[A-Za-z_$]|[^\x00-\x7F])(?:[A-Za-z0-9_$]|[^\x00-\x7F])*`;
|
|
23
|
+
const TS_QUOTED = String.raw `'(?:[^'\\\r\n]|\\['\\nr])*'`;
|
|
24
|
+
const TS_COMPUTED = String.raw `\[[^\]\r\n\t ]+\]`;
|
|
25
|
+
const TS_HEAD = `(?:${TS_IDENT}|${TS_QUOTED})`;
|
|
26
|
+
const TS_MEMBER = `(?:${TS_IDENT}|#${TS_IDENT}|${TS_QUOTED}|${TS_COMPUTED})`;
|
|
27
|
+
const TS_PACKAGE = String.raw `(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+`;
|
|
28
|
+
const TS_FILE = String.raw `(?:[^/#\r\n]+/)*[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
|
|
29
|
+
const SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_FILE}`;
|
|
30
|
+
const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/package\\.json`;
|
|
31
|
+
/**
|
|
32
|
+
* One expression per form the grammar defines, each anchored and each the whole of
|
|
33
|
+
* a reference. Several forms share one expression because they differ in what the
|
|
34
|
+
* symbol is rather than in how the reference is spelled, so the list is the set of
|
|
35
|
+
* spellings and not the set of subjects. A reference is valid when it matches one.
|
|
36
|
+
*
|
|
37
|
+
* They are written here rather than read from the Contract at run time: the Contract
|
|
38
|
+
* is data a reader outside this program consults, and a product that read it while
|
|
39
|
+
* running would need it installed beside itself. A test compares this list against
|
|
40
|
+
* the release this analyzer is written against, so a grammar that grows a form
|
|
41
|
+
* arrives here as a failing test.
|
|
42
|
+
*/
|
|
43
|
+
export const REF_EXPRESSIONS = [
|
|
44
|
+
`^${SCOPE}#$`,
|
|
45
|
+
`^${SCOPE}#${TS_HEAD}$`,
|
|
46
|
+
`^${SCOPE}#${TS_HEAD}:alias$`,
|
|
47
|
+
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+$`,
|
|
48
|
+
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+:static$`,
|
|
49
|
+
`^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})*(?::static)?<${TS_IDENT}>$`,
|
|
50
|
+
`^${MANIFEST_SCOPE}#${TS_PACKAGE}:(?:dependency|dev-dependency|peer-dependency)$`,
|
|
51
|
+
];
|
|
52
|
+
const REF_FORMS = REF_EXPRESSIONS.map((expression) => new RegExp(expression, "u"));
|
|
53
|
+
const IDENTIFIER = new RegExp(`^${TS_IDENT}$`, "u");
|
|
54
|
+
const PRIVATE_NAME = new RegExp(`^#${TS_IDENT}$`, "u");
|
|
55
|
+
const ASCII_WHITESPACE = /[\t\n\v\f\r ]/gu;
|
|
56
|
+
/**
|
|
57
|
+
* The canonical spelling of one declared name as a fragment component: bare when
|
|
58
|
+
* the name is an identifier name, reserved words included; with its leading number
|
|
59
|
+
* sign kept when the name is a private name, because the sign is part of the name
|
|
60
|
+
* in the language and a class may declare both spellings side by side; and in
|
|
61
|
+
* single quotes otherwise. The quotes are single so that a reference sits inside a
|
|
62
|
+
* JSON string with no escaping, which is where every document carries one.
|
|
63
|
+
*/
|
|
64
|
+
export function nameComponent(name) {
|
|
65
|
+
if (IDENTIFIER.test(name) || PRIVATE_NAME.test(name)) {
|
|
66
|
+
return name;
|
|
67
|
+
}
|
|
68
|
+
const escaped = name
|
|
69
|
+
.replaceAll("\\", "\\\\")
|
|
70
|
+
.replaceAll("'", "\\'")
|
|
71
|
+
.replaceAll("\n", "\\n")
|
|
72
|
+
.replaceAll("\r", "\\r");
|
|
73
|
+
return `'${escaped}'`;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The canonical spelling of one computed key: its source text between square
|
|
77
|
+
* brackets with every ASCII whitespace character removed. The text is compared as
|
|
78
|
+
* written, so two spellings of one well-known symbol are two references, which is
|
|
79
|
+
* right, because they are two declarations.
|
|
80
|
+
*/
|
|
81
|
+
export function computedComponent(text) {
|
|
82
|
+
return `[${text.replaceAll(ASCII_WHITESPACE, "")}]`;
|
|
83
|
+
}
|
|
84
|
+
function spell(component) {
|
|
85
|
+
return component.computed ? computedComponent(component.text) : nameComponent(component.text);
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* The reference of one symbol. The chain of a declaration fragment is read from the
|
|
89
|
+
* module-level declaration inward and is never empty; the module fragment is what
|
|
90
|
+
* names the module itself.
|
|
91
|
+
*/
|
|
92
|
+
export function renderRef(module, fragment) {
|
|
93
|
+
const scope = `${PREFIX}${module.package}/${module.path}`;
|
|
94
|
+
switch (fragment.of) {
|
|
95
|
+
case "module":
|
|
96
|
+
return `${scope}#`;
|
|
97
|
+
case "alias":
|
|
98
|
+
return `${scope}#${nameComponent(fragment.name)}:alias`;
|
|
99
|
+
case "dependency":
|
|
100
|
+
return `${scope}#${fragment.name}:${fragment.section}`;
|
|
101
|
+
case "declaration": {
|
|
102
|
+
const chain = fragment.chain.map(spell).join(".");
|
|
103
|
+
const isStatic = fragment.static ? ":static" : "";
|
|
104
|
+
const parameter = fragment.typeParameter === undefined ? "" : `<${fragment.typeParameter}>`;
|
|
105
|
+
return `${scope}#${chain}${isStatic}${parameter}`;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Whether one string is a reference of this language's grammar, in its canonical
|
|
111
|
+
* spelling. A string that is not, whether misspelled or spelled in a form the
|
|
112
|
+
* grammar does not define, matches no symbol; a reference of another language is
|
|
113
|
+
* not one of this language's either, which is what the language prefix is for.
|
|
114
|
+
*/
|
|
115
|
+
export function isRef(value) {
|
|
116
|
+
return REF_FORMS.some((form) => form.test(value));
|
|
117
|
+
}
|