@cplieger/deadset-ts 2.2.0 → 2.3.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.
@@ -1,4 +1,6 @@
1
1
  import { isArrayLiteralExpression, isBinaryExpression, isDeleteExpression, isElementAccessExpression, isForInStatement, isForOfStatement, isObjectLiteralExpression, isParenthesizedExpression, isPostfixUnaryExpression, isPrefixUnaryExpression, isPropertyAccessExpression, isPropertyAssignment, isShorthandPropertyAssignment, isSpreadAssignment, isSpreadElement, SyntaxKind, } from "@typescript/native/unstable/ast";
2
+ import { SymbolFlags } from "@typescript/native/unstable/sync";
3
+ import { globExpression } from "./glob.js";
2
4
  import { nodeKey } from "./inventory.js";
3
5
  import { byPosition, renderPosition } from "./position.js";
4
6
  /**
@@ -31,21 +33,6 @@ const ASSIGNMENTS = new Set([
31
33
  SyntaxKind.QuestionQuestionEqualsToken,
32
34
  SyntaxKind.CaretEqualsToken,
33
35
  ]);
34
- /**
35
- * The declaration forms that name something declared elsewhere. Each is a link rather
36
- * than a declaration of what it names, so a reference through one names the next link
37
- * and, at the end of the chain, the declaration itself: a re-export something imports
38
- * is used, and so is the declaration behind it.
39
- */
40
- const ALIASES = new Set([
41
- SyntaxKind.ImportClause,
42
- SyntaxKind.ImportEqualsDeclaration,
43
- SyntaxKind.ImportSpecifier,
44
- SyntaxKind.NamespaceImport,
45
- SyntaxKind.ExportAssignment,
46
- SyntaxKind.ExportSpecifier,
47
- SyntaxKind.NamespaceExport,
48
- ]);
49
36
  /** Whether one node is an identifier or a private name, the two forms a name takes. */
50
37
  function isName(node) {
51
38
  return node.kind === SyntaxKind.Identifier || node.kind === SyntaxKind.PrivateIdentifier;
@@ -197,62 +184,6 @@ function sitesOf(file, declarations, fileId) {
197
184
  file.forEachChild(visit);
198
185
  return found;
199
186
  }
200
- /**
201
- * One literal run of a pattern, with every character the expression syntax claims
202
- * escaped.
203
- *
204
- * The set is the syntax characters and the solidus, and it is closed: an escape of any
205
- * other character is refused outright by the expression syntax under the unicode flag,
206
- * so escaping a hyphen, which needs none outside a character class, would refuse every
207
- * pattern that names a hyphenated path.
208
- */
209
- function quoted(text) {
210
- return text.replaceAll(/[.*+?^${}()|[\]\\/]/gu, (character) => `\\${character}`);
211
- }
212
- /**
213
- * The expression one glob pattern denotes. `**` spans path separators while `*` and `?`
214
- * do not, a brace list is an alternation of its members, and every other character
215
- * stands for itself.
216
- */
217
- function expressionOf(pattern) {
218
- let source = "^";
219
- let at = 0;
220
- while (at < pattern.length) {
221
- const rest = pattern.slice(at);
222
- if (rest.startsWith("**/")) {
223
- source += "(?:[^/]*/)*";
224
- at += 3;
225
- continue;
226
- }
227
- if (rest.startsWith("**")) {
228
- source += ".*";
229
- at += 2;
230
- continue;
231
- }
232
- const character = pattern[at] ?? "";
233
- at += 1;
234
- if (character === "*") {
235
- source += "[^/]*";
236
- continue;
237
- }
238
- if (character === "?") {
239
- source += "[^/]";
240
- continue;
241
- }
242
- if (character === "{") {
243
- const close = pattern.indexOf("}", at);
244
- if (close < 0) {
245
- source += "\\{";
246
- continue;
247
- }
248
- source += `(?:${pattern.slice(at, close).split(",").map(quoted).join("|")})`;
249
- at = close + 1;
250
- continue;
251
- }
252
- source += quoted(character);
253
- }
254
- return new RegExp(`${source}$`, "u");
255
- }
256
187
  /**
257
188
  * The name one test-file rule takes. A rule's name is a word list the report's own
258
189
  * vocabulary spells, which cannot carry a pattern, so a rule is named by the place of
@@ -284,7 +215,7 @@ function compare(a, b) {
284
215
  export function references(project, held, targetRoot, options) {
285
216
  const cap = options.batchCap ?? DEFAULT_BATCH_CAP;
286
217
  const declarations = held.declarations;
287
- const patterns = options.testFiles.map(expressionOf);
218
+ const patterns = options.testFiles.map(globExpression);
288
219
  const matched = options.testFiles.map(() => 0);
289
220
  const found = [];
290
221
  const reached = new Map();
@@ -303,11 +234,17 @@ export function references(project, held, targetRoot, options) {
303
234
  * is the declaration behind it, so both are named; a link this project does not
304
235
  * declare is stepped over rather than recorded.
305
236
  *
306
- * A declaration handle carries the file it is in and the syntax it is written as, so
307
- * both questions the walk asks of a declaration are answered before it is resolved: a
308
- * declaration outside the project's own files belongs to another program and is never
309
- * read, which is what keeps the pass from fetching a library file to discover that the
310
- * inventory does not hold what is in it.
237
+ * A symbol is asked to step only where the binder flagged it an alias, which is the
238
+ * same condition the step itself asserts on. The declaration's syntax is not that
239
+ * condition and does not stand in for it: a default export of an expression is
240
+ * written as one of the alias forms and names nothing declared elsewhere, so it is
241
+ * recorded as the declaration it is and the chain ends there.
242
+ *
243
+ * A declaration handle carries the file it is in, so the question the walk asks of a
244
+ * declaration is answered before it is resolved: a declaration outside the project's
245
+ * own files belongs to another program and is never read, which is what keeps the
246
+ * pass from fetching a library file to discover that the inventory does not hold what
247
+ * is in it.
311
248
  */
312
249
  const targetsOf = (symbol) => {
313
250
  const cached = reached.get(symbol.id);
@@ -320,9 +257,8 @@ export function references(project, held, targetRoot, options) {
320
257
  let stepped = false;
321
258
  while (at !== undefined && !walked.has(at.id)) {
322
259
  walked.add(at.id);
323
- let alias = false;
260
+ const alias = (at.flags & SymbolFlags.Alias) !== 0;
324
261
  for (const handle of at.declarations) {
325
- alias = alias || ALIASES.has(handle.kind);
326
262
  if (!ownFiles.has(handle.path)) {
327
263
  continue;
328
264
  }
@@ -0,0 +1,345 @@
1
+ /**
2
+ * The root set of one project: every declaration the analysis keeps live without a
3
+ * reference, and why.
4
+ *
5
+ * A root makes its declaration live under REACHABILITY and plays no part in
6
+ * reference counting, which asks only whether some declaration of the loaded graph
7
+ * names it. The two relations are different sets, so an exported declaration that
8
+ * something outside the program reaches is live under the first and still a
9
+ * candidate under the second.
10
+ */
11
+ import { REPOSITORY_DOCUMENT } from "./config.js";
12
+ import { entryPoints } from "./entry-points.js";
13
+ import { globExpression } from "./glob.js";
14
+ import { nodeKey } from "./inventory.js";
15
+ import { byPosition } from "./position.js";
16
+ import { sourceFilesOf } from "./source-files.js";
17
+ /** The code the Contract gives a configured root that matches nothing. */
18
+ const UNMATCHED_ROOT_CODE = "DS1704";
19
+ /** The subject kind a finding under that code carries, from the Contract's vocabulary. */
20
+ const UNMATCHED_ROOT_SUBJECT = "root";
21
+ /** The fixed position of a finding about a document rather than about a line of one. */
22
+ const DOCUMENT_POSITION = 1;
23
+ /** Whether a configured string carries either special character. */
24
+ function isPattern(text) {
25
+ return text.includes("*") || text.includes("?");
26
+ }
27
+ /**
28
+ * Whether ref matches a pattern whose only special characters are `*`, which stands
29
+ * for any run of characters including the solidus, and `?`, which stands for exactly
30
+ * one character counted as a code point. A star keeps the position it last stood at,
31
+ * so the shortest run it can stand for is tried first.
32
+ */
33
+ function matchGlob(pattern, ref) {
34
+ let next = 0;
35
+ let star = -1;
36
+ let resume = 0;
37
+ let at = 0;
38
+ while (at < ref.length) {
39
+ if (next < pattern.length && (pattern[next] === "?" || pattern[next] === ref[at])) {
40
+ next += 1;
41
+ at += 1;
42
+ }
43
+ else if (next < pattern.length && pattern[next] === "*") {
44
+ star = next;
45
+ resume = at;
46
+ next += 1;
47
+ }
48
+ else if (star >= 0) {
49
+ resume += 1;
50
+ next = star + 1;
51
+ at = resume;
52
+ }
53
+ else {
54
+ return false;
55
+ }
56
+ }
57
+ while (next < pattern.length && pattern[next] === "*") {
58
+ next += 1;
59
+ }
60
+ return next === pattern.length;
61
+ }
62
+ /**
63
+ * Whether one configured string names the symbol reference ref. A string carrying
64
+ * neither special character names only the reference it spells.
65
+ *
66
+ * Both sides are read as code points: a character outside the basic plane is two
67
+ * string units, and `?` stands for one character.
68
+ */
69
+ export function matchRef(pattern, ref) {
70
+ if (!isPattern(pattern)) {
71
+ return pattern === ref;
72
+ }
73
+ return matchGlob(Array.from(pattern), Array.from(ref));
74
+ }
75
+ /** Two strings ordered bytewise, which is the order every set of a run is read in. */
76
+ function compare(a, b) {
77
+ if (a === b) {
78
+ return 0;
79
+ }
80
+ return a < b ? -1 : 1;
81
+ }
82
+ /** The members of one declaration, indexed by the container that holds them. */
83
+ function childrenOf(held) {
84
+ const byParent = new Map();
85
+ for (const symbol of held.symbols) {
86
+ const siblings = byParent.get(symbol.parent);
87
+ if (siblings === undefined) {
88
+ byParent.set(symbol.parent, [symbol]);
89
+ }
90
+ else {
91
+ siblings.push(symbol);
92
+ }
93
+ }
94
+ return byParent;
95
+ }
96
+ /**
97
+ * The roots one project holds, and every configured string that named nothing in it.
98
+ *
99
+ * A file a manifest member, an entry-file pattern or a tool's configuration names
100
+ * is a root with everything its module exports: each name of the module's export
101
+ * table, and where a name is a re-export, every link of the chain to the declaration
102
+ * it carries, so a re-exported declaration is a root of the file that publishes it.
103
+ * A file a runtime executes without reading its exports, a test file or a worker, is
104
+ * a root by itself.
105
+ */
106
+ export function roots(project, held, targetRoot, options) {
107
+ const files = sourceFilesOf(project, targetRoot);
108
+ const declared = held.declarations;
109
+ const fileIds = new Map();
110
+ for (const file of files.byName.values()) {
111
+ const id = declared.get(nodeKey(file, file));
112
+ if (id !== undefined) {
113
+ fileIds.set(file.fileName, id);
114
+ }
115
+ }
116
+ const found = new Map();
117
+ const add = (id, kind, source) => {
118
+ found.set([id, kind, source].join("\u0000"), { id, kind, source });
119
+ };
120
+ const entered = [];
121
+ const enter = (file, kind, source, exports) => {
122
+ entered.push({ file, kind, source, exports });
123
+ };
124
+ const published = [];
125
+ const publishes = (entry) => entry.published &&
126
+ (options.manifest.declaresExports ? entry.member.startsWith("exports") : true);
127
+ for (const entry of options.manifest.entries) {
128
+ for (const file of files.named(entry.path)) {
129
+ enter(file, entry.role === "run" ? "manifest-binary" : "manifest-entry", entry.member, true);
130
+ if (options.publishedAPI && publishes(entry)) {
131
+ published.push(file);
132
+ }
133
+ }
134
+ }
135
+ for (const pattern of options.entryFiles) {
136
+ const expression = globExpression(pattern);
137
+ for (const [path, file] of files.byPath) {
138
+ if (expression.test(path)) {
139
+ enter(file, "entry-file", pattern, true);
140
+ }
141
+ }
142
+ }
143
+ for (const point of entryPoints(project, files, options.testFiles)) {
144
+ enter(point.file, point.rule, point.source, point.exports);
145
+ }
146
+ const exportsOf = exportTables(project, held, [...entered.filter((entry) => entry.exports).map((entry) => entry.file), ...published], files.byName);
147
+ for (const entry of entered) {
148
+ const id = fileIds.get(entry.file.fileName);
149
+ if (id === undefined) {
150
+ continue;
151
+ }
152
+ add(id, entry.kind, entry.source);
153
+ if (entry.exports) {
154
+ for (const exported of exportsOf.get(entry.file.fileName) ?? []) {
155
+ add(exported, entry.kind, entry.source);
156
+ }
157
+ }
158
+ }
159
+ if (options.publishedAPI) {
160
+ for (const id of publishedAPI(published, exportsOf, held)) {
161
+ add(id, "published-api", "");
162
+ }
163
+ }
164
+ const unmatched = configured(held, options.patterns, add);
165
+ const sites = new Map(held.symbols.map((symbol) => [symbol.id, symbol.position]));
166
+ const ordered = [...found.values()].sort((a, b) => {
167
+ const left = sites.get(a.id);
168
+ const right = sites.get(b.id);
169
+ const site = left === undefined || right === undefined ? 0 : byPosition(left, right);
170
+ return site || compare(a.kind, b.kind) || compare(a.source, b.source);
171
+ });
172
+ return { configFile: project.configFile, liveUnderReachability: ordered, unmatched };
173
+ }
174
+ /**
175
+ * Every declaration each file's module exports, by the file's name: the declaration
176
+ * each name of the export table is, and every link of a re-export's chain. A name
177
+ * whose declaration is outside the project's own files belongs to another program
178
+ * and names nothing here.
179
+ *
180
+ * One batch resolves every file to its module and one request per module reads
181
+ * its export table, star re-exports resolved into it. A file that is no module
182
+ * exports nothing.
183
+ */
184
+ function exportTables(project, held, wanted, ownFiles) {
185
+ const unique = [...new Map(wanted.map((file) => [file.fileName, file])).values()];
186
+ const tables = new Map();
187
+ if (unique.length === 0) {
188
+ return tables;
189
+ }
190
+ const modules = project.symbolsAt(unique.map((file) => project.handle(file)));
191
+ const chains = new Map();
192
+ const chainOf = (symbol) => {
193
+ const cached = chains.get(symbol.id);
194
+ if (cached !== undefined) {
195
+ return cached;
196
+ }
197
+ const ids = [];
198
+ const walked = new Set();
199
+ let at = symbol;
200
+ while (at !== undefined && !walked.has(at.id)) {
201
+ walked.add(at.id);
202
+ for (const handle of at.declarations) {
203
+ if (!ownFiles.has(handle.path)) {
204
+ continue;
205
+ }
206
+ const node = project.declarationAt(handle)?.node;
207
+ const id = node === undefined
208
+ ? undefined
209
+ : held.declarations.get(nodeKey(node.getSourceFile(), node));
210
+ if (id !== undefined && !ids.includes(id)) {
211
+ ids.push(id);
212
+ }
213
+ }
214
+ at = project.aliasStepOf(at);
215
+ }
216
+ chains.set(symbol.id, ids);
217
+ return ids;
218
+ };
219
+ unique.forEach((file, index) => {
220
+ const module = modules[index];
221
+ if (module === undefined) {
222
+ tables.set(file.fileName, []);
223
+ return;
224
+ }
225
+ const ids = new Set();
226
+ for (const exported of project.checker.getExportsOfModule(module)) {
227
+ for (const id of chainOf(exported)) {
228
+ ids.add(id);
229
+ }
230
+ }
231
+ tables.set(file.fileName, [...ids]);
232
+ });
233
+ return tables;
234
+ }
235
+ /** The kinds of container whose export table, not its members' visibility, says what it publishes. */
236
+ const EXPORTING_CONTAINERS = new Set(["file", "namespace"]);
237
+ /**
238
+ * Every declaration a consumer of the library can name: what the published files
239
+ * export, and below each, the members a consumer reaches through it.
240
+ *
241
+ * The published files are the ones `exports` reaches where the manifest declares
242
+ * one, because a package that declares `exports` has said exactly what a consumer may
243
+ * import; where it declares none, they are the ones the members that preceded it
244
+ * name. Below a published module or namespace the walk follows what its export table
245
+ * names; below any other published declaration it follows every member a consumer
246
+ * can write: a public one, and a protected one, which a consumer's subclass names. A
247
+ * private member is not published, nor is anything below one, and a type parameter
248
+ * is not published either, because a consumer supplies a type argument by position.
249
+ *
250
+ * A published file is not itself published. A consumer imports a module and names
251
+ * what it exports; the file is a root because the manifest names it, which is a
252
+ * different reason.
253
+ */
254
+ function publishedAPI(published, exportsOf, held) {
255
+ const byId = new Map(held.symbols.map((symbol) => [symbol.id, symbol]));
256
+ const children = childrenOf(held);
257
+ const reached = new Set();
258
+ const pending = [];
259
+ for (const file of published) {
260
+ for (const id of exportsOf.get(file.fileName) ?? []) {
261
+ if (byId.get(id)?.kind === "file") {
262
+ pending.push(id);
263
+ }
264
+ else if (!reached.has(id)) {
265
+ reached.add(id);
266
+ pending.push(id);
267
+ }
268
+ }
269
+ }
270
+ for (let next = pending.shift(); next !== undefined; next = pending.shift()) {
271
+ const exporting = EXPORTING_CONTAINERS.has(byId.get(next)?.kind ?? "");
272
+ for (const member of children.get(next) ?? []) {
273
+ const reachable = exporting
274
+ ? member.exported
275
+ : member.visibility !== "private" && member.visibility !== "private-name";
276
+ if (member.kind === "type-parameter" || reached.has(member.id) || !reachable) {
277
+ continue;
278
+ }
279
+ reached.add(member.id);
280
+ pending.push(member.id);
281
+ }
282
+ }
283
+ return [...reached];
284
+ }
285
+ /**
286
+ * Keeps every declaration the configuration names and returns every string that
287
+ * named none, in configuration order. A string the configuration lists twice is one
288
+ * configured root.
289
+ */
290
+ function configured(held, patterns, add) {
291
+ const unmatched = [];
292
+ for (const pattern of new Set(patterns)) {
293
+ const kind = isPattern(pattern) ? "pattern" : "configured";
294
+ let matched = false;
295
+ for (const symbol of held.symbols) {
296
+ if (matchRef(pattern, symbol.ref)) {
297
+ add(symbol.id, kind, pattern);
298
+ matched = true;
299
+ }
300
+ }
301
+ if (!matched) {
302
+ unmatched.push(pattern);
303
+ }
304
+ }
305
+ return unmatched;
306
+ }
307
+ /**
308
+ * Every configured string that named no declaration in ANY project of the run, in
309
+ * configuration order and each once.
310
+ *
311
+ * A finding about a configured root is about the configuration and not about a
312
+ * declaration, so there is one per run: a string that names a declaration of one
313
+ * project has named something, and the projects it named nothing in are not a
314
+ * finding. A run with no project names nothing with any of them.
315
+ */
316
+ export function unmatchedEverywhere(patterns, perProject) {
317
+ return [...new Set(patterns)].filter((pattern) => perProject.every((project) => project.unmatched.includes(pattern)));
318
+ }
319
+ /**
320
+ * The findings a run reports for the configured strings that named nothing.
321
+ *
322
+ * `document` is the path of the document that declared the roots, below the target
323
+ * root; where the run read them from no document of the target it is empty and the
324
+ * finding names the conventional repository configuration, which is the document a
325
+ * maintainer writes a configured root in. Every finding of one run sits at that
326
+ * document's first position: the roots are written as an array whose members carry
327
+ * no line of their own, so the string each finding names is what tells one from
328
+ * another.
329
+ */
330
+ export function unmatchedRoots(sources, document) {
331
+ const position = {
332
+ path: document === "" ? REPOSITORY_DOCUMENT : document,
333
+ line: DOCUMENT_POSITION,
334
+ column: DOCUMENT_POSITION,
335
+ endLine: DOCUMENT_POSITION,
336
+ };
337
+ return sources.map((source) => ({
338
+ code: UNMATCHED_ROOT_CODE,
339
+ position,
340
+ symbol: { ref: source, kind: UNMATCHED_ROOT_SUBJECT, name: source, sizeLines: 1 },
341
+ message: isPattern(source)
342
+ ? "configured root pattern matches no symbol of the inventory"
343
+ : "configured root matches no symbol of the inventory",
344
+ }));
345
+ }
package/dist/src/run.js CHANGED
@@ -1,4 +1,5 @@
1
- import { ConfigError } from "./config.js";
1
+ import { runRoots } from "./analysis.js";
2
+ import { ConfigError, REPOSITORY_DOCUMENT } from "./config.js";
2
3
  import { diagnosticErrors, discoverProjects, DiscoveryError, renderDiagnostic, } from "./discover.js";
3
4
  import { joinPath, resolvePath } from "./paths.js";
4
5
  import { printConfig } from "./print.js";
@@ -6,10 +7,9 @@ import { resolve } from "./resolve.js";
6
7
  import { readScope, ScopeError, scopeForDir } from "./scope.js";
7
8
  import { diagnosticsOf, openEngine, runSession } from "./session.js";
8
9
  import { CONTRACT_VERSION } from "./version.js";
9
- /** The repository configuration's name at the target root. */
10
- const REPOSITORY_DOCUMENT = "deadset.json";
11
10
  /** The exit codes the Contract's exit-code table names. */
12
11
  const EXIT_CLEAN = 0;
12
+ const EXIT_FINDINGS = 1;
13
13
  const EXIT_USAGE = 2;
14
14
  const EXIT_FAILURE = 3;
15
15
  const VERBS = [
@@ -171,6 +171,9 @@ export function run(args, out, err, host, openClient = (collectTiming) => openEn
171
171
  if (verb === "print-projects") {
172
172
  return printProjectsVerb(args.slice(1), out, err, host, openClient);
173
173
  }
174
+ if (verb === "print-roots") {
175
+ return printRootsVerb(args.slice(1), out, err, host, openClient);
176
+ }
174
177
  if (verb !== undefined && VERBS.includes(verb)) {
175
178
  err.write(`deadset-ts: ${verb} is not implemented\n`);
176
179
  return EXIT_USAGE;
@@ -227,6 +230,48 @@ function printProjectsVerb(args, out, err, host, openClient) {
227
230
  return refuse(error, err);
228
231
  }
229
232
  }
233
+ /**
234
+ * One root as a line: the declaration's reference, why it is a root, and the string
235
+ * that named it, separated by tabs. A run of several configurations adds the ones
236
+ * whose project holds the root as a fourth field, and writes the third present and
237
+ * empty where no string named the root, so every line of one run holds the same
238
+ * fields.
239
+ */
240
+ function rootLine(root, several) {
241
+ const line = `${root.ref}\t${root.kind}`;
242
+ if (several) {
243
+ return `${line}\t${root.source}\t${root.configurations.join(" ")}`;
244
+ }
245
+ return root.source === "" ? line : `${line}\t${root.source}`;
246
+ }
247
+ /**
248
+ * Writes the root set of the run, one root per line, and then names on the error
249
+ * stream, by its issue kind, every configured root or pattern that named nothing;
250
+ * one such string fails the run. The line shape is this command's own rather than a
251
+ * Contract format, so that sort and cut read it.
252
+ */
253
+ function printRootsVerb(args, out, err, host, openClient) {
254
+ let engine;
255
+ try {
256
+ const options = readOptions(args);
257
+ const { config, provenance } = resolve(inputsOf(host, options));
258
+ const scope = scopeOf(host, options);
259
+ engine = openClient(false);
260
+ const answer = runRoots(engine, host, scope, config, provenance);
261
+ const several = answer.configurations.length > 1;
262
+ for (const root of answer.roots) {
263
+ out.write(`${rootLine(root, several)}\n`);
264
+ }
265
+ for (const finding of answer.findings) {
266
+ err.write(`${finding.code}: roots.patterns names nothing: ${finding.symbol.ref}\n`);
267
+ }
268
+ return answer.findings.length > 0 ? EXIT_FINDINGS : EXIT_CLEAN;
269
+ }
270
+ catch (error) {
271
+ engine?.close();
272
+ return refuse(error, err);
273
+ }
274
+ }
230
275
  /** Turns one refusal into the exit code the Contract's table gives it. */
231
276
  function refuse(error, err) {
232
277
  if (error instanceof ConfigError) {
@@ -1,4 +1,4 @@
1
- import { API } from "@typescript/native/unstable/sync";
1
+ import { API, SymbolFlags } from "@typescript/native/unstable/sync";
2
2
  import { isIdentifier } from "@typescript/native/unstable/ast";
3
3
  /**
4
4
  * Opens the compiler client the analysis reads through.
@@ -53,6 +53,11 @@ function viewOf(project) {
53
53
  resolvedSymbolAt: ({ node }) => isIdentifier(node) ? project.checker.getResolvedSymbol(node) : undefined,
54
54
  shorthandValueAt: ({ node }) => project.checker.getShorthandAssignmentValueSymbol(node),
55
55
  aliasStepOf: (symbol) => {
56
+ // The step is asked of an alias only: asked of any other symbol, the compiler
57
+ // fails an assertion and the session ends.
58
+ if ((symbol.flags & SymbolFlags.Alias) === 0) {
59
+ return undefined;
60
+ }
56
61
  const held = project.checker.getImmediateAliasedSymbol(symbol);
57
62
  // An alias that resolves to nothing answers with the checker's own unknown
58
63
  // symbol, which declares nothing; a symbol that is no alias answers with