@groma/scanner-react 0.1.2 → 0.1.4
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 +129 -11
- package/dist/index.js +1385 -112
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,8 +1,541 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
// plugins/scanners/react/src/
|
|
3
|
-
import
|
|
4
|
-
|
|
2
|
+
// plugins/scanners/react/src/index.ts
|
|
3
|
+
import ts5 from "./typescript.cjs";
|
|
4
|
+
|
|
5
|
+
// plugins/scanners/typescript-outline.ts
|
|
6
|
+
import { readFile } from "fs/promises";
|
|
7
|
+
import path from "path";
|
|
8
|
+
function is(node, kind) {
|
|
9
|
+
return node !== undefined && node.kind === kind;
|
|
10
|
+
}
|
|
11
|
+
var NO_EXPORTS = new Set;
|
|
12
|
+
function hasModifier(node, kind) {
|
|
13
|
+
return node.modifiers?.some((modifier) => modifier.kind === kind) ?? false;
|
|
14
|
+
}
|
|
15
|
+
function listedExports(k, statement) {
|
|
16
|
+
if (is(statement, k.ExportAssignment)) {
|
|
17
|
+
return is(statement.expression, k.Identifier) ? [statement.expression.text] : [];
|
|
18
|
+
}
|
|
19
|
+
if (!is(statement, k.ExportDeclaration) || statement.moduleSpecifier !== undefined)
|
|
20
|
+
return [];
|
|
21
|
+
const clause = statement.exportClause;
|
|
22
|
+
if (!is(clause, k.NamedExports))
|
|
23
|
+
return [];
|
|
24
|
+
return clause.elements.map((element) => (element.propertyName ?? element.name).text);
|
|
25
|
+
}
|
|
26
|
+
function topLevelVisibility(scope, name, statement) {
|
|
27
|
+
if (scope.topLevelPrivate)
|
|
28
|
+
return "private";
|
|
29
|
+
return hasModifier(statement, scope.k.ExportKeyword) || scope.exported.has(name) ? "public" : "private";
|
|
30
|
+
}
|
|
31
|
+
function memberVisibility(scope, member) {
|
|
32
|
+
const { k } = scope;
|
|
33
|
+
if (is(member.name, k.PrivateIdentifier) || hasModifier(member, k.PrivateKeyword))
|
|
34
|
+
return "private";
|
|
35
|
+
return hasModifier(member, k.ProtectedKeyword) ? "protected" : "public";
|
|
36
|
+
}
|
|
37
|
+
function symbolAt(scope, name, node, visibility) {
|
|
38
|
+
const line = scope.source.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
39
|
+
return { name, line, visibility, entry: scope.symbols.includes(name) };
|
|
40
|
+
}
|
|
41
|
+
function functionDeclarations(scope, statement) {
|
|
42
|
+
const { k } = scope;
|
|
43
|
+
if (is(statement, k.FunctionDeclaration) && is(statement.name, k.Identifier)) {
|
|
44
|
+
const name = statement.name.text;
|
|
45
|
+
return [{ kind: "function", ...symbolAt(scope, name, statement.name, topLevelVisibility(scope, name, statement)) }];
|
|
46
|
+
}
|
|
47
|
+
if (!is(statement, k.VariableStatement))
|
|
48
|
+
return [];
|
|
49
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
50
|
+
const { name, initializer } = declaration;
|
|
51
|
+
if (!is(name, k.Identifier))
|
|
52
|
+
return [];
|
|
53
|
+
if (!is(initializer, k.ArrowFunction) && !is(initializer, k.FunctionExpression))
|
|
54
|
+
return [];
|
|
55
|
+
return [{ kind: "function", ...symbolAt(scope, name.text, name, topLevelVisibility(scope, name.text, statement)) }];
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
function memberName(scope, name) {
|
|
59
|
+
const { k } = scope;
|
|
60
|
+
if (is(name, k.Identifier) || is(name, k.PrivateIdentifier))
|
|
61
|
+
return name;
|
|
62
|
+
return is(name, k.StringLiteral) || is(name, k.NumericLiteral) ? name : undefined;
|
|
63
|
+
}
|
|
64
|
+
function typeMember(scope, member) {
|
|
65
|
+
const { k } = scope;
|
|
66
|
+
if (member.kind === k.Constructor)
|
|
67
|
+
return [symbolAt(scope, "constructor", member, memberVisibility(scope, member))];
|
|
68
|
+
if (!is(member, k.MethodDeclaration) && !is(member, k.MethodSignature))
|
|
69
|
+
return [];
|
|
70
|
+
const name = memberName(scope, member.name);
|
|
71
|
+
return name === undefined ? [] : [symbolAt(scope, name.text, name, memberVisibility(scope, member))];
|
|
72
|
+
}
|
|
73
|
+
function typeMembers(k, statement) {
|
|
74
|
+
if (is(statement, k.ClassDeclaration) || is(statement, k.InterfaceDeclaration))
|
|
75
|
+
return statement.members;
|
|
76
|
+
return statement.kind === k.EnumDeclaration ? [] : undefined;
|
|
77
|
+
}
|
|
78
|
+
function typeDeclaration(scope, statement) {
|
|
79
|
+
const members = typeMembers(scope.k, statement);
|
|
80
|
+
const { name } = statement;
|
|
81
|
+
if (members === undefined || !is(name, scope.k.Identifier))
|
|
82
|
+
return [];
|
|
83
|
+
return [{
|
|
84
|
+
kind: "type",
|
|
85
|
+
...symbolAt(scope, name.text, name, topLevelVisibility(scope, name.text, statement)),
|
|
86
|
+
members: members.flatMap((member) => typeMember(scope, member))
|
|
87
|
+
}];
|
|
88
|
+
}
|
|
89
|
+
function moduleStatements(k, statement) {
|
|
90
|
+
let body = statement.body;
|
|
91
|
+
while (is(body, k.ModuleDeclaration))
|
|
92
|
+
body = body.body;
|
|
93
|
+
return is(body, k.ModuleBlock) ? body.statements : [];
|
|
94
|
+
}
|
|
95
|
+
function declarationsIn(scope, statements) {
|
|
96
|
+
return statements.flatMap((statement) => {
|
|
97
|
+
if (is(statement, scope.k.ModuleDeclaration)) {
|
|
98
|
+
return declarationsIn({ ...scope, exported: NO_EXPORTS }, moduleStatements(scope.k, statement));
|
|
99
|
+
}
|
|
100
|
+
return [...functionDeclarations(scope, statement), ...typeDeclaration(scope, statement)];
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function outlineSource(ts, source, context) {
|
|
104
|
+
const k = ts.SyntaxKind;
|
|
105
|
+
const exported = new Set([...source.statements.flatMap((statement) => listedExports(k, statement)), ...context.exported ?? []]);
|
|
106
|
+
const scope = { k, source, symbols: context.symbols, exported, topLevelPrivate: context.topLevelPrivate ?? false };
|
|
107
|
+
return declarationsIn(scope, source.statements);
|
|
108
|
+
}
|
|
109
|
+
function outlineDeclarations(ts, block) {
|
|
110
|
+
return outlineSource(ts, ts.createSourceFile(block.fileName, block.text, ts.ScriptTarget.Latest, true), block);
|
|
111
|
+
}
|
|
112
|
+
async function readTypeScriptOutline(ts, repositoryRoot, references) {
|
|
113
|
+
const files = [];
|
|
114
|
+
for (const reference of references) {
|
|
115
|
+
const fileName = path.join(repositoryRoot, reference.file);
|
|
116
|
+
const text = await readFile(fileName, "utf8");
|
|
117
|
+
const declarations = outlineDeclarations(ts, { fileName, text, symbols: reference.symbols });
|
|
118
|
+
if (declarations.length > 0)
|
|
119
|
+
files.push({ file: reference.file, declarations });
|
|
120
|
+
}
|
|
121
|
+
return files;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// plugins/scanners/typescript-project.ts
|
|
125
|
+
import path3 from "path";
|
|
126
|
+
|
|
127
|
+
// plugins/scanners/projects.ts
|
|
128
|
+
import { execFile } from "child_process";
|
|
129
|
+
import { existsSync } from "fs";
|
|
130
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
5
131
|
import path2 from "path";
|
|
132
|
+
import { promisify } from "util";
|
|
133
|
+
var execute = promisify(execFile);
|
|
134
|
+
var excluded = new Set([
|
|
135
|
+
".git",
|
|
136
|
+
"node_modules",
|
|
137
|
+
"vendor",
|
|
138
|
+
"target",
|
|
139
|
+
"dist",
|
|
140
|
+
"build",
|
|
141
|
+
"bin",
|
|
142
|
+
"obj",
|
|
143
|
+
".gradle",
|
|
144
|
+
".angular",
|
|
145
|
+
"coverage",
|
|
146
|
+
"generated",
|
|
147
|
+
"groma",
|
|
148
|
+
".groma"
|
|
149
|
+
]);
|
|
150
|
+
async function repositoryFiles(root, matches) {
|
|
151
|
+
const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
|
|
152
|
+
return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && existsSync(path2.join(root, file))))].sort();
|
|
153
|
+
}
|
|
154
|
+
function projectFiles(root, matches) {
|
|
155
|
+
return repositoryFiles(root, (file) => matches(file) && !file.split("/").some((part) => excluded.has(part)));
|
|
156
|
+
}
|
|
157
|
+
function hasDependency(manifest, dependency) {
|
|
158
|
+
return ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].some((section) => typeof manifest[section]?.[dependency] === "string");
|
|
159
|
+
}
|
|
160
|
+
async function frameworkProjects(root, dependency, extensions) {
|
|
161
|
+
const files = await projectFiles(root, () => true);
|
|
162
|
+
const manifests = files.filter((file) => path2.posix.basename(file) === "package.json");
|
|
163
|
+
const directories = new Set(manifests.map((file) => path2.posix.dirname(file)));
|
|
164
|
+
const sources = new Set;
|
|
165
|
+
for (const file of files.filter((file) => extensions.some((extension) => file.endsWith(extension)) && !file.endsWith(".d.ts"))) {
|
|
166
|
+
let directory = path2.posix.dirname(file);
|
|
167
|
+
while (directory !== "." && !directories.has(directory))
|
|
168
|
+
directory = path2.posix.dirname(directory);
|
|
169
|
+
sources.add(directory);
|
|
170
|
+
}
|
|
171
|
+
const projects = [];
|
|
172
|
+
for (const file of manifests) {
|
|
173
|
+
const directory = path2.posix.dirname(file);
|
|
174
|
+
if (!sources.has(directory) || !files.includes(path2.posix.join(directory, "tsconfig.json")))
|
|
175
|
+
continue;
|
|
176
|
+
const manifest = JSON.parse(await readFile2(path2.join(root, file), "utf8"));
|
|
177
|
+
if (hasDependency(manifest, dependency))
|
|
178
|
+
projects.push(path2.dirname(path2.join(root, file)));
|
|
179
|
+
}
|
|
180
|
+
return projects;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// plugins/scanners/typescript-project.ts
|
|
184
|
+
async function frameworkSourceFiles(options) {
|
|
185
|
+
const { root, dependency, projects, sources, also } = options;
|
|
186
|
+
const directories = (await frameworkProjects(root, dependency, projects)).map((directory) => path3.relative(root, directory).split(path3.sep).join("/") || ".");
|
|
187
|
+
if (directories.length === 0)
|
|
188
|
+
return [];
|
|
189
|
+
const listed = [];
|
|
190
|
+
for (const file of await projectFiles(root, (candidate) => !candidate.endsWith(".d.ts"))) {
|
|
191
|
+
const project = directories.find((directory) => directory === "." || file.startsWith(`${directory}/`));
|
|
192
|
+
if (project === undefined)
|
|
193
|
+
continue;
|
|
194
|
+
const local = project === "." ? file : file.slice(project.length + 1);
|
|
195
|
+
if (sources.some((extension) => file.endsWith(extension)) || (also?.(local) ?? false))
|
|
196
|
+
listed.push(file);
|
|
197
|
+
}
|
|
198
|
+
return listed.sort();
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// plugins/scanners/react/src/routes.ts
|
|
202
|
+
import { existsSync as existsSync2 } from "fs";
|
|
203
|
+
import path4 from "path";
|
|
204
|
+
import ts2 from "./typescript.cjs";
|
|
205
|
+
|
|
206
|
+
// plugins/scanners/http-url.ts
|
|
207
|
+
var pathText = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
|
|
208
|
+
var absolute = /^([A-Za-z][A-Za-z\d+.-]*:)?\/\//;
|
|
209
|
+
var HOLE = "\x00";
|
|
210
|
+
var computedPart = { kind: "hole", configured: false };
|
|
211
|
+
var configuredPart = { kind: "hole", configured: true };
|
|
212
|
+
var methodToken = /^[A-Z][A-Z-]*$/;
|
|
213
|
+
function methodText(text) {
|
|
214
|
+
const method = text?.toUpperCase();
|
|
215
|
+
return method !== undefined && methodToken.test(method) ? method : undefined;
|
|
216
|
+
}
|
|
217
|
+
function requestSegment(part) {
|
|
218
|
+
if (part === HOLE)
|
|
219
|
+
return { kind: "dynamic" };
|
|
220
|
+
if (part.includes(HOLE) || !pathText.test(part))
|
|
221
|
+
return { kind: "unknown" };
|
|
222
|
+
return { kind: "literal", value: part };
|
|
223
|
+
}
|
|
224
|
+
function segmentsOf(parts) {
|
|
225
|
+
const url = parts.map((part) => part.kind === "text" ? part.text : HOLE).join("");
|
|
226
|
+
const end = Math.min(...["?", "#"].map((mark) => url.includes(mark) ? url.indexOf(mark) : url.length));
|
|
227
|
+
return url.slice(0, end).split("/").filter((part) => part !== "").map(requestSegment);
|
|
228
|
+
}
|
|
229
|
+
function afterAuthority(text) {
|
|
230
|
+
const slash = text.indexOf("/", text.indexOf("//") + 2);
|
|
231
|
+
return slash < 0 ? "" : text.slice(slash);
|
|
232
|
+
}
|
|
233
|
+
function joinedText(input) {
|
|
234
|
+
const parts = [];
|
|
235
|
+
for (const part of input) {
|
|
236
|
+
const last = parts.at(-1);
|
|
237
|
+
if (part.kind === "text" && last?.kind === "text")
|
|
238
|
+
parts[parts.length - 1] = { kind: "text", text: last.text + part.text };
|
|
239
|
+
else if (part.kind === "hole" || part.text !== "")
|
|
240
|
+
parts.push(part);
|
|
241
|
+
}
|
|
242
|
+
return parts;
|
|
243
|
+
}
|
|
244
|
+
function requestUrl(input) {
|
|
245
|
+
const parts = joinedText(input);
|
|
246
|
+
const [first, ...rest] = parts;
|
|
247
|
+
if (first === undefined)
|
|
248
|
+
return { path: [{ kind: "unknown" }] };
|
|
249
|
+
if (first.kind === "hole") {
|
|
250
|
+
const path = segmentsOf(rest);
|
|
251
|
+
const next = rest[0];
|
|
252
|
+
const separated = next === undefined || next.kind === "text" && next.text.startsWith("/");
|
|
253
|
+
return first.configured && separated ? { configured: true, path } : { path: [{ kind: "unknown" }, ...path] };
|
|
254
|
+
}
|
|
255
|
+
if (absolute.test(first.text)) {
|
|
256
|
+
return { path: [{ kind: "unknown" }, ...segmentsOf([{ kind: "text", text: afterAuthority(first.text) }, ...rest])] };
|
|
257
|
+
}
|
|
258
|
+
if (!first.text.startsWith("/"))
|
|
259
|
+
return { path: [{ kind: "unknown" }, ...segmentsOf(parts)] };
|
|
260
|
+
return { path: segmentsOf(parts) };
|
|
261
|
+
}
|
|
262
|
+
function joinBase(base, url) {
|
|
263
|
+
const stated = joinedText(base);
|
|
264
|
+
const path = joinedText(url);
|
|
265
|
+
const [first] = path;
|
|
266
|
+
if (stated.length === 0)
|
|
267
|
+
return path;
|
|
268
|
+
if (first === undefined)
|
|
269
|
+
return stated;
|
|
270
|
+
if (first.kind === "hole")
|
|
271
|
+
return [computedPart, ...path.slice(1)];
|
|
272
|
+
return absolute.test(first.text) ? path : [...stated, { kind: "text", text: "/" }, ...path];
|
|
273
|
+
}
|
|
274
|
+
function withBase(base, url) {
|
|
275
|
+
const stated = joinedText(base);
|
|
276
|
+
const path = joinedText(url);
|
|
277
|
+
const [only] = stated;
|
|
278
|
+
const [first, ...rest] = path;
|
|
279
|
+
if (stated.length !== 1 || only?.kind !== "text" || first?.kind !== "text" || absolute.test(first.text)) {
|
|
280
|
+
return joinBase(stated, path);
|
|
281
|
+
}
|
|
282
|
+
const prefix = only.text.replace(/\/$/, "");
|
|
283
|
+
if (first.text.startsWith(prefix)) {
|
|
284
|
+
const next = first.text[prefix.length];
|
|
285
|
+
if (next === undefined)
|
|
286
|
+
return rest.length === 0 ? path : [computedPart, ...rest];
|
|
287
|
+
return next === "/" || next === "?" ? path : joinBase(stated, path);
|
|
288
|
+
}
|
|
289
|
+
return prefix.startsWith(first.text) && rest.length > 0 ? [computedPart, ...rest] : joinBase(stated, path);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// plugins/scanners/react/src/functions.ts
|
|
293
|
+
import ts from "./typescript.cjs";
|
|
294
|
+
function executable(node) {
|
|
295
|
+
return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) && !!node.body;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// plugins/scanners/react/src/routes.ts
|
|
299
|
+
var METHODS = new Set(["GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"]);
|
|
300
|
+
var APP_ROUTE = /^(?:(.+)\/)?route\.tsx?$/;
|
|
301
|
+
var PAGES_API = /^(api\/.+)\.tsx?$/;
|
|
302
|
+
var OPTIONAL_CATCH_ALL = /^\[\[\.\.\.(.+)\]\]$/;
|
|
303
|
+
var CATCH_ALL = /^\[\.\.\.(.+)\]$/;
|
|
304
|
+
var PARAMETER = /^\[(.+)\]$/;
|
|
305
|
+
function nextRouters(projectRoot) {
|
|
306
|
+
const active = (name) => existsSync2(path4.join(projectRoot, name)) ? name : `src/${name}`;
|
|
307
|
+
return { app: active("app"), pages: active("pages") };
|
|
308
|
+
}
|
|
309
|
+
function routeLocation(file, routers) {
|
|
310
|
+
const within = (directory) => file.startsWith(`${directory}/`) ? file.slice(directory.length + 1) : undefined;
|
|
311
|
+
const app = APP_ROUTE.exec(within(routers.app) ?? "");
|
|
312
|
+
if (app)
|
|
313
|
+
return { router: "app", ...app[1] === undefined ? {} : { directory: app[1] } };
|
|
314
|
+
const pages = PAGES_API.exec(within(routers.pages) ?? "");
|
|
315
|
+
return pages ? { router: "pages", directory: pages[1] } : undefined;
|
|
316
|
+
}
|
|
317
|
+
function routeCandidate(file) {
|
|
318
|
+
return ["", "src/"].some((prefix) => routeLocation(file, { app: `${prefix}app`, pages: `${prefix}pages` }) !== undefined);
|
|
319
|
+
}
|
|
320
|
+
var GROUP = /^\((?!\.+\))[^)]+\)$/;
|
|
321
|
+
function routeSegment(part, last) {
|
|
322
|
+
const optional = OPTIONAL_CATCH_ALL.exec(part);
|
|
323
|
+
if (optional)
|
|
324
|
+
return last && pathText.test(optional[1]) ? { kind: "catch-all", name: optional[1], optional: true } : undefined;
|
|
325
|
+
const rest = CATCH_ALL.exec(part);
|
|
326
|
+
if (rest)
|
|
327
|
+
return last && pathText.test(rest[1]) ? { kind: "catch-all", name: rest[1] } : undefined;
|
|
328
|
+
const parameter = PARAMETER.exec(part);
|
|
329
|
+
if (parameter)
|
|
330
|
+
return pathText.test(parameter[1]) ? { kind: "parameter", name: parameter[1] } : undefined;
|
|
331
|
+
if (part.startsWith("(") || part.startsWith("@") || part.includes("[") || part.includes("]"))
|
|
332
|
+
return;
|
|
333
|
+
return pathText.test(part) ? { kind: "literal", value: part } : undefined;
|
|
334
|
+
}
|
|
335
|
+
function routeParts(directory, stripIndex) {
|
|
336
|
+
const parts = (directory ?? "").split("/").filter((part) => part !== "" && !GROUP.test(part));
|
|
337
|
+
return stripIndex && parts.at(-1) === "index" ? parts.slice(0, -1) : parts;
|
|
338
|
+
}
|
|
339
|
+
function routePath(directory, stripIndex) {
|
|
340
|
+
const parts = routeParts(directory, stripIndex);
|
|
341
|
+
const segments = [];
|
|
342
|
+
for (const [index, part] of parts.entries()) {
|
|
343
|
+
const segment = routeSegment(part, index === parts.length - 1);
|
|
344
|
+
if (segment === undefined)
|
|
345
|
+
return;
|
|
346
|
+
segments.push(segment);
|
|
347
|
+
}
|
|
348
|
+
return segments;
|
|
349
|
+
}
|
|
350
|
+
function functionValue(node) {
|
|
351
|
+
return node !== undefined && executable(node) ? node : undefined;
|
|
352
|
+
}
|
|
353
|
+
function hasModifier2(statement, kind) {
|
|
354
|
+
return ts2.canHaveModifiers(statement) && (ts2.getModifiers(statement) ?? []).some((modifier) => modifier.kind === kind);
|
|
355
|
+
}
|
|
356
|
+
function declaredHandler(statement) {
|
|
357
|
+
if (!ts2.isFunctionDeclaration(statement) || statement.name === undefined)
|
|
358
|
+
return;
|
|
359
|
+
const handler = functionValue(statement);
|
|
360
|
+
return handler !== undefined && METHODS.has(statement.name.text) ? [statement.name.text, handler] : undefined;
|
|
361
|
+
}
|
|
362
|
+
function boundHandlers(statement) {
|
|
363
|
+
if (!ts2.isVariableStatement(statement))
|
|
364
|
+
return [];
|
|
365
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
366
|
+
const handler = functionValue(declaration.initializer);
|
|
367
|
+
if (handler === undefined || !ts2.isIdentifier(declaration.name) || !METHODS.has(declaration.name.text))
|
|
368
|
+
return [];
|
|
369
|
+
return [[declaration.name.text, handler]];
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function appHandlers(source) {
|
|
373
|
+
const exported = source.statements.filter((statement) => hasModifier2(statement, ts2.SyntaxKind.ExportKeyword));
|
|
374
|
+
return new Map(exported.flatMap((statement) => {
|
|
375
|
+
const declared = declaredHandler(statement);
|
|
376
|
+
return declared === undefined ? boundHandlers(statement) : [declared];
|
|
377
|
+
}));
|
|
378
|
+
}
|
|
379
|
+
function localFunction(source, name) {
|
|
380
|
+
for (const statement of source.statements) {
|
|
381
|
+
if (ts2.isFunctionDeclaration(statement) && statement.name?.text === name)
|
|
382
|
+
return functionValue(statement);
|
|
383
|
+
if (!ts2.isVariableStatement(statement))
|
|
384
|
+
continue;
|
|
385
|
+
for (const declaration of statement.declarationList.declarations) {
|
|
386
|
+
if (ts2.isIdentifier(declaration.name) && declaration.name.text === name)
|
|
387
|
+
return functionValue(declaration.initializer);
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
function pagesHandler(source) {
|
|
393
|
+
for (const statement of source.statements) {
|
|
394
|
+
if (ts2.isFunctionDeclaration(statement) && hasModifier2(statement, ts2.SyntaxKind.DefaultKeyword))
|
|
395
|
+
return functionValue(statement);
|
|
396
|
+
if (!ts2.isExportAssignment(statement) || statement.isExportEquals === true)
|
|
397
|
+
continue;
|
|
398
|
+
const value = functionValue(statement.expression);
|
|
399
|
+
if (value !== undefined)
|
|
400
|
+
return value;
|
|
401
|
+
if (ts2.isIdentifier(statement.expression))
|
|
402
|
+
return localFunction(source, statement.expression.text);
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
406
|
+
function nextRouteEndpoints(sources, projectRoot, routers, operationId) {
|
|
407
|
+
const endpoints = [];
|
|
408
|
+
for (const source of sources) {
|
|
409
|
+
const location = routeLocation(path4.relative(projectRoot, source.fileName).split(path4.sep).join("/"), routers);
|
|
410
|
+
const segments = location === undefined ? undefined : routePath(location.directory, location.router === "pages");
|
|
411
|
+
if (location === undefined || segments === undefined)
|
|
412
|
+
continue;
|
|
413
|
+
if (location.router === "app") {
|
|
414
|
+
for (const [method, handler] of appHandlers(source)) {
|
|
415
|
+
endpoints.push({ operation: operationId(handler), method, path: segments });
|
|
416
|
+
}
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
const handler = pagesHandler(source);
|
|
420
|
+
if (handler !== undefined)
|
|
421
|
+
endpoints.push({ operation: operationId(handler), method: "*", path: segments });
|
|
422
|
+
}
|
|
423
|
+
return endpoints;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// plugins/scanners/react/src/scan.ts
|
|
427
|
+
import { existsSync as existsSync3, readFileSync } from "fs";
|
|
428
|
+
import path5 from "path";
|
|
429
|
+
|
|
430
|
+
// packages/scanner/src/values.ts
|
|
431
|
+
function object(value, label) {
|
|
432
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
433
|
+
throw new Error(`${label} must be an object`);
|
|
434
|
+
}
|
|
435
|
+
return value;
|
|
436
|
+
}
|
|
437
|
+
function string(value, label) {
|
|
438
|
+
if (typeof value !== "string")
|
|
439
|
+
throw new Error(`${label} must be a string`);
|
|
440
|
+
return value;
|
|
441
|
+
}
|
|
442
|
+
function array(value, label) {
|
|
443
|
+
if (!Array.isArray(value))
|
|
444
|
+
throw new Error(`${label} must be an array`);
|
|
445
|
+
return value;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
// packages/scanner/src/http.ts
|
|
449
|
+
var pathText2 = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
|
|
450
|
+
var methodToken2 = /^[A-Z][A-Z-]*$/;
|
|
451
|
+
function fail(message) {
|
|
452
|
+
throw new Error(message);
|
|
453
|
+
}
|
|
454
|
+
function text(value, label) {
|
|
455
|
+
if (typeof value !== "string" || !pathText2.test(value))
|
|
456
|
+
fail(`${label} must be nonempty URL path characters`);
|
|
457
|
+
return value;
|
|
458
|
+
}
|
|
459
|
+
function method(value, label, anyMethod) {
|
|
460
|
+
if (typeof value !== "string" || !(methodToken2.test(value) || anyMethod && value === "*")) {
|
|
461
|
+
fail(`${label} must be an uppercase HTTP method${anyMethod ? " or *" : ""}`);
|
|
462
|
+
}
|
|
463
|
+
return value;
|
|
464
|
+
}
|
|
465
|
+
function operation(value, operations) {
|
|
466
|
+
if (typeof value !== "string" || !operations.has(value))
|
|
467
|
+
fail("HTTP fact references unknown operation");
|
|
468
|
+
return value;
|
|
469
|
+
}
|
|
470
|
+
function endpointSegment(value, last) {
|
|
471
|
+
const segment = object(value, "HTTP endpoint segment");
|
|
472
|
+
if (segment.kind === "literal")
|
|
473
|
+
return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
|
|
474
|
+
if (segment.kind !== "parameter" && segment.kind !== "catch-all")
|
|
475
|
+
fail(`unknown HTTP endpoint segment kind: ${String(segment.kind)}`);
|
|
476
|
+
if (segment.kind === "catch-all" && !last)
|
|
477
|
+
fail("an HTTP catch-all segment must be last");
|
|
478
|
+
for (const flag of ["optional", "constrained"]) {
|
|
479
|
+
if (segment[flag] !== undefined && typeof segment[flag] !== "boolean")
|
|
480
|
+
fail(`HTTP segment ${flag} must be a boolean`);
|
|
481
|
+
}
|
|
482
|
+
return {
|
|
483
|
+
kind: segment.kind,
|
|
484
|
+
name: text(segment.name, "HTTP segment name"),
|
|
485
|
+
...segment.optional ? { optional: true } : {},
|
|
486
|
+
...segment.constrained ? { constrained: true } : {}
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
function requestSegment2(value) {
|
|
490
|
+
const segment = object(value, "HTTP request segment");
|
|
491
|
+
if (segment.kind === "literal")
|
|
492
|
+
return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
|
|
493
|
+
if (segment.kind === "dynamic" || segment.kind === "unknown")
|
|
494
|
+
return { kind: segment.kind };
|
|
495
|
+
return fail(`unknown HTTP request segment kind: ${String(segment.kind)}`);
|
|
496
|
+
}
|
|
497
|
+
function order(value) {
|
|
498
|
+
const fact = object(value, "HTTP endpoint order");
|
|
499
|
+
const application = string(fact.application, "HTTP endpoint order application");
|
|
500
|
+
if (application === "")
|
|
501
|
+
fail("HTTP endpoint order application must be nonempty");
|
|
502
|
+
const position = fact.position;
|
|
503
|
+
if (typeof position !== "number" || !Number.isInteger(position) || position < 0) {
|
|
504
|
+
fail("HTTP endpoint order position must be a nonnegative integer");
|
|
505
|
+
}
|
|
506
|
+
return { application, position };
|
|
507
|
+
}
|
|
508
|
+
function endpoint(value, operations) {
|
|
509
|
+
const fact = object(value, "HTTP endpoint");
|
|
510
|
+
const path = array(fact.path, "HTTP endpoint path");
|
|
511
|
+
return {
|
|
512
|
+
operation: operation(fact.operation, operations),
|
|
513
|
+
method: method(fact.method, "HTTP endpoint method", true),
|
|
514
|
+
path: path.map((segment, index) => endpointSegment(segment, index === path.length - 1)),
|
|
515
|
+
...fact.order === undefined ? {} : { order: order(fact.order) }
|
|
516
|
+
};
|
|
517
|
+
}
|
|
518
|
+
function request(value, operations) {
|
|
519
|
+
const fact = object(value, "HTTP request");
|
|
520
|
+
if (fact.configured !== undefined && typeof fact.configured !== "boolean")
|
|
521
|
+
fail("HTTP request configured must be a boolean");
|
|
522
|
+
return {
|
|
523
|
+
operation: operation(fact.operation, operations),
|
|
524
|
+
...fact.method === undefined ? {} : { method: method(fact.method, "HTTP request method", false) },
|
|
525
|
+
...fact.configured ? { configured: true } : {},
|
|
526
|
+
path: array(fact.path, "HTTP request path").map(requestSegment2)
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
function httpEvidence(endpoints, requests, operations) {
|
|
530
|
+
if (endpoints === undefined && requests === undefined)
|
|
531
|
+
return {};
|
|
532
|
+
if (operations === undefined)
|
|
533
|
+
fail("HTTP facts require operation declarations");
|
|
534
|
+
return {
|
|
535
|
+
...endpoints === undefined ? {} : { httpEndpoints: array(endpoints, "httpEndpoints").map((fact) => endpoint(fact, operations)) },
|
|
536
|
+
...requests === undefined ? {} : { httpRequests: array(requests, "httpRequests").map((fact) => request(fact, operations)) }
|
|
537
|
+
};
|
|
538
|
+
}
|
|
6
539
|
|
|
7
540
|
// packages/scanner/src/index.ts
|
|
8
541
|
function compare(...values) {
|
|
@@ -47,7 +580,9 @@ function createScanObservation(input) {
|
|
|
47
580
|
scanner: input.scanner,
|
|
48
581
|
roots,
|
|
49
582
|
files,
|
|
583
|
+
...sourceUnits(input.sourceUnits, filePaths),
|
|
50
584
|
...operationEvidence(input, filePaths),
|
|
585
|
+
...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
|
|
51
586
|
diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
|
|
52
587
|
diagnosticKey(diagnostic),
|
|
53
588
|
diagnostic
|
|
@@ -56,6 +591,23 @@ function createScanObservation(input) {
|
|
|
56
591
|
})
|
|
57
592
|
};
|
|
58
593
|
}
|
|
594
|
+
function sourceUnits(input, paths) {
|
|
595
|
+
if (input === undefined)
|
|
596
|
+
return {};
|
|
597
|
+
const units = array(input, "sourceUnits").map((entry) => {
|
|
598
|
+
const unit = object(entry, "source unit");
|
|
599
|
+
const primary = string(unit.primary, "source unit.primary");
|
|
600
|
+
const files = [...new Set(array(unit.files, "source unit.files").map((file) => string(file, "source unit.file")))].sort();
|
|
601
|
+
if (!files.includes(primary))
|
|
602
|
+
throw new Error(`source unit omits primary file: ${primary}`);
|
|
603
|
+
for (const file of files) {
|
|
604
|
+
if (!paths.has(file))
|
|
605
|
+
throw new Error(`source unit references unknown file: ${file}`);
|
|
606
|
+
}
|
|
607
|
+
return { primary, files };
|
|
608
|
+
});
|
|
609
|
+
return { sourceUnits: [...new Map(units.map((unit) => [compare(unit.primary, ...unit.files), unit])).values()].sort((a, b) => compare(a.primary, ...a.files).localeCompare(compare(b.primary, ...b.files))) };
|
|
610
|
+
}
|
|
59
611
|
function validateRoots(input) {
|
|
60
612
|
const roots = uniqueBy(input, (root) => root.id, "root id");
|
|
61
613
|
const byId = new Map(roots.map((root) => [root.id, root]));
|
|
@@ -86,16 +638,6 @@ function diagnosticLocation(value) {
|
|
|
86
638
|
...value.line === undefined ? {} : { line: Number(value.line) }
|
|
87
639
|
};
|
|
88
640
|
}
|
|
89
|
-
function string(value, label) {
|
|
90
|
-
if (typeof value !== "string")
|
|
91
|
-
throw new Error(`${label} must be a string`);
|
|
92
|
-
return value;
|
|
93
|
-
}
|
|
94
|
-
function array(value, label) {
|
|
95
|
-
if (!Array.isArray(value))
|
|
96
|
-
throw new Error(`${label} must be an array`);
|
|
97
|
-
return value;
|
|
98
|
-
}
|
|
99
641
|
function operationEvidence(input, files) {
|
|
100
642
|
if (input.operations === undefined && input.invocations !== undefined)
|
|
101
643
|
throw new Error("invocations require operation declarations");
|
|
@@ -167,47 +709,7 @@ function sourcePosition(position) {
|
|
|
167
709
|
}
|
|
168
710
|
|
|
169
711
|
// plugins/scanners/react/src/scan.ts
|
|
170
|
-
import
|
|
171
|
-
|
|
172
|
-
// plugins/scanners/projects.ts
|
|
173
|
-
import { execFile } from "child_process";
|
|
174
|
-
import { existsSync } from "fs";
|
|
175
|
-
import { readFile } from "fs/promises";
|
|
176
|
-
import path from "path";
|
|
177
|
-
import { promisify } from "util";
|
|
178
|
-
var execute = promisify(execFile);
|
|
179
|
-
var excluded = new Set([
|
|
180
|
-
".git",
|
|
181
|
-
"node_modules",
|
|
182
|
-
"vendor",
|
|
183
|
-
"target",
|
|
184
|
-
"dist",
|
|
185
|
-
"build",
|
|
186
|
-
"bin",
|
|
187
|
-
"obj",
|
|
188
|
-
".gradle",
|
|
189
|
-
".angular",
|
|
190
|
-
"coverage",
|
|
191
|
-
"generated",
|
|
192
|
-
"groma",
|
|
193
|
-
".groma"
|
|
194
|
-
]);
|
|
195
|
-
async function projectFiles(root, matches) {
|
|
196
|
-
const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
|
|
197
|
-
return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && !file.split("/").some((part) => excluded.has(part)) && existsSync(path.join(root, file))))].sort();
|
|
198
|
-
}
|
|
199
|
-
function hasDependency(manifest, dependency) {
|
|
200
|
-
return ["dependencies", "devDependencies", "peerDependencies", "optionalDependencies"].some((section) => typeof manifest[section]?.[dependency] === "string");
|
|
201
|
-
}
|
|
202
|
-
async function frameworkProjects(root, dependency) {
|
|
203
|
-
const projects = [];
|
|
204
|
-
for (const file of await projectFiles(root, (file) => path.posix.basename(file) === "package.json")) {
|
|
205
|
-
const manifest = JSON.parse(await readFile(path.join(root, file), "utf8"));
|
|
206
|
-
if (hasDependency(manifest, dependency))
|
|
207
|
-
projects.push(path.dirname(path.join(root, file)));
|
|
208
|
-
}
|
|
209
|
-
return projects;
|
|
210
|
-
}
|
|
712
|
+
import ts4 from "./typescript.cjs";
|
|
211
713
|
|
|
212
714
|
// plugins/scanners/observations.ts
|
|
213
715
|
function combineObservations(parts) {
|
|
@@ -218,6 +720,9 @@ function combineObservations(parts) {
|
|
|
218
720
|
const operations = [];
|
|
219
721
|
const invocations = [];
|
|
220
722
|
const diagnostics = [];
|
|
723
|
+
const sourceUnits = [];
|
|
724
|
+
const httpEndpoints = [];
|
|
725
|
+
const httpRequests = [];
|
|
221
726
|
for (const { key, observation } of parts) {
|
|
222
727
|
const id = (value) => JSON.stringify([key, value]);
|
|
223
728
|
roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
|
|
@@ -229,7 +734,10 @@ function combineObservations(parts) {
|
|
|
229
734
|
}
|
|
230
735
|
operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
|
|
231
736
|
invocations.push(...(observation.invocations ?? []).map((call) => ({ ...call, source: id(call.source), targets: call.targets.map(id) })));
|
|
737
|
+
httpEndpoints.push(...(observation.httpEndpoints ?? []).map((endpoint) => ({ ...endpoint, operation: id(endpoint.operation) })));
|
|
738
|
+
httpRequests.push(...(observation.httpRequests ?? []).map((request) => ({ ...request, operation: id(request.operation) })));
|
|
232
739
|
diagnostics.push(...observation.diagnostics);
|
|
740
|
+
sourceUnits.push(...observation.sourceUnits ?? []);
|
|
233
741
|
}
|
|
234
742
|
return createScanObservation({
|
|
235
743
|
scanner: parts[0].observation.scanner,
|
|
@@ -237,58 +745,810 @@ function combineObservations(parts) {
|
|
|
237
745
|
files: [...files.values()],
|
|
238
746
|
operations,
|
|
239
747
|
invocations,
|
|
240
|
-
diagnostics
|
|
748
|
+
diagnostics,
|
|
749
|
+
...parts.some((part) => part.observation.sourceUnits !== undefined) ? { sourceUnits } : {},
|
|
750
|
+
...parts.some((part) => part.observation.httpEndpoints !== undefined) ? { httpEndpoints } : {},
|
|
751
|
+
...parts.some((part) => part.observation.httpRequests !== undefined) ? { httpRequests } : {}
|
|
241
752
|
});
|
|
242
753
|
}
|
|
243
754
|
|
|
755
|
+
// plugins/scanners/react/src/http.ts
|
|
756
|
+
import ts3 from "./typescript.cjs";
|
|
757
|
+
|
|
758
|
+
// plugins/scanners/http-syntax.ts
|
|
759
|
+
function wrapper(ts, node) {
|
|
760
|
+
return ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node) || ts.isTypeAssertionExpression(node);
|
|
761
|
+
}
|
|
762
|
+
function unwrapped(ts, node) {
|
|
763
|
+
let current = node;
|
|
764
|
+
while (wrapper(ts, current))
|
|
765
|
+
current = current.expression;
|
|
766
|
+
return current;
|
|
767
|
+
}
|
|
768
|
+
function propertyKey(ts, property) {
|
|
769
|
+
const name = property.name;
|
|
770
|
+
if (name === undefined)
|
|
771
|
+
return;
|
|
772
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) ? name.text : undefined;
|
|
773
|
+
}
|
|
774
|
+
function ownProperty(ts, object, name) {
|
|
775
|
+
let found = "absent";
|
|
776
|
+
for (const property of object.properties) {
|
|
777
|
+
const key = propertyKey(ts, property);
|
|
778
|
+
if (key === undefined || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property))
|
|
779
|
+
return "unknown";
|
|
780
|
+
if (key === name)
|
|
781
|
+
found = ts.isPropertyAssignment(property) ? { node: property.initializer } : "unknown";
|
|
782
|
+
}
|
|
783
|
+
return found;
|
|
784
|
+
}
|
|
785
|
+
function holderOf(ts, node) {
|
|
786
|
+
let current = node;
|
|
787
|
+
while (wrapper(ts, current.parent))
|
|
788
|
+
current = current.parent;
|
|
789
|
+
return ts.isVariableDeclaration(current.parent) ? current.parent : undefined;
|
|
790
|
+
}
|
|
791
|
+
function urlText(ts, node) {
|
|
792
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node))
|
|
793
|
+
return true;
|
|
794
|
+
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// plugins/scanners/http-uses.ts
|
|
798
|
+
function patternPart(ts, parent, target) {
|
|
799
|
+
if (ts.isPropertyAssignment(parent))
|
|
800
|
+
return parent.initializer === target;
|
|
801
|
+
return ts.isArrayLiteralExpression(parent) || ts.isObjectLiteralExpression(parent) || ts.isSpreadElement(parent) || ts.isSpreadAssignment(parent) || ts.isShorthandPropertyAssignment(parent) || wrapper(ts, parent);
|
|
802
|
+
}
|
|
803
|
+
function assignment(ts, parent, direct) {
|
|
804
|
+
const operator = parent.operatorToken.kind;
|
|
805
|
+
if (operator < ts.SyntaxKind.FirstAssignment || operator > ts.SyntaxKind.LastAssignment)
|
|
806
|
+
return;
|
|
807
|
+
return operator === ts.SyntaxKind.EqualsToken && direct ? { assigned: parent.right } : {};
|
|
808
|
+
}
|
|
809
|
+
function counted(ts, parent) {
|
|
810
|
+
return (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && (parent.operator === ts.SyntaxKind.PlusPlusToken || parent.operator === ts.SyntaxKind.MinusMinusToken);
|
|
811
|
+
}
|
|
812
|
+
function writeOf(ts, node) {
|
|
813
|
+
let target = node;
|
|
814
|
+
while (patternPart(ts, target.parent, target))
|
|
815
|
+
target = target.parent;
|
|
816
|
+
const parent = target.parent;
|
|
817
|
+
const direct = target === node || wrapper(ts, target);
|
|
818
|
+
if (ts.isBinaryExpression(parent) && parent.left === target)
|
|
819
|
+
return assignment(ts, parent, direct);
|
|
820
|
+
if ((ts.isForInStatement(parent) || ts.isForOfStatement(parent)) && parent.initializer === target)
|
|
821
|
+
return {};
|
|
822
|
+
return direct && (counted(ts, parent) || ts.isDeleteExpression(parent)) ? {} : undefined;
|
|
823
|
+
}
|
|
824
|
+
function literalKey(ts, node) {
|
|
825
|
+
return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ? node.text : undefined;
|
|
826
|
+
}
|
|
827
|
+
function calledThrough(ts, node) {
|
|
828
|
+
const parent = node.parent;
|
|
829
|
+
if (ts.isCallExpression(parent) || ts.isNewExpression(parent))
|
|
830
|
+
return parent.expression === node;
|
|
831
|
+
return ts.isTaggedTemplateExpression(parent) && parent.tag === node;
|
|
832
|
+
}
|
|
833
|
+
function accessKey(ts, parent, object) {
|
|
834
|
+
if (ts.isPropertyAccessExpression(parent)) {
|
|
835
|
+
return parent.expression === object && ts.isIdentifier(parent.name) ? parent.name.text : undefined;
|
|
836
|
+
}
|
|
837
|
+
if (!ts.isElementAccessExpression(parent) || parent.expression !== object)
|
|
838
|
+
return;
|
|
839
|
+
return literalKey(ts, parent.argumentExpression);
|
|
840
|
+
}
|
|
841
|
+
function useOf(ts, reference) {
|
|
842
|
+
let chain = reference;
|
|
843
|
+
let receiver;
|
|
844
|
+
const path = [];
|
|
845
|
+
for (;; ) {
|
|
846
|
+
const parent = chain.parent;
|
|
847
|
+
if (wrapper(ts, parent)) {
|
|
848
|
+
chain = parent;
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
const key = accessKey(ts, parent, chain);
|
|
852
|
+
if (key === undefined)
|
|
853
|
+
break;
|
|
854
|
+
receiver = chain;
|
|
855
|
+
path.push(key);
|
|
856
|
+
chain = parent;
|
|
857
|
+
}
|
|
858
|
+
const write = writeOf(ts, chain);
|
|
859
|
+
if (write !== undefined)
|
|
860
|
+
return { kind: "write", path, reference, value: chain, ...write };
|
|
861
|
+
if (receiver !== undefined && calledThrough(ts, chain)) {
|
|
862
|
+
return { kind: path.length > 1 ? "read" : "escape", path: path.slice(0, -1), reference, value: receiver };
|
|
863
|
+
}
|
|
864
|
+
return { kind: path.length > 0 ? "read" : "escape", path, reference, value: chain };
|
|
865
|
+
}
|
|
866
|
+
function importedName(ts, node) {
|
|
867
|
+
const parent = node.parent;
|
|
868
|
+
if (ts.isImportClause(parent))
|
|
869
|
+
return parent.name === node;
|
|
870
|
+
return (ts.isImportSpecifier(parent) || ts.isNamespaceImport(parent) || ts.isImportEqualsDeclaration(parent)) && parent.name === node;
|
|
871
|
+
}
|
|
872
|
+
function typePosition(ts, node) {
|
|
873
|
+
let current = node;
|
|
874
|
+
while (ts.isQualifiedName(current.parent))
|
|
875
|
+
current = current.parent;
|
|
876
|
+
return ts.isTypeQueryNode(current.parent);
|
|
877
|
+
}
|
|
878
|
+
function exportStatement(ts, node) {
|
|
879
|
+
const parent = node.parent;
|
|
880
|
+
return ts.isExportSpecifier(parent) || ts.isExportAssignment(parent) && parent.expression === node;
|
|
881
|
+
}
|
|
882
|
+
function exported(ts, declaration) {
|
|
883
|
+
const statement = declaration.parent.parent;
|
|
884
|
+
return statement?.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
|
|
885
|
+
}
|
|
886
|
+
function topLevel(ts, declaration) {
|
|
887
|
+
const statement = declaration.parent?.parent;
|
|
888
|
+
return statement?.parent !== undefined && ts.isSourceFile(statement.parent);
|
|
889
|
+
}
|
|
890
|
+
function scriptFile(ts, source, names) {
|
|
891
|
+
if (ts.isExternalModule(source) || /\.[cm]js$/.test(source.fileName))
|
|
892
|
+
return false;
|
|
893
|
+
const required = (names.get("require") ?? []).some((node) => ts.isCallExpression(node.parent) && node.parent.expression === node);
|
|
894
|
+
const exporting = ["module", "exports"].some((name) => (names.get(name) ?? []).some((node) => ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node));
|
|
895
|
+
return !required && !exporting;
|
|
896
|
+
}
|
|
897
|
+
function dynamicBinding(ts, call) {
|
|
898
|
+
let holder = call;
|
|
899
|
+
let awaited = false;
|
|
900
|
+
while (ts.isAwaitExpression(holder.parent) || ts.isParenthesizedExpression(holder.parent)) {
|
|
901
|
+
awaited ||= ts.isAwaitExpression(holder.parent);
|
|
902
|
+
holder = holder.parent;
|
|
903
|
+
}
|
|
904
|
+
const parent = holder.parent;
|
|
905
|
+
if (ts.isVariableDeclaration(parent))
|
|
906
|
+
return awaited && ts.isIdentifier(parent.name) ? parent.name : undefined;
|
|
907
|
+
const then = ts.isPropertyAccessExpression(parent) && parent.expression === holder && !awaited ? parent.parent : undefined;
|
|
908
|
+
const callback = then !== undefined && ts.isCallExpression(then) ? then.arguments[0] : undefined;
|
|
909
|
+
if (callback === undefined || !ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback))
|
|
910
|
+
return;
|
|
911
|
+
const [first] = callback.parameters;
|
|
912
|
+
return first !== undefined && ts.isIdentifier(first.name) ? first.name : undefined;
|
|
913
|
+
}
|
|
914
|
+
function requiredModule(ts, node) {
|
|
915
|
+
if (node !== undefined && ts.isExternalModuleReference(node))
|
|
916
|
+
return ts.isStringLiteral(node.expression) ? node.expression.text : undefined;
|
|
917
|
+
if (node === undefined || !ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== "require")
|
|
918
|
+
return;
|
|
919
|
+
const [specifier] = node.arguments;
|
|
920
|
+
return specifier !== undefined && ts.isStringLiteral(specifier) ? specifier.text : undefined;
|
|
921
|
+
}
|
|
922
|
+
function importModule(ts, statement, name) {
|
|
923
|
+
return ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) ? { module: statement.moduleSpecifier.text, name } : undefined;
|
|
924
|
+
}
|
|
925
|
+
function requiredValue(ts, initializer) {
|
|
926
|
+
const value = initializer === undefined ? undefined : unwrapped(ts, initializer);
|
|
927
|
+
const direct = requiredModule(ts, value);
|
|
928
|
+
if (direct !== undefined)
|
|
929
|
+
return { module: direct, name: "default" };
|
|
930
|
+
if (value === undefined || !ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.name))
|
|
931
|
+
return;
|
|
932
|
+
const module = requiredModule(ts, unwrapped(ts, value.expression));
|
|
933
|
+
return module === undefined ? undefined : { module, name: value.name.text };
|
|
934
|
+
}
|
|
935
|
+
function boundExport(ts, declaration) {
|
|
936
|
+
if (ts.isImportClause(declaration))
|
|
937
|
+
return importModule(ts, declaration.parent, "default");
|
|
938
|
+
if (ts.isNamespaceImport(declaration))
|
|
939
|
+
return importModule(ts, declaration.parent.parent, "*");
|
|
940
|
+
if (ts.isImportSpecifier(declaration)) {
|
|
941
|
+
return importModule(ts, declaration.parent.parent.parent, (declaration.propertyName ?? declaration.name).text);
|
|
942
|
+
}
|
|
943
|
+
if (ts.isImportEqualsDeclaration(declaration))
|
|
944
|
+
return requiredValue(ts, declaration.moduleReference);
|
|
945
|
+
if (ts.isVariableDeclaration(declaration))
|
|
946
|
+
return requiredValue(ts, declaration.initializer);
|
|
947
|
+
if (!ts.isBindingElement(declaration))
|
|
948
|
+
return;
|
|
949
|
+
const variable = declaration.parent.parent;
|
|
950
|
+
const module = ts.isVariableDeclaration(variable) ? requiredModule(ts, variable.initializer) : undefined;
|
|
951
|
+
const key = declaration.propertyName ?? declaration.name;
|
|
952
|
+
return module !== undefined && key !== undefined && ts.isIdentifier(key) ? { module, name: key.text } : undefined;
|
|
953
|
+
}
|
|
954
|
+
function defaultImport(ts, name) {
|
|
955
|
+
const declaration = name.parent;
|
|
956
|
+
const origin = declaration.name === name ? boundExport(ts, declaration) : undefined;
|
|
957
|
+
return origin?.name === "default" ? { declaration, module: origin.module } : undefined;
|
|
958
|
+
}
|
|
959
|
+
function indexName(ts, index, node) {
|
|
960
|
+
const same = index.names.get(node.text);
|
|
961
|
+
if (same === undefined)
|
|
962
|
+
index.names.set(node.text, [node]);
|
|
963
|
+
else
|
|
964
|
+
same.push(node);
|
|
965
|
+
if (importedName(ts, node))
|
|
966
|
+
index.imported.add(node.text);
|
|
967
|
+
const imported = defaultImport(ts, node);
|
|
968
|
+
if (imported !== undefined)
|
|
969
|
+
index.defaults.set(imported.module, [...index.defaults.get(imported.module) ?? [], imported.declaration]);
|
|
970
|
+
}
|
|
971
|
+
function indexSources(ts, sources) {
|
|
972
|
+
const index = { names: new Map, imported: new Set, dynamic: [], defaults: new Map };
|
|
973
|
+
const visit = (node) => {
|
|
974
|
+
if (ts.isIdentifier(node))
|
|
975
|
+
indexName(ts, index, node);
|
|
976
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
977
|
+
const binding = dynamicBinding(ts, node);
|
|
978
|
+
index.dynamic.push({ call: node, ...binding === undefined ? {} : { binding } });
|
|
979
|
+
}
|
|
980
|
+
ts.forEachChild(node, visit);
|
|
981
|
+
};
|
|
982
|
+
for (const source of sources)
|
|
983
|
+
visit(source);
|
|
984
|
+
return index;
|
|
985
|
+
}
|
|
986
|
+
function startsWith(path, prefix) {
|
|
987
|
+
return prefix.length <= path.length && prefix.every((name, index) => path[index] === name);
|
|
988
|
+
}
|
|
989
|
+
function useAt(ts, node) {
|
|
990
|
+
const parent = node.parent;
|
|
991
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node)
|
|
992
|
+
return { kind: "write", path: [], reference: node, value: node };
|
|
993
|
+
if (exportStatement(ts, node))
|
|
994
|
+
return { kind: "export", path: [], reference: node, value: node };
|
|
995
|
+
const accessed = ts.isPropertyAccessExpression(parent) && parent.name === node;
|
|
996
|
+
return useOf(ts, accessed ? parent : node);
|
|
997
|
+
}
|
|
998
|
+
function objectUse(ts, node) {
|
|
999
|
+
const parent = node.parent;
|
|
1000
|
+
const named = ts.isPropertyAccessExpression(parent) && parent.expression === node && ts.isIdentifier(parent.name);
|
|
1001
|
+
const declaring = (ts.isVariableDeclaration(parent) || ts.isParameter(parent)) && parent.name === node;
|
|
1002
|
+
return !named && !declaring && !importedName(ts, node) && !typePosition(ts, node);
|
|
1003
|
+
}
|
|
1004
|
+
function settingEffect(use, path, keys) {
|
|
1005
|
+
if (use.path.length === 0 || use.kind === "export")
|
|
1006
|
+
return { kind: "none" };
|
|
1007
|
+
if (startsWith(path, use.path))
|
|
1008
|
+
return { kind: "changes" };
|
|
1009
|
+
const key = startsWith(use.path, path) ? use.path[path.length] : undefined;
|
|
1010
|
+
if (key === undefined || !keys.includes(key))
|
|
1011
|
+
return { kind: "none" };
|
|
1012
|
+
if (use.assigned !== undefined && use.path.length === path.length + 1)
|
|
1013
|
+
return { kind: "assigns", key, value: use.assigned };
|
|
1014
|
+
return { kind: "changes" };
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
// plugins/scanners/http-bindings.ts
|
|
1018
|
+
function bindingUses(ts, checker, sources, options) {
|
|
1019
|
+
let index;
|
|
1020
|
+
const indexed = () => {
|
|
1021
|
+
index ??= indexSources(ts, sources);
|
|
1022
|
+
return index;
|
|
1023
|
+
};
|
|
1024
|
+
const uses = new Map;
|
|
1025
|
+
const symbols = new Map;
|
|
1026
|
+
const shorthand = (node) => ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node;
|
|
1027
|
+
const resolveAll = (names) => {
|
|
1028
|
+
const pending = names.filter((node) => !symbols.has(node) && !shorthand(node));
|
|
1029
|
+
const batch = pending.length === 0 ? Promise.resolve([]) : checker.symbolsAt(pending);
|
|
1030
|
+
for (const [position, node] of pending.entries())
|
|
1031
|
+
symbols.set(node, batch.then((found) => found[position]));
|
|
1032
|
+
};
|
|
1033
|
+
const symbolOf = (node) => {
|
|
1034
|
+
let found = symbols.get(node);
|
|
1035
|
+
if (found === undefined) {
|
|
1036
|
+
found = shorthand(node) ? checker.shorthandValue(node.parent) : checker.symbolsAt([node]).then(([symbol]) => symbol);
|
|
1037
|
+
symbols.set(node, found);
|
|
1038
|
+
}
|
|
1039
|
+
return found;
|
|
1040
|
+
};
|
|
1041
|
+
const refersTo = async (node, declaration) => {
|
|
1042
|
+
const symbol = await symbolOf(node);
|
|
1043
|
+
if (symbol === undefined || node.parent === declaration)
|
|
1044
|
+
return false;
|
|
1045
|
+
if (await checker.declaration(symbol) === declaration)
|
|
1046
|
+
return true;
|
|
1047
|
+
if (importedName(ts, node))
|
|
1048
|
+
return false;
|
|
1049
|
+
return await checker.declaration(await checker.aliased(symbol)) === declaration;
|
|
1050
|
+
};
|
|
1051
|
+
const usesOf = (declaration) => {
|
|
1052
|
+
let found = uses.get(declaration);
|
|
1053
|
+
if (found !== undefined)
|
|
1054
|
+
return found;
|
|
1055
|
+
found = (async () => {
|
|
1056
|
+
const { names, imported } = indexed();
|
|
1057
|
+
const name = declaration.name !== undefined && ts.isIdentifier(declaration.name) ? declaration.name.text : undefined;
|
|
1058
|
+
const texts = name === undefined ? [] : [name, "default", ...imported];
|
|
1059
|
+
const candidates = [...new Set(texts.flatMap((text) => names.get(text) ?? []))].filter((node) => !typePosition(ts, node));
|
|
1060
|
+
resolveAll(candidates);
|
|
1061
|
+
const referring = await Promise.all(candidates.map((node) => refersTo(node, declaration)));
|
|
1062
|
+
return candidates.filter((_, position) => referring[position]).map((node) => useAt(ts, node));
|
|
1063
|
+
})();
|
|
1064
|
+
uses.set(declaration, found);
|
|
1065
|
+
return found;
|
|
1066
|
+
};
|
|
1067
|
+
const dynamicEscapes = async ({ binding }) => {
|
|
1068
|
+
if (binding === undefined)
|
|
1069
|
+
return true;
|
|
1070
|
+
const bound = await symbolOf(binding);
|
|
1071
|
+
const references = indexed().names.get(binding.text) ?? [];
|
|
1072
|
+
resolveAll(references);
|
|
1073
|
+
for (const node of references) {
|
|
1074
|
+
const symbol = await symbolOf(node);
|
|
1075
|
+
if (bound !== undefined && symbol !== undefined && checker.same(symbol, bound) && objectUse(ts, node))
|
|
1076
|
+
return true;
|
|
1077
|
+
}
|
|
1078
|
+
return false;
|
|
1079
|
+
};
|
|
1080
|
+
const moduleOf = async (node) => {
|
|
1081
|
+
const symbol = await symbolOf(node);
|
|
1082
|
+
const module = symbol === undefined ? undefined : await checker.aliased(symbol);
|
|
1083
|
+
return module !== undefined && checker.module(module) ? module : undefined;
|
|
1084
|
+
};
|
|
1085
|
+
const loadedModule = async (entry) => {
|
|
1086
|
+
const [argument] = entry.call.arguments;
|
|
1087
|
+
const [module] = argument === undefined ? [] : await checker.symbolsAt([argument]);
|
|
1088
|
+
return module !== undefined && await dynamicEscapes(entry) ? module : undefined;
|
|
1089
|
+
};
|
|
1090
|
+
const escapedModules = async () => {
|
|
1091
|
+
const { names, imported, dynamic } = indexed();
|
|
1092
|
+
const importedNames = [...imported].flatMap((text) => names.get(text) ?? []).filter((node) => objectUse(ts, node));
|
|
1093
|
+
resolveAll(importedNames);
|
|
1094
|
+
const found = await Promise.all([...importedNames.map(moduleOf), ...dynamic.map(loadedModule)]);
|
|
1095
|
+
const modules = [];
|
|
1096
|
+
for (const module of found) {
|
|
1097
|
+
if (module !== undefined && !modules.some((known) => checker.same(known, module)))
|
|
1098
|
+
modules.push(module);
|
|
1099
|
+
}
|
|
1100
|
+
return modules;
|
|
1101
|
+
};
|
|
1102
|
+
let reachable;
|
|
1103
|
+
const escapedExports = () => {
|
|
1104
|
+
reachable ??= (async () => {
|
|
1105
|
+
const exports = (await Promise.all((await escapedModules()).map((module) => checker.exportsOf(module)))).flat();
|
|
1106
|
+
const declarations = await Promise.all(exports.map(async (symbol) => checker.declaration(await checker.aliased(symbol))));
|
|
1107
|
+
return new Set(declarations.filter((declaration) => declaration !== undefined));
|
|
1108
|
+
})();
|
|
1109
|
+
return reachable;
|
|
1110
|
+
};
|
|
1111
|
+
const script = () => options.fileAlone && sources.some((source) => scriptFile(ts, source, indexed().names));
|
|
1112
|
+
const leaves = async (declaration) => {
|
|
1113
|
+
if (!options.fileAlone)
|
|
1114
|
+
return (await escapedExports()).has(declaration);
|
|
1115
|
+
return exported(ts, declaration) || topLevel(ts, declaration) && script();
|
|
1116
|
+
};
|
|
1117
|
+
const readChanges = async (use) => use.kind !== "read" || !await checker.primitiveAt(use.value);
|
|
1118
|
+
const changes = async (use, path, reference) => {
|
|
1119
|
+
if (use.reference === reference || !startsWith(path, use.path))
|
|
1120
|
+
return false;
|
|
1121
|
+
return use.kind === "export" ? options.fileAlone : readChanges(use);
|
|
1122
|
+
};
|
|
1123
|
+
const settingAt = async (settings, use, path, keys) => {
|
|
1124
|
+
const effect = settingEffect(use, path, keys);
|
|
1125
|
+
if (effect.kind === "assigns")
|
|
1126
|
+
settings.assigned.set(effect.key, [...settings.assigned.get(effect.key) ?? [], effect.value]);
|
|
1127
|
+
else if (effect.kind === "changes")
|
|
1128
|
+
settings.changed ||= await readChanges(use);
|
|
1129
|
+
};
|
|
1130
|
+
return {
|
|
1131
|
+
async unchanged(declaration, path, reference) {
|
|
1132
|
+
if (await leaves(declaration))
|
|
1133
|
+
return false;
|
|
1134
|
+
for (const use of await usesOf(declaration))
|
|
1135
|
+
if (await changes(use, path, reference))
|
|
1136
|
+
return false;
|
|
1137
|
+
return true;
|
|
1138
|
+
},
|
|
1139
|
+
async reassigned(declaration) {
|
|
1140
|
+
if (options.fileAlone && topLevel(ts, declaration) && script())
|
|
1141
|
+
return true;
|
|
1142
|
+
return (await usesOf(declaration)).some((use) => use.kind === "write" && use.path.length === 0);
|
|
1143
|
+
},
|
|
1144
|
+
async settings(declaration, path, keys) {
|
|
1145
|
+
const settings = { assigned: new Map, changed: await leaves(declaration) };
|
|
1146
|
+
for (const use of await usesOf(declaration))
|
|
1147
|
+
await settingAt(settings, use, path, keys);
|
|
1148
|
+
return settings;
|
|
1149
|
+
},
|
|
1150
|
+
defaultImports: (module) => indexed().defaults.get(module) ?? [],
|
|
1151
|
+
references: async (declaration) => (await usesOf(declaration)).map((use) => use.reference),
|
|
1152
|
+
named: (text) => indexed().names.get(text) ?? [],
|
|
1153
|
+
fileAlone: options.fileAlone
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
// plugins/scanners/http-values.ts
|
|
1158
|
+
function urlContext(ts, checker, sources, fileAlone = false) {
|
|
1159
|
+
return { ts, checker, bindings: bindingUses(ts, checker, sources, { fileAlone }) };
|
|
1160
|
+
}
|
|
1161
|
+
var MAX_DEPTH = 8;
|
|
1162
|
+
function declarationFile(node) {
|
|
1163
|
+
return /\.d\.[cm]?ts$/.test(node.getSourceFile().fileName);
|
|
1164
|
+
}
|
|
1165
|
+
async function declarationOf(context, node) {
|
|
1166
|
+
const { checker } = context;
|
|
1167
|
+
const [symbol] = await checker.symbolsAt([node]);
|
|
1168
|
+
return symbol === undefined ? undefined : checker.declaration(await checker.aliased(symbol));
|
|
1169
|
+
}
|
|
1170
|
+
var moduleNamesByFile = new WeakMap;
|
|
1171
|
+
function moduleNames(ts, source) {
|
|
1172
|
+
let names = moduleNamesByFile.get(source);
|
|
1173
|
+
if (names !== undefined)
|
|
1174
|
+
return names;
|
|
1175
|
+
const found = new Set;
|
|
1176
|
+
const visit = (node) => {
|
|
1177
|
+
const name = node.name;
|
|
1178
|
+
if (name !== undefined && ts.isIdentifier(name) && boundExport(ts, node) !== undefined)
|
|
1179
|
+
found.add(name.text);
|
|
1180
|
+
ts.forEachChild(node, visit);
|
|
1181
|
+
};
|
|
1182
|
+
visit(source);
|
|
1183
|
+
names = found;
|
|
1184
|
+
moduleNamesByFile.set(source, names);
|
|
1185
|
+
return names;
|
|
1186
|
+
}
|
|
1187
|
+
async function importOrigin(context, node) {
|
|
1188
|
+
const { ts, checker } = context;
|
|
1189
|
+
const required = requiredModule(ts, node);
|
|
1190
|
+
if (required !== undefined)
|
|
1191
|
+
return { module: required, name: "default" };
|
|
1192
|
+
if (!ts.isIdentifier(node) || !moduleNames(ts, node.getSourceFile()).has(node.text))
|
|
1193
|
+
return;
|
|
1194
|
+
const [symbol] = await checker.symbolsAt([node]);
|
|
1195
|
+
const declaration = symbol === undefined ? undefined : await checker.declaration(symbol);
|
|
1196
|
+
const origin = declaration === undefined ? undefined : boundExport(ts, declaration);
|
|
1197
|
+
if (origin === undefined || !ts.isVariableDeclaration(declaration) && !ts.isBindingElement(declaration))
|
|
1198
|
+
return origin;
|
|
1199
|
+
return await unassigned(context, declaration) ? origin : undefined;
|
|
1200
|
+
}
|
|
1201
|
+
async function runtimeGlobal(context, node) {
|
|
1202
|
+
if (await importOrigin(context, node) !== undefined)
|
|
1203
|
+
return false;
|
|
1204
|
+
const declaration = await declarationOf(context, node);
|
|
1205
|
+
return declaration === undefined || declarationFile(declaration);
|
|
1206
|
+
}
|
|
1207
|
+
function ambient(ts, declaration) {
|
|
1208
|
+
const flag = ts.NodeFlags.Ambient ?? 0;
|
|
1209
|
+
return ((declaration.flags ?? 0) & flag) !== 0;
|
|
1210
|
+
}
|
|
1211
|
+
async function unassigned(context, declaration) {
|
|
1212
|
+
const { ts, checker } = context;
|
|
1213
|
+
const list = ts.isVariableDeclaration(declaration) ? declaration.parent : declaration.parent.parent.parent;
|
|
1214
|
+
if (((list.flags ?? 0) & ts.NodeFlags.Const) !== 0)
|
|
1215
|
+
return true;
|
|
1216
|
+
if (declaration.name === undefined)
|
|
1217
|
+
return false;
|
|
1218
|
+
const [symbol] = await checker.symbolsAt([declaration.name]);
|
|
1219
|
+
return symbol !== undefined && checker.declarationCount(symbol) === 1 && !await context.bindings.reassigned(declaration);
|
|
1220
|
+
}
|
|
1221
|
+
async function variableHeld(context, name, path, depth) {
|
|
1222
|
+
const { ts } = context;
|
|
1223
|
+
const declaration = await declarationOf(context, name);
|
|
1224
|
+
if (declaration === undefined || declarationFile(declaration) || ambient(ts, declaration))
|
|
1225
|
+
return "unseen";
|
|
1226
|
+
if (path.length === 0 && ts.isFunctionDeclaration(declaration))
|
|
1227
|
+
return { node: declaration };
|
|
1228
|
+
if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined)
|
|
1229
|
+
return "unknown";
|
|
1230
|
+
if (!ts.isIdentifier(declaration.name) || !await unassigned(context, declaration))
|
|
1231
|
+
return "unknown";
|
|
1232
|
+
if (path.length > 0 && !await context.bindings.unchanged(declaration, path, name))
|
|
1233
|
+
return "unknown";
|
|
1234
|
+
return held(context, declaration.initializer, path, depth + 1);
|
|
1235
|
+
}
|
|
1236
|
+
async function extendsClass(context, klass, base, depth = 0) {
|
|
1237
|
+
const { ts } = context;
|
|
1238
|
+
if (!ts.isClassLike(klass) || depth > MAX_DEPTH)
|
|
1239
|
+
return false;
|
|
1240
|
+
const clauses = klass.heritageClauses ?? [];
|
|
1241
|
+
const clause = Array.from(clauses).find((entry) => entry.token === ts.SyntaxKind.ExtendsKeyword);
|
|
1242
|
+
const parent = clause?.types[0];
|
|
1243
|
+
const declaration = parent === undefined ? undefined : await declarationOf(context, parent.expression);
|
|
1244
|
+
return declaration !== undefined && (declaration === base || extendsClass(context, declaration, base, depth + 1));
|
|
1245
|
+
}
|
|
1246
|
+
async function assignmentAt(context, node, same, declaration) {
|
|
1247
|
+
const { ts } = context;
|
|
1248
|
+
const parent = node.parent;
|
|
1249
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
1250
|
+
const use = same ? useAt(ts, node) : undefined;
|
|
1251
|
+
return use?.kind === "write" ? { value: use.path.length === 0 ? use.assigned : undefined } : undefined;
|
|
1252
|
+
}
|
|
1253
|
+
if (!ts.isPropertyDeclaration(parent) || parent.name !== node || declaration === undefined || parent === declaration)
|
|
1254
|
+
return;
|
|
1255
|
+
return await extendsClass(context, parent.parent, declaration.parent) ? { value: parent.initializer } : undefined;
|
|
1256
|
+
}
|
|
1257
|
+
async function fieldAssignments(context, name, declaration) {
|
|
1258
|
+
const { ts, checker } = context;
|
|
1259
|
+
const [symbol] = await checker.symbolsAt([name]);
|
|
1260
|
+
const initialized = declaration !== undefined && (ts.isPropertyDeclaration(declaration) || ts.isPropertyAssignment(declaration)) ? declaration.initializer : undefined;
|
|
1261
|
+
const assignments = initialized === undefined ? [] : [initialized];
|
|
1262
|
+
const others = context.bindings.named(name.text).filter((node) => node !== name);
|
|
1263
|
+
const symbols = await checker.symbolsAt(others);
|
|
1264
|
+
for (const [position, node] of others.entries()) {
|
|
1265
|
+
const found = symbols[position];
|
|
1266
|
+
const same = found !== undefined && symbol !== undefined && checker.same(found, symbol);
|
|
1267
|
+
const assignment = await assignmentAt(context, node, same, declaration);
|
|
1268
|
+
if (assignment !== undefined)
|
|
1269
|
+
assignments.push(assignment.value);
|
|
1270
|
+
}
|
|
1271
|
+
return assignments;
|
|
1272
|
+
}
|
|
1273
|
+
async function fieldHeld(context, access, path, depth) {
|
|
1274
|
+
const { ts, checker } = context;
|
|
1275
|
+
const [symbol] = await checker.symbolsAt([access.name]);
|
|
1276
|
+
const declaration = symbol === undefined ? undefined : await checker.declaration(symbol);
|
|
1277
|
+
const assignments = await fieldAssignments(context, access.name, declaration);
|
|
1278
|
+
const injected = declaration !== undefined && ts.isParameter(declaration);
|
|
1279
|
+
if (assignments.length === 0)
|
|
1280
|
+
return "unseen";
|
|
1281
|
+
const [only] = assignments;
|
|
1282
|
+
if (injected || assignments.length > 1 || only === undefined || path.length > 0 || context.bindings.fileAlone)
|
|
1283
|
+
return "unknown";
|
|
1284
|
+
return held(context, only, path, depth + 1);
|
|
1285
|
+
}
|
|
1286
|
+
async function held(context, node, path, depth) {
|
|
1287
|
+
const { ts } = context;
|
|
1288
|
+
if (depth > MAX_DEPTH)
|
|
1289
|
+
return "unknown";
|
|
1290
|
+
const value = unwrapped(ts, node);
|
|
1291
|
+
const [name, ...rest] = path;
|
|
1292
|
+
if (ts.isObjectLiteralExpression(value) && name !== undefined) {
|
|
1293
|
+
const property = ownProperty(ts, value, name);
|
|
1294
|
+
if (typeof property !== "string")
|
|
1295
|
+
return held(context, property.node, rest, depth + 1);
|
|
1296
|
+
return property === "absent" && rest.length === 0 ? "absent" : "unknown";
|
|
1297
|
+
}
|
|
1298
|
+
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.name)) {
|
|
1299
|
+
if (value.expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
1300
|
+
return fieldHeld(context, value, path, depth);
|
|
1301
|
+
return held(context, value.expression, [value.name.text, ...path], depth + 1);
|
|
1302
|
+
}
|
|
1303
|
+
if (ts.isIdentifier(value))
|
|
1304
|
+
return variableHeld(context, value, path, depth);
|
|
1305
|
+
if (name === undefined)
|
|
1306
|
+
return { node: value };
|
|
1307
|
+
return ts.isMetaProperty(value) ? "unseen" : "unknown";
|
|
1308
|
+
}
|
|
1309
|
+
function heldAt(context, node, ...path) {
|
|
1310
|
+
return held(context, node, path, 0);
|
|
1311
|
+
}
|
|
1312
|
+
async function heldParts(context, value) {
|
|
1313
|
+
if (value === "unseen")
|
|
1314
|
+
return [configuredPart];
|
|
1315
|
+
return typeof value === "string" ? [computedPart] : urlParts(context, value.node);
|
|
1316
|
+
}
|
|
1317
|
+
async function urlParts(context, node, depth = 0) {
|
|
1318
|
+
const { ts } = context;
|
|
1319
|
+
if (depth > MAX_DEPTH)
|
|
1320
|
+
return [computedPart];
|
|
1321
|
+
const value = unwrapped(ts, node);
|
|
1322
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
1323
|
+
return [{ kind: "text", text: value.text }];
|
|
1324
|
+
if (ts.isTemplateExpression(value)) {
|
|
1325
|
+
const parts = [{ kind: "text", text: value.head.text }];
|
|
1326
|
+
for (const span of value.templateSpans) {
|
|
1327
|
+
parts.push(...await urlParts(context, span.expression, depth + 1), { kind: "text", text: span.literal.text });
|
|
1328
|
+
}
|
|
1329
|
+
return parts;
|
|
1330
|
+
}
|
|
1331
|
+
if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
1332
|
+
return [...await urlParts(context, value.left, depth + 1), ...await urlParts(context, value.right, depth + 1)];
|
|
1333
|
+
}
|
|
1334
|
+
if (!ts.isIdentifier(value) && !ts.isPropertyAccessExpression(value))
|
|
1335
|
+
return [computedPart];
|
|
1336
|
+
const resolved = await held(context, value, [], depth + 1);
|
|
1337
|
+
return typeof resolved === "string" ? heldParts(context, resolved) : urlParts(context, resolved.node, depth + 1);
|
|
1338
|
+
}
|
|
1339
|
+
async function literalText(context, node) {
|
|
1340
|
+
if (node === undefined)
|
|
1341
|
+
return;
|
|
1342
|
+
const [first, ...rest] = await urlParts(context, node);
|
|
1343
|
+
return rest.length === 0 && first?.kind === "text" ? first.text : undefined;
|
|
1344
|
+
}
|
|
1345
|
+
async function methodName(context, node) {
|
|
1346
|
+
return methodText(await literalText(context, node));
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
// plugins/scanners/http-clients.ts
|
|
1350
|
+
async function declaredMethod(context, options, fallback) {
|
|
1351
|
+
if (options === undefined)
|
|
1352
|
+
return fallback;
|
|
1353
|
+
const declared = await heldAt(context, options, "method");
|
|
1354
|
+
if (declared === "absent")
|
|
1355
|
+
return fallback;
|
|
1356
|
+
return typeof declared === "string" ? undefined : methodName(context, declared.node);
|
|
1357
|
+
}
|
|
1358
|
+
async function optionsBase(context, config, fallback) {
|
|
1359
|
+
if (config === undefined)
|
|
1360
|
+
return fallback;
|
|
1361
|
+
const base = await heldAt(context, config, "baseURL");
|
|
1362
|
+
return base === "absent" ? fallback : heldParts(context, base);
|
|
1363
|
+
}
|
|
1364
|
+
async function runtimeFetch(context, callee) {
|
|
1365
|
+
if (!context.ts.isIdentifier(callee) || callee.text !== "fetch")
|
|
1366
|
+
return;
|
|
1367
|
+
const origin = await importOrigin(context, callee);
|
|
1368
|
+
const fetch = origin === undefined ? await runtimeGlobal(context, callee) : origin.module === "node-fetch" && origin.name === "default";
|
|
1369
|
+
return fetch ? "fetch" : undefined;
|
|
1370
|
+
}
|
|
1371
|
+
async function fetchRequest(context, call, client) {
|
|
1372
|
+
const [url, options] = call.arguments;
|
|
1373
|
+
const kind = url === undefined ? undefined : await client(call.expression);
|
|
1374
|
+
if (url === undefined || kind === undefined)
|
|
1375
|
+
return;
|
|
1376
|
+
const input = await heldAt(context, url);
|
|
1377
|
+
const method = input === "unseen" || typeof input === "object" && urlText(context.ts, input.node) ? await declaredMethod(context, options, "GET") : undefined;
|
|
1378
|
+
const parts = await urlParts(context, url);
|
|
1379
|
+
const target = kind === "ofetch" ? withBase(await optionsBase(context, options, []), parts) : parts;
|
|
1380
|
+
return { ...method === undefined ? {} : { method }, ...requestUrl(target) };
|
|
1381
|
+
}
|
|
1382
|
+
async function withDefaults(context, client, holders) {
|
|
1383
|
+
const found = await Promise.all(holders.map((holder) => context.bindings.settings(holder, ["defaults"], ["baseURL", "method"])));
|
|
1384
|
+
const bases = found.flatMap((settings) => settings.assigned.get("baseURL") ?? []);
|
|
1385
|
+
const methods = found.flatMap((settings) => settings.assigned.get("method") ?? []);
|
|
1386
|
+
if (found.some((settings) => settings.changed) || bases.length > 1 || methods.length > 1)
|
|
1387
|
+
return { base: [computedPart], method: undefined };
|
|
1388
|
+
return {
|
|
1389
|
+
base: bases[0] === undefined ? client.base : await urlParts(context, bases[0]),
|
|
1390
|
+
method: methods[0] === undefined ? client.method : methodText(await literalText(context, methods[0]))
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
function defaultClient(context) {
|
|
1394
|
+
return withDefaults(context, { base: [], method: "GET" }, context.bindings.defaultImports("axios"));
|
|
1395
|
+
}
|
|
1396
|
+
async function sent(context, client, url, method, config) {
|
|
1397
|
+
const effective = method ?? await declaredMethod(context, config, client.method);
|
|
1398
|
+
const base = await optionsBase(context, config, client.base);
|
|
1399
|
+
return { ...effective === undefined ? {} : { method: effective }, ...requestUrl(joinBase(base, url)) };
|
|
1400
|
+
}
|
|
1401
|
+
async function configRequest(context, client, config) {
|
|
1402
|
+
const url = config === undefined ? "absent" : await heldAt(context, config, "url");
|
|
1403
|
+
return url === "absent" ? undefined : sent(context, client, await heldParts(context, url), undefined, config);
|
|
1404
|
+
}
|
|
1405
|
+
async function holdsObject(context, node) {
|
|
1406
|
+
const value = await heldAt(context, node);
|
|
1407
|
+
return typeof value === "object" && context.ts.isObjectLiteralExpression(value.node);
|
|
1408
|
+
}
|
|
1409
|
+
var SHORTHAND = new Map([
|
|
1410
|
+
["get", "GET"],
|
|
1411
|
+
["post", "POST"],
|
|
1412
|
+
["put", "PUT"],
|
|
1413
|
+
["patch", "PATCH"],
|
|
1414
|
+
["delete", "DELETE"],
|
|
1415
|
+
["head", "HEAD"],
|
|
1416
|
+
["options", "OPTIONS"]
|
|
1417
|
+
]);
|
|
1418
|
+
var WITH_BODY = new Set(["post", "put", "patch"]);
|
|
1419
|
+
async function isAxios(context, node) {
|
|
1420
|
+
const origin = await importOrigin(context, node);
|
|
1421
|
+
return origin?.module === "axios" && origin.name === "default";
|
|
1422
|
+
}
|
|
1423
|
+
async function createdClient(context, node) {
|
|
1424
|
+
const { ts } = context;
|
|
1425
|
+
const created = await heldAt(context, node);
|
|
1426
|
+
const call = typeof created === "object" && ts.isCallExpression(created.node) ? created.node : undefined;
|
|
1427
|
+
const callee = call?.expression;
|
|
1428
|
+
if (call === undefined || callee === undefined || !ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name))
|
|
1429
|
+
return;
|
|
1430
|
+
if (callee.name.text !== "create" || !await isAxios(context, callee.expression))
|
|
1431
|
+
return;
|
|
1432
|
+
const parent = await defaultClient(context);
|
|
1433
|
+
const inherited = { base: parent.base.length === 0 ? [] : [computedPart], method: parent.method === "GET" ? "GET" : undefined };
|
|
1434
|
+
const [config] = call.arguments;
|
|
1435
|
+
const client = { base: await optionsBase(context, config, inherited.base), method: await declaredMethod(context, config, inherited.method) };
|
|
1436
|
+
const holders = [await declarationOf(context, node), holderOf(ts, call)].filter((holder) => holder !== undefined);
|
|
1437
|
+
return withDefaults(context, client, [...new Set(holders)]);
|
|
1438
|
+
}
|
|
1439
|
+
async function axiosClient(context, node) {
|
|
1440
|
+
if (!context.ts.isIdentifier(node))
|
|
1441
|
+
return;
|
|
1442
|
+
return await isAxios(context, node) ? defaultClient(context) : createdClient(context, node);
|
|
1443
|
+
}
|
|
1444
|
+
async function axiosRequest(context, call) {
|
|
1445
|
+
const { ts } = context;
|
|
1446
|
+
const callee = call.expression;
|
|
1447
|
+
const [first, second, third] = call.arguments;
|
|
1448
|
+
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
|
|
1449
|
+
const client = await axiosClient(context, callee.expression);
|
|
1450
|
+
if (client === undefined)
|
|
1451
|
+
return;
|
|
1452
|
+
const member = callee.name.text;
|
|
1453
|
+
const method = SHORTHAND.get(member);
|
|
1454
|
+
if (method !== undefined && first !== undefined) {
|
|
1455
|
+
return sent(context, client, await urlParts(context, first), method, WITH_BODY.has(member) ? third : second);
|
|
1456
|
+
}
|
|
1457
|
+
return member === "request" ? configRequest(context, client, first) : undefined;
|
|
1458
|
+
}
|
|
1459
|
+
const client = await axiosClient(context, callee);
|
|
1460
|
+
if (client === undefined || first === undefined)
|
|
1461
|
+
return;
|
|
1462
|
+
if (await holdsObject(context, first))
|
|
1463
|
+
return configRequest(context, client, first);
|
|
1464
|
+
return sent(context, client, await urlParts(context, first), undefined, second);
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// plugins/scanners/http-checker.ts
|
|
1468
|
+
function primitive(ts, type) {
|
|
1469
|
+
if (type.isUnion())
|
|
1470
|
+
return (type.types ?? []).every((member) => primitive(ts, member));
|
|
1471
|
+
const flags = ts.TypeFlags;
|
|
1472
|
+
const mask = flags.StringLike | flags.NumberLike | flags.BigIntLike | flags.BooleanLike | flags.EnumLike | flags.ESSymbolLike | flags.VoidLike | flags.Null;
|
|
1473
|
+
return (type.flags & mask) !== 0;
|
|
1474
|
+
}
|
|
1475
|
+
function classicChecker(ts, checker) {
|
|
1476
|
+
const symbol = (handle) => handle;
|
|
1477
|
+
return {
|
|
1478
|
+
symbolsAt: async (nodes) => nodes.map((node) => checker.getSymbolAtLocation(node)),
|
|
1479
|
+
shorthandValue: async (property) => checker.getShorthandAssignmentValueSymbol(property),
|
|
1480
|
+
aliased: async (handle) => symbol(handle).flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol(handle)) : handle,
|
|
1481
|
+
module: (handle) => (symbol(handle).flags & ts.SymbolFlags.ValueModule) !== 0,
|
|
1482
|
+
declaration: async (handle) => symbol(handle).valueDeclaration ?? symbol(handle).declarations?.[0],
|
|
1483
|
+
declarationCount: (handle) => symbol(handle).declarations?.length ?? 0,
|
|
1484
|
+
exportsOf: async (handle) => checker.getExportsOfModule(symbol(handle)),
|
|
1485
|
+
primitiveAt: async (node) => primitive(ts, checker.getTypeAtLocation(node)),
|
|
1486
|
+
same: (left, right) => left === right
|
|
1487
|
+
};
|
|
1488
|
+
}
|
|
1489
|
+
|
|
1490
|
+
// plugins/scanners/react/src/http.ts
|
|
1491
|
+
async function reactHttpRequests(sources, analyzed, checker, callerOperation) {
|
|
1492
|
+
const context = urlContext(ts3, classicChecker(ts3, checker), analyzed);
|
|
1493
|
+
const calls = [];
|
|
1494
|
+
const visit = (node) => {
|
|
1495
|
+
if (ts3.isCallExpression(node))
|
|
1496
|
+
calls.push(node);
|
|
1497
|
+
ts3.forEachChild(node, visit);
|
|
1498
|
+
};
|
|
1499
|
+
for (const source of sources)
|
|
1500
|
+
visit(source);
|
|
1501
|
+
const requests = [];
|
|
1502
|
+
for (const call of calls) {
|
|
1503
|
+
const request = await fetchRequest(context, call, (callee) => runtimeFetch(context, callee)) ?? await axiosRequest(context, call);
|
|
1504
|
+
const operation = request === undefined ? undefined : callerOperation(call);
|
|
1505
|
+
if (request !== undefined && operation !== undefined)
|
|
1506
|
+
requests.push({ operation, ...request });
|
|
1507
|
+
}
|
|
1508
|
+
return requests;
|
|
1509
|
+
}
|
|
1510
|
+
|
|
244
1511
|
// plugins/scanners/react/src/scan.ts
|
|
245
1512
|
function relative(root, file) {
|
|
246
|
-
return
|
|
1513
|
+
return path5.relative(root, file).split(path5.sep).join("/");
|
|
247
1514
|
}
|
|
248
1515
|
function failDiagnostics(diagnostics) {
|
|
249
|
-
const errors = diagnostics.filter((item) => item.category ===
|
|
1516
|
+
const errors = diagnostics.filter((item) => item.category === ts4.DiagnosticCategory.Error);
|
|
250
1517
|
if (errors.length)
|
|
251
|
-
throw new Error(errors.map((item) => `${item.file?.fileName ?? "React"}: ${
|
|
1518
|
+
throw new Error(errors.map((item) => `${item.file?.fileName ?? "React"}: ${ts4.flattenDiagnosticMessageText(item.messageText, `
|
|
252
1519
|
`)}`).join(`
|
|
253
1520
|
`));
|
|
254
1521
|
}
|
|
255
1522
|
function reactProject(root, repositoryRoot = root) {
|
|
256
|
-
const manifestFile =
|
|
257
|
-
if (!
|
|
1523
|
+
const manifestFile = path5.join(root, "package.json");
|
|
1524
|
+
if (!existsSync3(manifestFile))
|
|
258
1525
|
return;
|
|
259
1526
|
const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
|
|
260
1527
|
if (!hasDependency(manifest, "react"))
|
|
261
1528
|
return;
|
|
262
1529
|
try {
|
|
263
|
-
|
|
264
|
-
const
|
|
265
|
-
const config = ts.readConfigFile(configFile, ts.sys.readFile);
|
|
1530
|
+
const configFile = path5.join(root, "tsconfig.json");
|
|
1531
|
+
const config = ts4.readConfigFile(configFile, ts4.sys.readFile);
|
|
266
1532
|
if (config.error)
|
|
267
1533
|
failDiagnostics([config.error]);
|
|
268
|
-
const parsed =
|
|
1534
|
+
const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, root, undefined, configFile);
|
|
269
1535
|
failDiagnostics(parsed.errors);
|
|
270
|
-
const program =
|
|
271
|
-
const
|
|
1536
|
+
const program = ts4.createProgram(parsed.fileNames, { ...parsed.options, noEmit: true });
|
|
1537
|
+
const owned = program.getSourceFiles().filter((source) => {
|
|
272
1538
|
const file = relative(repositoryRoot, source.fileName);
|
|
273
|
-
return
|
|
1539
|
+
return !source.isDeclarationFile && !file.startsWith("../") && !file.includes("node_modules/");
|
|
274
1540
|
});
|
|
1541
|
+
const sources = owned.filter((source) => source.fileName.endsWith(".tsx"));
|
|
275
1542
|
if (!sources.length)
|
|
276
1543
|
throw new Error("The project tsconfig.json must include React TSX source files.");
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
throw new Error("Install @types/react in the project.");
|
|
280
|
-
failDiagnostics(program.getOptionsDiagnostics());
|
|
1544
|
+
const routers = nextRouters(root);
|
|
1545
|
+
const routes = hasDependency(manifest, "next") ? owned.filter((source) => routeLocation(relative(root, source.fileName), routers) !== undefined) : [];
|
|
281
1546
|
failDiagnostics(program.getSyntacticDiagnostics());
|
|
282
|
-
|
|
283
|
-
failDiagnostics(program.getSemanticDiagnostics(source));
|
|
284
|
-
return { manifest, program, sources };
|
|
1547
|
+
return { manifest, program, owned, sources, routes, routers };
|
|
285
1548
|
} catch (error) {
|
|
286
|
-
throw new Error(`
|
|
1549
|
+
throw new Error(`REACT_SOURCE_INVALID: Check the project tsconfig.json and TSX syntax. ${error}`);
|
|
287
1550
|
}
|
|
288
1551
|
}
|
|
289
|
-
function executable(node) {
|
|
290
|
-
return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) && !!node.body;
|
|
291
|
-
}
|
|
292
1552
|
function caller(node) {
|
|
293
1553
|
for (let parent = node.parent;parent; parent = parent.parent)
|
|
294
1554
|
if (executable(parent))
|
|
@@ -296,20 +1556,20 @@ function caller(node) {
|
|
|
296
1556
|
return;
|
|
297
1557
|
}
|
|
298
1558
|
function writesSymbol(target, symbol, checker) {
|
|
299
|
-
if (
|
|
1559
|
+
if (ts4.isIdentifier(target))
|
|
300
1560
|
return checker.getSymbolAtLocation(target) === symbol;
|
|
301
|
-
if (
|
|
1561
|
+
if (ts4.isShorthandPropertyAssignment(target))
|
|
302
1562
|
return checker.getShorthandAssignmentValueSymbol(target) === symbol;
|
|
303
|
-
if (
|
|
1563
|
+
if (ts4.isArrayLiteralExpression(target))
|
|
304
1564
|
return target.elements.some((item) => writesSymbol(item, symbol, checker));
|
|
305
|
-
if (
|
|
1565
|
+
if (ts4.isObjectLiteralExpression(target))
|
|
306
1566
|
return target.properties.some((item) => writesSymbol(item, symbol, checker));
|
|
307
|
-
if (
|
|
1567
|
+
if (ts4.isPropertyAssignment(target))
|
|
308
1568
|
return writesSymbol(target.initializer, symbol, checker);
|
|
309
|
-
if (
|
|
1569
|
+
if (ts4.isSpreadElement(target) || ts4.isSpreadAssignment(target) || ts4.isParenthesizedExpression(target)) {
|
|
310
1570
|
return writesSymbol(target.expression, symbol, checker);
|
|
311
1571
|
}
|
|
312
|
-
if (
|
|
1572
|
+
if (ts4.isBinaryExpression(target) && target.operatorToken.kind === ts4.SyntaxKind.EqualsToken) {
|
|
313
1573
|
return writesSymbol(target.left, symbol, checker);
|
|
314
1574
|
}
|
|
315
1575
|
return false;
|
|
@@ -327,24 +1587,24 @@ class Evidence {
|
|
|
327
1587
|
this.checker = checker;
|
|
328
1588
|
this.files = new Set(sources);
|
|
329
1589
|
}
|
|
330
|
-
|
|
1590
|
+
operationId(node) {
|
|
331
1591
|
const file = relative(this.root, node.getSourceFile().fileName);
|
|
332
1592
|
const position = node.getStart();
|
|
333
1593
|
const id = `${file}#${position}`;
|
|
334
|
-
const name = node.name?.text ?? (
|
|
1594
|
+
const name = node.name?.text ?? (ts4.isVariableDeclaration(node.parent) ? node.parent.name.getText() : "callback");
|
|
335
1595
|
this.operations.set(id, { id, file, name, position });
|
|
336
1596
|
return id;
|
|
337
1597
|
}
|
|
338
1598
|
functionValue(name) {
|
|
339
1599
|
let symbol = this.checker.getSymbolAtLocation(name);
|
|
340
|
-
if (symbol && symbol.flags &
|
|
1600
|
+
if (symbol && symbol.flags & ts4.SymbolFlags.Alias)
|
|
341
1601
|
symbol = this.checker.getAliasedSymbol(symbol);
|
|
342
1602
|
const declaration = symbol?.valueDeclaration;
|
|
343
1603
|
if (!declaration || !this.files.has(declaration.getSourceFile()))
|
|
344
1604
|
return;
|
|
345
1605
|
if (executable(declaration))
|
|
346
1606
|
return declaration;
|
|
347
|
-
if (
|
|
1607
|
+
if (ts4.isVariableDeclaration(declaration) && declaration.parent.flags & ts4.NodeFlags.Const && declaration.initializer && executable(declaration.initializer))
|
|
348
1608
|
return declaration.initializer;
|
|
349
1609
|
return;
|
|
350
1610
|
}
|
|
@@ -360,10 +1620,10 @@ class Evidence {
|
|
|
360
1620
|
}
|
|
361
1621
|
propCalls(component, attribute) {
|
|
362
1622
|
const parameter = component.parameters[0];
|
|
363
|
-
if (!parameter || !
|
|
1623
|
+
if (!parameter || !ts4.isObjectBindingPattern(parameter.name))
|
|
364
1624
|
return [];
|
|
365
1625
|
const binding = parameter.name.elements.find((element) => !element.dotDotDotToken && !element.initializer && (element.propertyName ?? element.name).getText() === attribute.name.getText());
|
|
366
|
-
if (!binding || !
|
|
1626
|
+
if (!binding || !ts4.isIdentifier(binding.name))
|
|
367
1627
|
return [];
|
|
368
1628
|
const symbol = this.checker.getSymbolAtLocation(binding.name);
|
|
369
1629
|
if (!symbol)
|
|
@@ -372,25 +1632,25 @@ class Evidence {
|
|
|
372
1632
|
let reassigned = false;
|
|
373
1633
|
const checker = this.checker;
|
|
374
1634
|
const visit = (node) => {
|
|
375
|
-
if (
|
|
1635
|
+
if (ts4.isBinaryExpression(node) && node.operatorToken.kind >= ts4.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts4.SyntaxKind.LastAssignment && writesSymbol(node.left, symbol, checker))
|
|
376
1636
|
reassigned = true;
|
|
377
|
-
if (
|
|
1637
|
+
if (ts4.isForOfStatement(node) && writesSymbol(node.initializer, symbol, checker))
|
|
378
1638
|
reassigned = true;
|
|
379
|
-
if (
|
|
1639
|
+
if (ts4.isCallExpression(node) && ts4.isIdentifier(node.expression) && checker.getSymbolAtLocation(node.expression) === symbol)
|
|
380
1640
|
calls.push(node);
|
|
381
|
-
|
|
1641
|
+
ts4.forEachChild(node, visit);
|
|
382
1642
|
};
|
|
383
1643
|
visit(component);
|
|
384
1644
|
return reassigned ? [] : calls;
|
|
385
1645
|
}
|
|
386
1646
|
binding(component, attribute) {
|
|
387
1647
|
const initializer = attribute.initializer;
|
|
388
|
-
if (!initializer || !
|
|
1648
|
+
if (!initializer || !ts4.isJsxExpression(initializer) || !initializer.expression)
|
|
389
1649
|
return;
|
|
390
1650
|
const expression = initializer.expression;
|
|
391
1651
|
if (!this.checker.getTypeAtLocation(expression).getCallSignatures().length)
|
|
392
1652
|
return;
|
|
393
|
-
const target =
|
|
1653
|
+
const target = ts4.isIdentifier(expression) ? this.functionValue(expression) : undefined;
|
|
394
1654
|
const calls = this.propCalls(component, attribute);
|
|
395
1655
|
if (!target || !calls.length) {
|
|
396
1656
|
this.unsupported(attribute);
|
|
@@ -402,8 +1662,8 @@ class Evidence {
|
|
|
402
1662
|
if (!operation)
|
|
403
1663
|
continue;
|
|
404
1664
|
this.invocations.push({
|
|
405
|
-
source: this.
|
|
406
|
-
targets: [this.
|
|
1665
|
+
source: this.operationId(operation),
|
|
1666
|
+
targets: [this.operationId(target)],
|
|
407
1667
|
unresolved: false,
|
|
408
1668
|
member: attribute.name.getText(),
|
|
409
1669
|
position: call.getStart(),
|
|
@@ -420,30 +1680,28 @@ class Evidence {
|
|
|
420
1680
|
const component = this.functionValue(node.tagName);
|
|
421
1681
|
if (!component)
|
|
422
1682
|
return;
|
|
423
|
-
if (node.attributes.properties.some(
|
|
1683
|
+
if (node.attributes.properties.some(ts4.isJsxSpreadAttribute)) {
|
|
424
1684
|
this.unsupported(node);
|
|
425
1685
|
return;
|
|
426
1686
|
}
|
|
427
1687
|
for (const attribute of node.attributes.properties)
|
|
428
|
-
if (
|
|
1688
|
+
if (ts4.isJsxAttribute(attribute))
|
|
429
1689
|
this.binding(component, attribute);
|
|
430
1690
|
}
|
|
431
1691
|
inspect(node) {
|
|
432
|
-
if (
|
|
1692
|
+
if (ts4.isJsxSelfClosingElement(node) || ts4.isJsxOpeningElement(node))
|
|
433
1693
|
this.element(node);
|
|
434
|
-
|
|
1694
|
+
ts4.forEachChild(node, (child) => this.inspect(child));
|
|
435
1695
|
}
|
|
436
1696
|
}
|
|
437
1697
|
async function checkReactReadiness(root) {
|
|
438
|
-
const projects = await frameworkProjects(root, "react");
|
|
439
|
-
if (!projects.length)
|
|
440
|
-
throw new Error("REACT_PROJECT_REQUIRED: No React project declaration was found.");
|
|
1698
|
+
const projects = await frameworkProjects(root, "react", [".tsx"]);
|
|
441
1699
|
for (const project of projects)
|
|
442
1700
|
reactProject(project, root);
|
|
443
1701
|
}
|
|
444
1702
|
async function scanReact(root) {
|
|
445
1703
|
const parts = [];
|
|
446
|
-
for (const project of await frameworkProjects(root, "react")) {
|
|
1704
|
+
for (const project of await frameworkProjects(root, "react", [".tsx"])) {
|
|
447
1705
|
const observation = await scanReactProject(project, root);
|
|
448
1706
|
if (observation)
|
|
449
1707
|
parts.push({ key: relative(root, project), observation });
|
|
@@ -454,18 +1712,25 @@ async function scanReactProject(projectRoot, root) {
|
|
|
454
1712
|
const project = reactProject(projectRoot, root);
|
|
455
1713
|
if (!project)
|
|
456
1714
|
return;
|
|
457
|
-
const { manifest, program, sources } = project;
|
|
1715
|
+
const { manifest, program, owned, sources, routes, routers } = project;
|
|
458
1716
|
const evidence = new Evidence(root, program.getTypeChecker(), sources);
|
|
459
1717
|
for (const source of sources)
|
|
460
1718
|
evidence.inspect(source);
|
|
461
|
-
const
|
|
1719
|
+
const httpRequests = await reactHttpRequests(sources, owned, program.getTypeChecker(), (call) => {
|
|
1720
|
+
const operation = caller(call);
|
|
1721
|
+
return operation === undefined ? undefined : evidence.operationId(operation);
|
|
1722
|
+
});
|
|
1723
|
+
const httpEndpoints = nextRouteEndpoints(routes, projectRoot, routers, (handler) => evidence.operationId(handler));
|
|
1724
|
+
const files = [...new Set([...sources, ...routes].map((source) => relative(root, source.fileName)))].map((file) => ({ file, symbols: [] }));
|
|
462
1725
|
return createScanObservation({
|
|
463
|
-
scanner: { id: "react", technology: "typescript/react", engine: "typescript-sdk", engineVersion:
|
|
464
|
-
roots: [{ id: "react-project", kind: "package", name: manifest.name, file: relative(root,
|
|
1726
|
+
scanner: { id: "react", technology: "typescript/react", engine: "typescript-sdk", engineVersion: ts4.version },
|
|
1727
|
+
roots: [{ id: "react-project", kind: "package", name: manifest.name, file: relative(root, path5.join(projectRoot, "package.json")) }],
|
|
465
1728
|
files: files.map((file) => ({ ...file, roots: ["react-project"] })),
|
|
466
1729
|
operations: [...evidence.operations.values()],
|
|
467
1730
|
invocations: evidence.invocations,
|
|
468
|
-
diagnostics: evidence.diagnostics
|
|
1731
|
+
diagnostics: evidence.diagnostics,
|
|
1732
|
+
...httpEndpoints.length ? { httpEndpoints } : {},
|
|
1733
|
+
...httpRequests.length ? { httpRequests } : {}
|
|
469
1734
|
});
|
|
470
1735
|
}
|
|
471
1736
|
|
|
@@ -474,6 +1739,14 @@ var src_default = {
|
|
|
474
1739
|
id: "react",
|
|
475
1740
|
watch: { include: ["**/*.tsx", "**/*.ts", "**/tsconfig*.json", "**/package.json"], exclude: [] },
|
|
476
1741
|
checkReadiness: checkReactReadiness,
|
|
1742
|
+
listSourceFiles: (root) => frameworkSourceFiles({
|
|
1743
|
+
root,
|
|
1744
|
+
dependency: "react",
|
|
1745
|
+
projects: [".tsx"],
|
|
1746
|
+
sources: [".tsx"],
|
|
1747
|
+
also: routeCandidate
|
|
1748
|
+
}),
|
|
1749
|
+
readCodeStructure: (root, references) => readTypeScriptOutline(ts5, root, references),
|
|
477
1750
|
scan: scanReact
|
|
478
1751
|
};
|
|
479
1752
|
export {
|