@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/src/paths.ts CHANGED
@@ -71,3 +71,23 @@ export function dirnamePath(path: string): string {
71
71
  export function resolvePath(against: string, path: string): string {
72
72
  return isAbsolutePath(path) ? normalizePath(path) : joinPath(against, path);
73
73
  }
74
+
75
+ /**
76
+ * One path as it reads below a root, or undefined when it is not below the root.
77
+ * Both are normalized first, so a root written with a trailing separator or a `.`
78
+ * segment reaches the same answer, and the comparison is then lexical, so a root
79
+ * and a path spelled through different symbolic links name nothing in common. The
80
+ * root itself is not below itself.
81
+ *
82
+ * The caller resolves both against the same directory before calling: a relative
83
+ * root and an absolute path are two spellings this cannot reconcile.
84
+ */
85
+ export function relativePath(root: string, path: string): string | undefined {
86
+ const at = normalizePath(root);
87
+ const prefix = at.endsWith(SEPARATOR) ? at : at + SEPARATOR;
88
+ const under = normalizePath(path);
89
+ if (!under.startsWith(prefix) || under.length === prefix.length) {
90
+ return undefined;
91
+ }
92
+ return under.slice(prefix.length);
93
+ }
@@ -0,0 +1,75 @@
1
+ import type { SourceFile } from "@typescript/native/unstable/ast";
2
+ import { relativePath } from "./paths.ts";
3
+
4
+ /**
5
+ * Where one declaration is written, as every report renders it.
6
+ *
7
+ * The column counts UTF-16 code units, which is the unit the SARIF specification
8
+ * and the Language Server Protocol count in, and which is also the unit the
9
+ * compiler's own positions are measured in, so no conversion stands between the
10
+ * two.
11
+ */
12
+ export interface Position {
13
+ /** The declaring file's path below the target root, with the solidus as separator. */
14
+ readonly path: string;
15
+ /** The line the declaration starts on, counted from one. */
16
+ readonly line: number;
17
+ /** The column, counted from one in UTF-16 code units. */
18
+ readonly column: number;
19
+ }
20
+
21
+ /**
22
+ * A file the program holds whose path is not below the target root, which leaves
23
+ * the analysis unable to name a declaration it found. It ends the run rather than
24
+ * being passed over, so a position that does not render is never silently dropped.
25
+ */
26
+ export class PositionError extends Error {
27
+ /** The file as the program named it. */
28
+ readonly file: string;
29
+
30
+ constructor(file: string, root: string) {
31
+ super(`${file} is not below ${root}`);
32
+ this.name = "PositionError";
33
+ this.file = file;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * The position of one offset of one file, rendered against the target root.
39
+ *
40
+ * `offset` is a UTF-16 code unit offset into the file's text, which is what every
41
+ * node of the tree carries.
42
+ */
43
+ export function renderPosition(file: SourceFile, root: string, offset: number): Position {
44
+ const path = relativePath(root, file.fileName);
45
+ if (path === undefined) {
46
+ throw new PositionError(file.fileName, root);
47
+ }
48
+ const { line, character } = file.getLineAndCharacterOfPosition(offset);
49
+ return { path, line: line + 1, column: character + 1 };
50
+ }
51
+
52
+ /**
53
+ * The identifier of the declaration written at one position: the path, the line and
54
+ * the column joined by colons. It is what the graph keys on inside one run, and
55
+ * what the configuration intersection compares across projects, because a handle is
56
+ * meaningful only inside the program that produced it while a rendered position is
57
+ * meaningful anywhere.
58
+ */
59
+ export function positionKey(position: Position): string {
60
+ return `${position.path}:${String(position.line)}:${String(position.column)}`;
61
+ }
62
+
63
+ /**
64
+ * Two positions ordered by file, line and column, which is the order the inventory
65
+ * reads in and the order every set of a run is reported in.
66
+ */
67
+ export function byPosition(a: Position, b: Position): number {
68
+ if (a.path !== b.path) {
69
+ return a.path < b.path ? -1 : 1;
70
+ }
71
+ if (a.line !== b.line) {
72
+ return a.line - b.line;
73
+ }
74
+ return a.column - b.column;
75
+ }
package/src/ref.ts ADDED
@@ -0,0 +1,179 @@
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
+
13
+ /** The language prefix, which namespaces every reference this analyzer emits. */
14
+ const PREFIX = "ts://";
15
+
16
+ /**
17
+ * The building blocks of the reference grammar, spelled in the intersection of the
18
+ * two regular-expression dialects a reference is checked in: Go's `regexp` and an
19
+ * ECMAScript `RegExp` with no flags. Neither dialect offers a Unicode identifier
20
+ * property without a flag, so a character outside ASCII is admitted as a class;
21
+ * the expressions check the form of a reference and never validate a name, because
22
+ * the compiler that accepted the declaration has already validated it.
23
+ */
24
+ const TS_IDENT = String.raw`(?:[A-Za-z_$]|[^\x00-\x7F])(?:[A-Za-z0-9_$]|[^\x00-\x7F])*`;
25
+ const TS_QUOTED = String.raw`'(?:[^'\\\r\n]|\\['\\nr])*'`;
26
+ const TS_COMPUTED = String.raw`\[[^\]\r\n\t ]+\]`;
27
+ const TS_HEAD = `(?:${TS_IDENT}|${TS_QUOTED})`;
28
+ const TS_MEMBER = `(?:${TS_IDENT}|#${TS_IDENT}|${TS_QUOTED}|${TS_COMPUTED})`;
29
+ const TS_PACKAGE = String.raw`(?:@[A-Za-z0-9._-]+/)?[A-Za-z0-9._-]+`;
30
+ const TS_FILE = String.raw`(?:[^/#\r\n]+/)*[^/#\r\n]+(?:(?:\.d)?\.[mc]?ts|\.tsx|\.[mc]?js|\.jsx)`;
31
+
32
+ const SCOPE = `${PREFIX}${TS_PACKAGE}/${TS_FILE}`;
33
+ const MANIFEST_SCOPE = `${PREFIX}${TS_PACKAGE}/package\\.json`;
34
+
35
+ /**
36
+ * One expression per form the grammar defines, each anchored and each the whole of
37
+ * a reference. Several forms share one expression because they differ in what the
38
+ * symbol is rather than in how the reference is spelled, so the list is the set of
39
+ * spellings and not the set of subjects. A reference is valid when it matches one.
40
+ *
41
+ * They are written here rather than read from the Contract at run time: the Contract
42
+ * is data a reader outside this program consults, and a product that read it while
43
+ * running would need it installed beside itself. A test compares this list against
44
+ * the release this analyzer is written against, so a grammar that grows a form
45
+ * arrives here as a failing test.
46
+ */
47
+ export const REF_EXPRESSIONS: readonly string[] = [
48
+ `^${SCOPE}#$`,
49
+ `^${SCOPE}#${TS_HEAD}$`,
50
+ `^${SCOPE}#${TS_HEAD}:alias$`,
51
+ `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+$`,
52
+ `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})+:static$`,
53
+ `^${SCOPE}#${TS_HEAD}(?:\\.${TS_MEMBER})*(?::static)?<${TS_IDENT}>$`,
54
+ `^${MANIFEST_SCOPE}#${TS_PACKAGE}:(?:dependency|dev-dependency|peer-dependency)$`,
55
+ ];
56
+
57
+ const REF_FORMS: readonly RegExp[] = REF_EXPRESSIONS.map(
58
+ (expression) => new RegExp(expression, "u"),
59
+ );
60
+
61
+ const IDENTIFIER = new RegExp(`^${TS_IDENT}$`, "u");
62
+ const PRIVATE_NAME = new RegExp(`^#${TS_IDENT}$`, "u");
63
+ const ASCII_WHITESPACE = /[\t\n\v\f\r ]/gu;
64
+
65
+ /** The section of a manifest that declares a dependency. */
66
+ export type DependencySection = "dependency" | "dev-dependency" | "peer-dependency";
67
+
68
+ /** The scope half of a reference: the package a file belongs to, and its path in it. */
69
+ export interface Module {
70
+ /**
71
+ * The name of the package the file belongs to, or a single dot where no manifest
72
+ * at or above it up to the target root carries one. The two cannot collide,
73
+ * because a package name never starts with a dot.
74
+ */
75
+ readonly package: string;
76
+ /** The file's path inside that package, with its own extension. */
77
+ readonly path: string;
78
+ }
79
+
80
+ /**
81
+ * One component of a fragment: a declared name, or the source text of a computed
82
+ * key. The two are spelled differently and cannot be told apart from the text
83
+ * alone, so the caller says which it has.
84
+ */
85
+ export interface Component {
86
+ /** The declared name, or the source text between the brackets of a computed key. */
87
+ readonly text: string;
88
+ /** Whether the text is a computed key rather than a name. */
89
+ readonly computed: boolean;
90
+ }
91
+
92
+ /** What one reference's fragment names. */
93
+ export type Fragment =
94
+ /** The module itself, which is the parent of every module-level declaration. */
95
+ | { readonly of: "module" }
96
+ /**
97
+ * One declaration, named by the chain of components from the module inward with
98
+ * its own last. A static member of a class is always marked, whether or not the
99
+ * class also declares an instance member of that name, so the spelling does not
100
+ * depend on what else the class declares. A type parameter is the declaring
101
+ * function's or method's own chain followed by the parameter's name.
102
+ */
103
+ | {
104
+ readonly of: "declaration";
105
+ readonly chain: readonly Component[];
106
+ readonly static: boolean;
107
+ readonly typeParameter?: string;
108
+ }
109
+ /** One export specifier, a symbol of its own beside the declaration it names. */
110
+ | { readonly of: "alias"; readonly name: string }
111
+ /** One dependency of a manifest, scoped by that manifest. */
112
+ | { readonly of: "dependency"; readonly name: string; readonly section: DependencySection };
113
+
114
+ /**
115
+ * The canonical spelling of one declared name as a fragment component: bare when
116
+ * the name is an identifier name, reserved words included; with its leading number
117
+ * sign kept when the name is a private name, because the sign is part of the name
118
+ * in the language and a class may declare both spellings side by side; and in
119
+ * single quotes otherwise. The quotes are single so that a reference sits inside a
120
+ * JSON string with no escaping, which is where every document carries one.
121
+ */
122
+ export function nameComponent(name: string): string {
123
+ if (IDENTIFIER.test(name) || PRIVATE_NAME.test(name)) {
124
+ return name;
125
+ }
126
+ const escaped = name
127
+ .replaceAll("\\", "\\\\")
128
+ .replaceAll("'", "\\'")
129
+ .replaceAll("\n", "\\n")
130
+ .replaceAll("\r", "\\r");
131
+ return `'${escaped}'`;
132
+ }
133
+
134
+ /**
135
+ * The canonical spelling of one computed key: its source text between square
136
+ * brackets with every ASCII whitespace character removed. The text is compared as
137
+ * written, so two spellings of one well-known symbol are two references, which is
138
+ * right, because they are two declarations.
139
+ */
140
+ export function computedComponent(text: string): string {
141
+ return `[${text.replaceAll(ASCII_WHITESPACE, "")}]`;
142
+ }
143
+
144
+ function spell(component: Component): string {
145
+ return component.computed ? computedComponent(component.text) : nameComponent(component.text);
146
+ }
147
+
148
+ /**
149
+ * The reference of one symbol. The chain of a declaration fragment is read from the
150
+ * module-level declaration inward and is never empty; the module fragment is what
151
+ * names the module itself.
152
+ */
153
+ export function renderRef(module: Module, fragment: Fragment): string {
154
+ const scope = `${PREFIX}${module.package}/${module.path}`;
155
+ switch (fragment.of) {
156
+ case "module":
157
+ return `${scope}#`;
158
+ case "alias":
159
+ return `${scope}#${nameComponent(fragment.name)}:alias`;
160
+ case "dependency":
161
+ return `${scope}#${fragment.name}:${fragment.section}`;
162
+ case "declaration": {
163
+ const chain = fragment.chain.map(spell).join(".");
164
+ const isStatic = fragment.static ? ":static" : "";
165
+ const parameter = fragment.typeParameter === undefined ? "" : `<${fragment.typeParameter}>`;
166
+ return `${scope}#${chain}${isStatic}${parameter}`;
167
+ }
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Whether one string is a reference of this language's grammar, in its canonical
173
+ * spelling. A string that is not, whether misspelled or spelled in a form the
174
+ * grammar does not define, matches no symbol; a reference of another language is
175
+ * not one of this language's either, which is what the language prefix is for.
176
+ */
177
+ export function isRef(value: string): boolean {
178
+ return REF_FORMS.some((form) => form.test(value));
179
+ }