@groma/scanner-react 0.1.3 → 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 +123 -8
- package/dist/index.js +1383 -121
- 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,60 +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, extensions) {
|
|
203
|
-
const files = await projectFiles(root, () => true);
|
|
204
|
-
const manifests = files.filter((file) => path.posix.basename(file) === "package.json");
|
|
205
|
-
const directories = new Set(manifests.map((file) => path.posix.dirname(file)));
|
|
206
|
-
const sources = new Set;
|
|
207
|
-
for (const file of files.filter((file) => extensions.some((extension) => file.endsWith(extension)) && !file.endsWith(".d.ts"))) {
|
|
208
|
-
let directory = path.posix.dirname(file);
|
|
209
|
-
while (directory !== "." && !directories.has(directory))
|
|
210
|
-
directory = path.posix.dirname(directory);
|
|
211
|
-
sources.add(directory);
|
|
212
|
-
}
|
|
213
|
-
const projects = [];
|
|
214
|
-
for (const file of manifests) {
|
|
215
|
-
const directory = path.posix.dirname(file);
|
|
216
|
-
if (!sources.has(directory) || !files.includes(path.posix.join(directory, "tsconfig.json")))
|
|
217
|
-
continue;
|
|
218
|
-
const manifest = JSON.parse(await readFile(path.join(root, file), "utf8"));
|
|
219
|
-
if (hasDependency(manifest, dependency))
|
|
220
|
-
projects.push(path.dirname(path.join(root, file)));
|
|
221
|
-
}
|
|
222
|
-
return projects;
|
|
223
|
-
}
|
|
712
|
+
import ts4 from "./typescript.cjs";
|
|
224
713
|
|
|
225
714
|
// plugins/scanners/observations.ts
|
|
226
715
|
function combineObservations(parts) {
|
|
@@ -231,6 +720,9 @@ function combineObservations(parts) {
|
|
|
231
720
|
const operations = [];
|
|
232
721
|
const invocations = [];
|
|
233
722
|
const diagnostics = [];
|
|
723
|
+
const sourceUnits = [];
|
|
724
|
+
const httpEndpoints = [];
|
|
725
|
+
const httpRequests = [];
|
|
234
726
|
for (const { key, observation } of parts) {
|
|
235
727
|
const id = (value) => JSON.stringify([key, value]);
|
|
236
728
|
roots.push(...observation.roots.map((root) => ({ ...root, id: id(root.id), ...root.parent ? { parent: id(root.parent) } : {} })));
|
|
@@ -242,7 +734,10 @@ function combineObservations(parts) {
|
|
|
242
734
|
}
|
|
243
735
|
operations.push(...(observation.operations ?? []).map((operation) => ({ ...operation, id: id(operation.id) })));
|
|
244
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) })));
|
|
245
739
|
diagnostics.push(...observation.diagnostics);
|
|
740
|
+
sourceUnits.push(...observation.sourceUnits ?? []);
|
|
246
741
|
}
|
|
247
742
|
return createScanObservation({
|
|
248
743
|
scanner: parts[0].observation.scanner,
|
|
@@ -250,58 +745,810 @@ function combineObservations(parts) {
|
|
|
250
745
|
files: [...files.values()],
|
|
251
746
|
operations,
|
|
252
747
|
invocations,
|
|
253
|
-
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 } : {}
|
|
254
752
|
});
|
|
255
753
|
}
|
|
256
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
|
+
|
|
257
1511
|
// plugins/scanners/react/src/scan.ts
|
|
258
1512
|
function relative(root, file) {
|
|
259
|
-
return
|
|
1513
|
+
return path5.relative(root, file).split(path5.sep).join("/");
|
|
260
1514
|
}
|
|
261
1515
|
function failDiagnostics(diagnostics) {
|
|
262
|
-
const errors = diagnostics.filter((item) => item.category ===
|
|
1516
|
+
const errors = diagnostics.filter((item) => item.category === ts4.DiagnosticCategory.Error);
|
|
263
1517
|
if (errors.length)
|
|
264
|
-
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, `
|
|
265
1519
|
`)}`).join(`
|
|
266
1520
|
`));
|
|
267
1521
|
}
|
|
268
1522
|
function reactProject(root, repositoryRoot = root) {
|
|
269
|
-
const manifestFile =
|
|
270
|
-
if (!
|
|
1523
|
+
const manifestFile = path5.join(root, "package.json");
|
|
1524
|
+
if (!existsSync3(manifestFile))
|
|
271
1525
|
return;
|
|
272
1526
|
const manifest = JSON.parse(readFileSync(manifestFile, "utf8"));
|
|
273
1527
|
if (!hasDependency(manifest, "react"))
|
|
274
1528
|
return;
|
|
275
1529
|
try {
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
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);
|
|
279
1532
|
if (config.error)
|
|
280
1533
|
failDiagnostics([config.error]);
|
|
281
|
-
const parsed =
|
|
1534
|
+
const parsed = ts4.parseJsonConfigFileContent(config.config, ts4.sys, root, undefined, configFile);
|
|
282
1535
|
failDiagnostics(parsed.errors);
|
|
283
|
-
const program =
|
|
284
|
-
const
|
|
1536
|
+
const program = ts4.createProgram(parsed.fileNames, { ...parsed.options, noEmit: true });
|
|
1537
|
+
const owned = program.getSourceFiles().filter((source) => {
|
|
285
1538
|
const file = relative(repositoryRoot, source.fileName);
|
|
286
|
-
return
|
|
1539
|
+
return !source.isDeclarationFile && !file.startsWith("../") && !file.includes("node_modules/");
|
|
287
1540
|
});
|
|
1541
|
+
const sources = owned.filter((source) => source.fileName.endsWith(".tsx"));
|
|
288
1542
|
if (!sources.length)
|
|
289
1543
|
throw new Error("The project tsconfig.json must include React TSX source files.");
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
throw new Error("Install @types/react in the project.");
|
|
293
|
-
failDiagnostics(program.getOptionsDiagnostics());
|
|
1544
|
+
const routers = nextRouters(root);
|
|
1545
|
+
const routes = hasDependency(manifest, "next") ? owned.filter((source) => routeLocation(relative(root, source.fileName), routers) !== undefined) : [];
|
|
294
1546
|
failDiagnostics(program.getSyntacticDiagnostics());
|
|
295
|
-
|
|
296
|
-
failDiagnostics(program.getSemanticDiagnostics(source));
|
|
297
|
-
return { manifest, program, sources };
|
|
1547
|
+
return { manifest, program, owned, sources, routes, routers };
|
|
298
1548
|
} catch (error) {
|
|
299
|
-
throw new Error(`
|
|
1549
|
+
throw new Error(`REACT_SOURCE_INVALID: Check the project tsconfig.json and TSX syntax. ${error}`);
|
|
300
1550
|
}
|
|
301
1551
|
}
|
|
302
|
-
function executable(node) {
|
|
303
|
-
return ts.isArrowFunction(node) || ts.isFunctionExpression(node) || ts.isFunctionDeclaration(node) && !!node.body;
|
|
304
|
-
}
|
|
305
1552
|
function caller(node) {
|
|
306
1553
|
for (let parent = node.parent;parent; parent = parent.parent)
|
|
307
1554
|
if (executable(parent))
|
|
@@ -309,20 +1556,20 @@ function caller(node) {
|
|
|
309
1556
|
return;
|
|
310
1557
|
}
|
|
311
1558
|
function writesSymbol(target, symbol, checker) {
|
|
312
|
-
if (
|
|
1559
|
+
if (ts4.isIdentifier(target))
|
|
313
1560
|
return checker.getSymbolAtLocation(target) === symbol;
|
|
314
|
-
if (
|
|
1561
|
+
if (ts4.isShorthandPropertyAssignment(target))
|
|
315
1562
|
return checker.getShorthandAssignmentValueSymbol(target) === symbol;
|
|
316
|
-
if (
|
|
1563
|
+
if (ts4.isArrayLiteralExpression(target))
|
|
317
1564
|
return target.elements.some((item) => writesSymbol(item, symbol, checker));
|
|
318
|
-
if (
|
|
1565
|
+
if (ts4.isObjectLiteralExpression(target))
|
|
319
1566
|
return target.properties.some((item) => writesSymbol(item, symbol, checker));
|
|
320
|
-
if (
|
|
1567
|
+
if (ts4.isPropertyAssignment(target))
|
|
321
1568
|
return writesSymbol(target.initializer, symbol, checker);
|
|
322
|
-
if (
|
|
1569
|
+
if (ts4.isSpreadElement(target) || ts4.isSpreadAssignment(target) || ts4.isParenthesizedExpression(target)) {
|
|
323
1570
|
return writesSymbol(target.expression, symbol, checker);
|
|
324
1571
|
}
|
|
325
|
-
if (
|
|
1572
|
+
if (ts4.isBinaryExpression(target) && target.operatorToken.kind === ts4.SyntaxKind.EqualsToken) {
|
|
326
1573
|
return writesSymbol(target.left, symbol, checker);
|
|
327
1574
|
}
|
|
328
1575
|
return false;
|
|
@@ -340,24 +1587,24 @@ class Evidence {
|
|
|
340
1587
|
this.checker = checker;
|
|
341
1588
|
this.files = new Set(sources);
|
|
342
1589
|
}
|
|
343
|
-
|
|
1590
|
+
operationId(node) {
|
|
344
1591
|
const file = relative(this.root, node.getSourceFile().fileName);
|
|
345
1592
|
const position = node.getStart();
|
|
346
1593
|
const id = `${file}#${position}`;
|
|
347
|
-
const name = node.name?.text ?? (
|
|
1594
|
+
const name = node.name?.text ?? (ts4.isVariableDeclaration(node.parent) ? node.parent.name.getText() : "callback");
|
|
348
1595
|
this.operations.set(id, { id, file, name, position });
|
|
349
1596
|
return id;
|
|
350
1597
|
}
|
|
351
1598
|
functionValue(name) {
|
|
352
1599
|
let symbol = this.checker.getSymbolAtLocation(name);
|
|
353
|
-
if (symbol && symbol.flags &
|
|
1600
|
+
if (symbol && symbol.flags & ts4.SymbolFlags.Alias)
|
|
354
1601
|
symbol = this.checker.getAliasedSymbol(symbol);
|
|
355
1602
|
const declaration = symbol?.valueDeclaration;
|
|
356
1603
|
if (!declaration || !this.files.has(declaration.getSourceFile()))
|
|
357
1604
|
return;
|
|
358
1605
|
if (executable(declaration))
|
|
359
1606
|
return declaration;
|
|
360
|
-
if (
|
|
1607
|
+
if (ts4.isVariableDeclaration(declaration) && declaration.parent.flags & ts4.NodeFlags.Const && declaration.initializer && executable(declaration.initializer))
|
|
361
1608
|
return declaration.initializer;
|
|
362
1609
|
return;
|
|
363
1610
|
}
|
|
@@ -373,10 +1620,10 @@ class Evidence {
|
|
|
373
1620
|
}
|
|
374
1621
|
propCalls(component, attribute) {
|
|
375
1622
|
const parameter = component.parameters[0];
|
|
376
|
-
if (!parameter || !
|
|
1623
|
+
if (!parameter || !ts4.isObjectBindingPattern(parameter.name))
|
|
377
1624
|
return [];
|
|
378
1625
|
const binding = parameter.name.elements.find((element) => !element.dotDotDotToken && !element.initializer && (element.propertyName ?? element.name).getText() === attribute.name.getText());
|
|
379
|
-
if (!binding || !
|
|
1626
|
+
if (!binding || !ts4.isIdentifier(binding.name))
|
|
380
1627
|
return [];
|
|
381
1628
|
const symbol = this.checker.getSymbolAtLocation(binding.name);
|
|
382
1629
|
if (!symbol)
|
|
@@ -385,25 +1632,25 @@ class Evidence {
|
|
|
385
1632
|
let reassigned = false;
|
|
386
1633
|
const checker = this.checker;
|
|
387
1634
|
const visit = (node) => {
|
|
388
|
-
if (
|
|
1635
|
+
if (ts4.isBinaryExpression(node) && node.operatorToken.kind >= ts4.SyntaxKind.FirstAssignment && node.operatorToken.kind <= ts4.SyntaxKind.LastAssignment && writesSymbol(node.left, symbol, checker))
|
|
389
1636
|
reassigned = true;
|
|
390
|
-
if (
|
|
1637
|
+
if (ts4.isForOfStatement(node) && writesSymbol(node.initializer, symbol, checker))
|
|
391
1638
|
reassigned = true;
|
|
392
|
-
if (
|
|
1639
|
+
if (ts4.isCallExpression(node) && ts4.isIdentifier(node.expression) && checker.getSymbolAtLocation(node.expression) === symbol)
|
|
393
1640
|
calls.push(node);
|
|
394
|
-
|
|
1641
|
+
ts4.forEachChild(node, visit);
|
|
395
1642
|
};
|
|
396
1643
|
visit(component);
|
|
397
1644
|
return reassigned ? [] : calls;
|
|
398
1645
|
}
|
|
399
1646
|
binding(component, attribute) {
|
|
400
1647
|
const initializer = attribute.initializer;
|
|
401
|
-
if (!initializer || !
|
|
1648
|
+
if (!initializer || !ts4.isJsxExpression(initializer) || !initializer.expression)
|
|
402
1649
|
return;
|
|
403
1650
|
const expression = initializer.expression;
|
|
404
1651
|
if (!this.checker.getTypeAtLocation(expression).getCallSignatures().length)
|
|
405
1652
|
return;
|
|
406
|
-
const target =
|
|
1653
|
+
const target = ts4.isIdentifier(expression) ? this.functionValue(expression) : undefined;
|
|
407
1654
|
const calls = this.propCalls(component, attribute);
|
|
408
1655
|
if (!target || !calls.length) {
|
|
409
1656
|
this.unsupported(attribute);
|
|
@@ -415,8 +1662,8 @@ class Evidence {
|
|
|
415
1662
|
if (!operation)
|
|
416
1663
|
continue;
|
|
417
1664
|
this.invocations.push({
|
|
418
|
-
source: this.
|
|
419
|
-
targets: [this.
|
|
1665
|
+
source: this.operationId(operation),
|
|
1666
|
+
targets: [this.operationId(target)],
|
|
420
1667
|
unresolved: false,
|
|
421
1668
|
member: attribute.name.getText(),
|
|
422
1669
|
position: call.getStart(),
|
|
@@ -433,18 +1680,18 @@ class Evidence {
|
|
|
433
1680
|
const component = this.functionValue(node.tagName);
|
|
434
1681
|
if (!component)
|
|
435
1682
|
return;
|
|
436
|
-
if (node.attributes.properties.some(
|
|
1683
|
+
if (node.attributes.properties.some(ts4.isJsxSpreadAttribute)) {
|
|
437
1684
|
this.unsupported(node);
|
|
438
1685
|
return;
|
|
439
1686
|
}
|
|
440
1687
|
for (const attribute of node.attributes.properties)
|
|
441
|
-
if (
|
|
1688
|
+
if (ts4.isJsxAttribute(attribute))
|
|
442
1689
|
this.binding(component, attribute);
|
|
443
1690
|
}
|
|
444
1691
|
inspect(node) {
|
|
445
|
-
if (
|
|
1692
|
+
if (ts4.isJsxSelfClosingElement(node) || ts4.isJsxOpeningElement(node))
|
|
446
1693
|
this.element(node);
|
|
447
|
-
|
|
1694
|
+
ts4.forEachChild(node, (child) => this.inspect(child));
|
|
448
1695
|
}
|
|
449
1696
|
}
|
|
450
1697
|
async function checkReactReadiness(root) {
|
|
@@ -465,18 +1712,25 @@ async function scanReactProject(projectRoot, root) {
|
|
|
465
1712
|
const project = reactProject(projectRoot, root);
|
|
466
1713
|
if (!project)
|
|
467
1714
|
return;
|
|
468
|
-
const { manifest, program, sources } = project;
|
|
1715
|
+
const { manifest, program, owned, sources, routes, routers } = project;
|
|
469
1716
|
const evidence = new Evidence(root, program.getTypeChecker(), sources);
|
|
470
1717
|
for (const source of sources)
|
|
471
1718
|
evidence.inspect(source);
|
|
472
|
-
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: [] }));
|
|
473
1725
|
return createScanObservation({
|
|
474
|
-
scanner: { id: "react", technology: "typescript/react", engine: "typescript-sdk", engineVersion:
|
|
475
|
-
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")) }],
|
|
476
1728
|
files: files.map((file) => ({ ...file, roots: ["react-project"] })),
|
|
477
1729
|
operations: [...evidence.operations.values()],
|
|
478
1730
|
invocations: evidence.invocations,
|
|
479
|
-
diagnostics: evidence.diagnostics
|
|
1731
|
+
diagnostics: evidence.diagnostics,
|
|
1732
|
+
...httpEndpoints.length ? { httpEndpoints } : {},
|
|
1733
|
+
...httpRequests.length ? { httpRequests } : {}
|
|
480
1734
|
});
|
|
481
1735
|
}
|
|
482
1736
|
|
|
@@ -485,6 +1739,14 @@ var src_default = {
|
|
|
485
1739
|
id: "react",
|
|
486
1740
|
watch: { include: ["**/*.tsx", "**/*.ts", "**/tsconfig*.json", "**/package.json"], exclude: [] },
|
|
487
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),
|
|
488
1750
|
scan: scanReact
|
|
489
1751
|
};
|
|
490
1752
|
export {
|