@groma/scanner-javascript 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +273 -0
- package/dist/index.js +2509 -0
- package/dist/typescript.cjs +201039 -0
- package/package.json +43 -0
- package/typescript-LICENSE.txt +55 -0
- package/typescript-ThirdPartyNoticeText.txt +193 -0
- package/validation.md +53 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2509 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// plugins/scanners/javascript/src/index.ts
|
|
3
|
+
import path4 from "path";
|
|
4
|
+
|
|
5
|
+
// packages/scanner/src/values.ts
|
|
6
|
+
function object(value, label) {
|
|
7
|
+
if (value === null || typeof value !== "object" || Array.isArray(value)) {
|
|
8
|
+
throw new Error(`${label} must be an object`);
|
|
9
|
+
}
|
|
10
|
+
return value;
|
|
11
|
+
}
|
|
12
|
+
function string(value, label) {
|
|
13
|
+
if (typeof value !== "string")
|
|
14
|
+
throw new Error(`${label} must be a string`);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
function array(value, label) {
|
|
18
|
+
if (!Array.isArray(value))
|
|
19
|
+
throw new Error(`${label} must be an array`);
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// packages/scanner/src/http.ts
|
|
24
|
+
var pathText = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
|
|
25
|
+
var methodToken = /^[A-Z][A-Z-]*$/;
|
|
26
|
+
function fail(message) {
|
|
27
|
+
throw new Error(message);
|
|
28
|
+
}
|
|
29
|
+
function text(value, label) {
|
|
30
|
+
if (typeof value !== "string" || !pathText.test(value))
|
|
31
|
+
fail(`${label} must be nonempty URL path characters`);
|
|
32
|
+
return value;
|
|
33
|
+
}
|
|
34
|
+
function method(value, label, anyMethod) {
|
|
35
|
+
if (typeof value !== "string" || !(methodToken.test(value) || anyMethod && value === "*")) {
|
|
36
|
+
fail(`${label} must be an uppercase HTTP method${anyMethod ? " or *" : ""}`);
|
|
37
|
+
}
|
|
38
|
+
return value;
|
|
39
|
+
}
|
|
40
|
+
function operation(value, operations) {
|
|
41
|
+
if (typeof value !== "string" || !operations.has(value))
|
|
42
|
+
fail("HTTP fact references unknown operation");
|
|
43
|
+
return value;
|
|
44
|
+
}
|
|
45
|
+
function endpointSegment(value, last) {
|
|
46
|
+
const segment = object(value, "HTTP endpoint segment");
|
|
47
|
+
if (segment.kind === "literal")
|
|
48
|
+
return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
|
|
49
|
+
if (segment.kind !== "parameter" && segment.kind !== "catch-all")
|
|
50
|
+
fail(`unknown HTTP endpoint segment kind: ${String(segment.kind)}`);
|
|
51
|
+
if (segment.kind === "catch-all" && !last)
|
|
52
|
+
fail("an HTTP catch-all segment must be last");
|
|
53
|
+
for (const flag of ["optional", "constrained"]) {
|
|
54
|
+
if (segment[flag] !== undefined && typeof segment[flag] !== "boolean")
|
|
55
|
+
fail(`HTTP segment ${flag} must be a boolean`);
|
|
56
|
+
}
|
|
57
|
+
return {
|
|
58
|
+
kind: segment.kind,
|
|
59
|
+
name: text(segment.name, "HTTP segment name"),
|
|
60
|
+
...segment.optional ? { optional: true } : {},
|
|
61
|
+
...segment.constrained ? { constrained: true } : {}
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function requestSegment(value) {
|
|
65
|
+
const segment = object(value, "HTTP request segment");
|
|
66
|
+
if (segment.kind === "literal")
|
|
67
|
+
return { kind: "literal", value: text(segment.value, "HTTP literal segment") };
|
|
68
|
+
if (segment.kind === "dynamic" || segment.kind === "unknown")
|
|
69
|
+
return { kind: segment.kind };
|
|
70
|
+
return fail(`unknown HTTP request segment kind: ${String(segment.kind)}`);
|
|
71
|
+
}
|
|
72
|
+
function order(value) {
|
|
73
|
+
const fact = object(value, "HTTP endpoint order");
|
|
74
|
+
const application = string(fact.application, "HTTP endpoint order application");
|
|
75
|
+
if (application === "")
|
|
76
|
+
fail("HTTP endpoint order application must be nonempty");
|
|
77
|
+
const position = fact.position;
|
|
78
|
+
if (typeof position !== "number" || !Number.isInteger(position) || position < 0) {
|
|
79
|
+
fail("HTTP endpoint order position must be a nonnegative integer");
|
|
80
|
+
}
|
|
81
|
+
return { application, position };
|
|
82
|
+
}
|
|
83
|
+
function endpoint(value, operations) {
|
|
84
|
+
const fact = object(value, "HTTP endpoint");
|
|
85
|
+
const path = array(fact.path, "HTTP endpoint path");
|
|
86
|
+
return {
|
|
87
|
+
operation: operation(fact.operation, operations),
|
|
88
|
+
method: method(fact.method, "HTTP endpoint method", true),
|
|
89
|
+
path: path.map((segment, index) => endpointSegment(segment, index === path.length - 1)),
|
|
90
|
+
...fact.order === undefined ? {} : { order: order(fact.order) }
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function request(value, operations) {
|
|
94
|
+
const fact = object(value, "HTTP request");
|
|
95
|
+
if (fact.configured !== undefined && typeof fact.configured !== "boolean")
|
|
96
|
+
fail("HTTP request configured must be a boolean");
|
|
97
|
+
return {
|
|
98
|
+
operation: operation(fact.operation, operations),
|
|
99
|
+
...fact.method === undefined ? {} : { method: method(fact.method, "HTTP request method", false) },
|
|
100
|
+
...fact.configured ? { configured: true } : {},
|
|
101
|
+
path: array(fact.path, "HTTP request path").map(requestSegment)
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function httpEvidence(endpoints, requests, operations) {
|
|
105
|
+
if (endpoints === undefined && requests === undefined)
|
|
106
|
+
return {};
|
|
107
|
+
if (operations === undefined)
|
|
108
|
+
fail("HTTP facts require operation declarations");
|
|
109
|
+
return {
|
|
110
|
+
...endpoints === undefined ? {} : { httpEndpoints: array(endpoints, "httpEndpoints").map((fact) => endpoint(fact, operations)) },
|
|
111
|
+
...requests === undefined ? {} : { httpRequests: array(requests, "httpRequests").map((fact) => request(fact, operations)) }
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// packages/scanner/src/index.ts
|
|
116
|
+
function compare(...values) {
|
|
117
|
+
return values.join("\x00");
|
|
118
|
+
}
|
|
119
|
+
function uniqueBy(values, key, label) {
|
|
120
|
+
const seen = new Set;
|
|
121
|
+
for (const value of values) {
|
|
122
|
+
const id = key(value);
|
|
123
|
+
if (seen.has(id))
|
|
124
|
+
throw new Error(`duplicate ${label}: ${id}`);
|
|
125
|
+
seen.add(id);
|
|
126
|
+
}
|
|
127
|
+
return [...values].sort((left, right) => key(left).localeCompare(key(right)));
|
|
128
|
+
}
|
|
129
|
+
function createScanObservation(input) {
|
|
130
|
+
const roots = validateRoots(input.roots);
|
|
131
|
+
const rootIds = new Set(roots.map((root) => root.id));
|
|
132
|
+
const files = uniqueBy(input.files.map((file) => ({
|
|
133
|
+
...file,
|
|
134
|
+
roots: [...new Set(file.roots)].sort(),
|
|
135
|
+
symbols: [...new Map(file.symbols.map((symbol) => [
|
|
136
|
+
compare(symbol.id, symbol.kind),
|
|
137
|
+
symbol
|
|
138
|
+
])).values()].sort((left, right) => {
|
|
139
|
+
return compare(left.id, left.kind).localeCompare(compare(right.id, right.kind));
|
|
140
|
+
})
|
|
141
|
+
})), (file) => file.file, "file path");
|
|
142
|
+
const filePaths = new Set(files.map((file) => file.file));
|
|
143
|
+
for (const file of files) {
|
|
144
|
+
if (file.roots.length === 0)
|
|
145
|
+
throw new Error(`file has no root: ${file.file}`);
|
|
146
|
+
for (const root of file.roots) {
|
|
147
|
+
if (!rootIds.has(root))
|
|
148
|
+
throw new Error(`file references unknown root: ${root}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const diagnostic of input.diagnostics)
|
|
152
|
+
diagnosticLocation(diagnostic);
|
|
153
|
+
return {
|
|
154
|
+
schemaVersion: 1,
|
|
155
|
+
scanner: input.scanner,
|
|
156
|
+
roots,
|
|
157
|
+
files,
|
|
158
|
+
...sourceUnits(input.sourceUnits, filePaths),
|
|
159
|
+
...operationEvidence(input, filePaths),
|
|
160
|
+
...httpEvidence(input.httpEndpoints, input.httpRequests, input.operations && new Set(input.operations.map((operation) => operation.id))),
|
|
161
|
+
diagnostics: [...new Map(input.diagnostics.map((diagnostic) => [
|
|
162
|
+
diagnosticKey(diagnostic),
|
|
163
|
+
diagnostic
|
|
164
|
+
])).values()].sort((left, right) => {
|
|
165
|
+
return diagnosticKey(left).localeCompare(diagnosticKey(right));
|
|
166
|
+
})
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function sourceUnits(input, paths) {
|
|
170
|
+
if (input === undefined)
|
|
171
|
+
return {};
|
|
172
|
+
const units = array(input, "sourceUnits").map((entry) => {
|
|
173
|
+
const unit = object(entry, "source unit");
|
|
174
|
+
const primary = string(unit.primary, "source unit.primary");
|
|
175
|
+
const files = [...new Set(array(unit.files, "source unit.files").map((file) => string(file, "source unit.file")))].sort();
|
|
176
|
+
if (!files.includes(primary))
|
|
177
|
+
throw new Error(`source unit omits primary file: ${primary}`);
|
|
178
|
+
for (const file of files) {
|
|
179
|
+
if (!paths.has(file))
|
|
180
|
+
throw new Error(`source unit references unknown file: ${file}`);
|
|
181
|
+
}
|
|
182
|
+
return { primary, files };
|
|
183
|
+
});
|
|
184
|
+
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))) };
|
|
185
|
+
}
|
|
186
|
+
function validateRoots(input) {
|
|
187
|
+
const roots = uniqueBy(input, (root) => root.id, "root id");
|
|
188
|
+
const byId = new Map(roots.map((root) => [root.id, root]));
|
|
189
|
+
for (const root of roots) {
|
|
190
|
+
const visited = new Set([root.id]);
|
|
191
|
+
let parent = root.parent;
|
|
192
|
+
while (parent !== undefined) {
|
|
193
|
+
if (visited.has(parent))
|
|
194
|
+
throw new Error(`root hierarchy contains a cycle: ${parent}`);
|
|
195
|
+
visited.add(parent);
|
|
196
|
+
const ancestor = byId.get(parent);
|
|
197
|
+
if (ancestor === undefined)
|
|
198
|
+
throw new Error(`root references unknown parent: ${parent}`);
|
|
199
|
+
parent = ancestor.parent;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return roots;
|
|
203
|
+
}
|
|
204
|
+
function diagnosticKey(diagnostic) {
|
|
205
|
+
return compare(diagnostic.severity, diagnostic.code, diagnostic.message, diagnostic.file ?? "", String(diagnostic.line ?? ""));
|
|
206
|
+
}
|
|
207
|
+
function diagnosticLocation(value) {
|
|
208
|
+
if (value.line !== undefined && (!Number.isInteger(value.line) || Number(value.line) < 1)) {
|
|
209
|
+
throw new Error("diagnostic.line must be a positive integer");
|
|
210
|
+
}
|
|
211
|
+
return {
|
|
212
|
+
...value.file === undefined ? {} : { file: string(value.file, "diagnostic.file") },
|
|
213
|
+
...value.line === undefined ? {} : { line: Number(value.line) }
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function operationEvidence(input, files) {
|
|
217
|
+
if (input.operations === undefined && input.invocations !== undefined)
|
|
218
|
+
throw new Error("invocations require operation declarations");
|
|
219
|
+
if (input.operations === undefined)
|
|
220
|
+
return {};
|
|
221
|
+
const operations = uniqueBy(input.operations, (operation) => operation.id, "operation id");
|
|
222
|
+
const ids = new Set(operations.map((operation) => operation.id));
|
|
223
|
+
for (const operation of operations) {
|
|
224
|
+
if (!files.has(operation.file))
|
|
225
|
+
throw new Error(`operation references unknown file: ${operation.file}`);
|
|
226
|
+
sourcePosition(operation.position);
|
|
227
|
+
validateOperationTokens(operation);
|
|
228
|
+
}
|
|
229
|
+
const invocations = input.invocations ?? [];
|
|
230
|
+
for (const invocation of invocations)
|
|
231
|
+
validateInvocation(invocation, ids, files);
|
|
232
|
+
return { operations, invocations };
|
|
233
|
+
}
|
|
234
|
+
function operationTokens(operation) {
|
|
235
|
+
if (operation.tokens === undefined) {
|
|
236
|
+
if (operation.startLine !== undefined || operation.endLine !== undefined) {
|
|
237
|
+
throw new Error("operation range requires tokens");
|
|
238
|
+
}
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
if (operation.startLine === undefined || operation.endLine === undefined) {
|
|
242
|
+
throw new Error("operation tokens require a source range");
|
|
243
|
+
}
|
|
244
|
+
if (!Number.isInteger(operation.startLine) || Number(operation.startLine) < 1) {
|
|
245
|
+
throw new Error("operation.startLine must be a positive integer");
|
|
246
|
+
}
|
|
247
|
+
if (!Number.isInteger(operation.endLine) || Number(operation.endLine) < 1) {
|
|
248
|
+
throw new Error("operation.endLine must be a positive integer");
|
|
249
|
+
}
|
|
250
|
+
const startLine = Number(operation.startLine);
|
|
251
|
+
const endLine = Number(operation.endLine);
|
|
252
|
+
if (endLine < startLine)
|
|
253
|
+
throw new Error("operation.endLine must be at or after startLine");
|
|
254
|
+
return {
|
|
255
|
+
startLine,
|
|
256
|
+
endLine,
|
|
257
|
+
tokens: array(operation.tokens, "operation.tokens").map((token) => string(token, "operation.token"))
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
function validateOperationTokens(operation) {
|
|
261
|
+
operationTokens({
|
|
262
|
+
tokens: operation.tokens,
|
|
263
|
+
startLine: operation.startLine,
|
|
264
|
+
endLine: operation.endLine
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
function validateInvocation(invocation, ids, files) {
|
|
268
|
+
sourcePosition(invocation.position);
|
|
269
|
+
sourcePosition(invocation.binding?.position);
|
|
270
|
+
if (!invocation.targets.length && !invocation.unresolved)
|
|
271
|
+
throw new Error("an empty target set must be unresolved");
|
|
272
|
+
if (!ids.has(invocation.source) || invocation.targets.some((target) => !ids.has(target))) {
|
|
273
|
+
throw new Error("invocation references unknown operation");
|
|
274
|
+
}
|
|
275
|
+
if (invocation.binding && !files.has(invocation.binding.file))
|
|
276
|
+
throw new Error("invocation binding references unknown file");
|
|
277
|
+
}
|
|
278
|
+
function sourcePosition(position) {
|
|
279
|
+
if (position === undefined)
|
|
280
|
+
return {};
|
|
281
|
+
if (!Number.isInteger(position) || Number(position) < 0)
|
|
282
|
+
throw new Error("source position must be a non-negative integer");
|
|
283
|
+
return { position: Number(position) };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// plugins/scanners/javascript/src/index.ts
|
|
287
|
+
import ts6 from "./typescript.cjs";
|
|
288
|
+
|
|
289
|
+
// plugins/scanners/javascript/src/evidence.ts
|
|
290
|
+
import ts2 from "./typescript.cjs";
|
|
291
|
+
|
|
292
|
+
// plugins/scanners/typescript-operations.ts
|
|
293
|
+
function is(node, kind) {
|
|
294
|
+
return node !== undefined && node.kind === kind;
|
|
295
|
+
}
|
|
296
|
+
function vocabularyOf(k) {
|
|
297
|
+
return {
|
|
298
|
+
leading: new Map([
|
|
299
|
+
[k.TrueKeyword, "true"],
|
|
300
|
+
[k.FalseKeyword, "false"],
|
|
301
|
+
[k.NullKeyword, "null"],
|
|
302
|
+
[k.ThisKeyword, "this"],
|
|
303
|
+
[k.SuperKeyword, "super"],
|
|
304
|
+
[k.QuestionDotToken, "?."],
|
|
305
|
+
[k.SpreadElement, "..."],
|
|
306
|
+
[k.SpreadAssignment, "..."],
|
|
307
|
+
[k.ElementAccessExpression, "index"],
|
|
308
|
+
[k.ReturnStatement, "return"],
|
|
309
|
+
[k.ForStatement, "for"],
|
|
310
|
+
[k.ForInStatement, "for"],
|
|
311
|
+
[k.ForOfStatement, "for"],
|
|
312
|
+
[k.WhileStatement, "while"],
|
|
313
|
+
[k.DoStatement, "do"],
|
|
314
|
+
[k.SwitchStatement, "switch"],
|
|
315
|
+
[k.CaseClause, "case"],
|
|
316
|
+
[k.DefaultClause, "default"],
|
|
317
|
+
[k.BreakStatement, "break"],
|
|
318
|
+
[k.ContinueStatement, "continue"],
|
|
319
|
+
[k.ThrowStatement, "throw"],
|
|
320
|
+
[k.CatchClause, "catch"],
|
|
321
|
+
[k.AwaitKeyword, "await"],
|
|
322
|
+
[k.AwaitExpression, "await"],
|
|
323
|
+
[k.YieldExpression, "yield"],
|
|
324
|
+
[k.TypeOfExpression, "typeof"],
|
|
325
|
+
[k.VoidExpression, "void"],
|
|
326
|
+
[k.DeleteExpression, "delete"]
|
|
327
|
+
]),
|
|
328
|
+
quoted: new Set([k.StringLiteral, k.NoSubstitutionTemplateLiteral, k.TemplateHead, k.TemplateMiddle, k.TemplateTail]),
|
|
329
|
+
verbatim: new Set([k.NumericLiteral, k.BigIntLiteral, k.RegularExpressionLiteral]),
|
|
330
|
+
unary: new Map([
|
|
331
|
+
[k.ExclamationToken, "!"],
|
|
332
|
+
[k.MinusToken, "-"],
|
|
333
|
+
[k.PlusToken, "+"],
|
|
334
|
+
[k.TildeToken, "~"],
|
|
335
|
+
[k.PlusPlusToken, "++"],
|
|
336
|
+
[k.MinusMinusToken, "--"]
|
|
337
|
+
]),
|
|
338
|
+
operators: new Set([
|
|
339
|
+
k.BinaryExpression,
|
|
340
|
+
k.ConditionalExpression,
|
|
341
|
+
k.PrefixUnaryExpression,
|
|
342
|
+
k.PostfixUnaryExpression,
|
|
343
|
+
k.TypeOfExpression,
|
|
344
|
+
k.VoidExpression,
|
|
345
|
+
k.DeleteExpression,
|
|
346
|
+
k.AwaitExpression,
|
|
347
|
+
k.YieldExpression
|
|
348
|
+
]),
|
|
349
|
+
types: new Set([
|
|
350
|
+
k.TypeParameter,
|
|
351
|
+
k.ArrayType,
|
|
352
|
+
k.FunctionType,
|
|
353
|
+
k.LiteralType,
|
|
354
|
+
k.TypeLiteral,
|
|
355
|
+
k.ExpressionWithTypeArguments,
|
|
356
|
+
k.TypeReference,
|
|
357
|
+
k.ConditionalType
|
|
358
|
+
]),
|
|
359
|
+
assertions: new Set([k.AsExpression, k.SatisfiesExpression, k.NonNullExpression]),
|
|
360
|
+
functions: new Set([
|
|
361
|
+
k.ArrowFunction,
|
|
362
|
+
k.FunctionExpression,
|
|
363
|
+
k.FunctionDeclaration,
|
|
364
|
+
k.MethodDeclaration,
|
|
365
|
+
k.Constructor,
|
|
366
|
+
k.GetAccessor,
|
|
367
|
+
k.SetAccessor
|
|
368
|
+
])
|
|
369
|
+
};
|
|
370
|
+
}
|
|
371
|
+
function typeScriptOperations(ts) {
|
|
372
|
+
const k = ts.SyntaxKind;
|
|
373
|
+
const words = vocabularyOf(k);
|
|
374
|
+
function executable(node) {
|
|
375
|
+
if (node.kind === k.ArrowFunction || node.kind === k.FunctionExpression)
|
|
376
|
+
return true;
|
|
377
|
+
const declared = node.kind === k.FunctionDeclaration || node.kind === k.MethodDeclaration || node.kind === k.Constructor;
|
|
378
|
+
return declared && node.body !== undefined;
|
|
379
|
+
}
|
|
380
|
+
function callArgumentProperty(node) {
|
|
381
|
+
const property = is(node.parent, k.PropertyAssignment) ? node.parent : node;
|
|
382
|
+
if (property.kind !== k.PropertyAssignment && property.kind !== k.MethodDeclaration)
|
|
383
|
+
return false;
|
|
384
|
+
if (!is(property.parent, k.ObjectLiteralExpression))
|
|
385
|
+
return false;
|
|
386
|
+
let holder = property.parent.parent;
|
|
387
|
+
while (holder !== undefined && (holder.kind === k.ParenthesizedExpression || words.assertions.has(holder.kind))) {
|
|
388
|
+
holder = holder.parent;
|
|
389
|
+
}
|
|
390
|
+
return is(holder, k.CallExpression) || is(holder, k.NewExpression);
|
|
391
|
+
}
|
|
392
|
+
function comparableName(node) {
|
|
393
|
+
if (!executable(node) || callArgumentProperty(node))
|
|
394
|
+
return;
|
|
395
|
+
if (node.kind === k.Constructor)
|
|
396
|
+
return "constructor";
|
|
397
|
+
const name = node.name;
|
|
398
|
+
if (is(name, k.Identifier))
|
|
399
|
+
return name.text;
|
|
400
|
+
const parent = node.parent;
|
|
401
|
+
if (is(parent, k.PropertyAssignment) || is(parent, k.VariableDeclaration))
|
|
402
|
+
return parent.name.getText();
|
|
403
|
+
return;
|
|
404
|
+
}
|
|
405
|
+
function operationFields(node) {
|
|
406
|
+
const name = comparableName(node);
|
|
407
|
+
if (name === undefined)
|
|
408
|
+
return { name: "(anonymous)" };
|
|
409
|
+
const source = node.getSourceFile();
|
|
410
|
+
const line = (position) => source.getLineAndCharacterOfPosition(position).line + 1;
|
|
411
|
+
return { name, startLine: line(node.getStart()), endLine: line(node.end), tokens: tokenize(k, words, node) };
|
|
412
|
+
}
|
|
413
|
+
return { executable, operationFields };
|
|
414
|
+
}
|
|
415
|
+
function lookup(scope, name) {
|
|
416
|
+
return scope.slots.get(name) ?? (scope.parent === undefined ? undefined : lookup(scope.parent, name));
|
|
417
|
+
}
|
|
418
|
+
function tokenize(k, words, operation) {
|
|
419
|
+
const tokens = [];
|
|
420
|
+
let next = 0;
|
|
421
|
+
let scope = { slots: new Map };
|
|
422
|
+
function bindPattern(part) {
|
|
423
|
+
if (part === undefined)
|
|
424
|
+
return;
|
|
425
|
+
if (is(part, k.Identifier)) {
|
|
426
|
+
scope.slots.set(part.text, next++);
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
if (is(part, k.BindingElement))
|
|
430
|
+
bindElement(part);
|
|
431
|
+
else
|
|
432
|
+
part.forEachChild((child) => bindPattern(child));
|
|
433
|
+
}
|
|
434
|
+
function bindElement(element) {
|
|
435
|
+
const key = element.propertyName ?? (is(element.parent, k.ObjectBindingPattern) ? element.name : undefined);
|
|
436
|
+
if (is(key, k.Identifier) || is(key, k.StringLiteral) || is(key, k.NumericLiteral))
|
|
437
|
+
tokens.push(`k.${key.text}`);
|
|
438
|
+
else
|
|
439
|
+
walk(key);
|
|
440
|
+
bindPattern(element.name);
|
|
441
|
+
walk(element.initializer);
|
|
442
|
+
}
|
|
443
|
+
function inScope(run) {
|
|
444
|
+
const previous = scope;
|
|
445
|
+
scope = { parent: previous, slots: new Map };
|
|
446
|
+
run();
|
|
447
|
+
scope = previous;
|
|
448
|
+
}
|
|
449
|
+
function declare(node) {
|
|
450
|
+
bindPattern(node.name);
|
|
451
|
+
walk(node.initializer);
|
|
452
|
+
}
|
|
453
|
+
function declareParameters(node) {
|
|
454
|
+
for (const parameter of node.parameters)
|
|
455
|
+
if (is(parameter, k.Parameter))
|
|
456
|
+
declare(parameter);
|
|
457
|
+
}
|
|
458
|
+
function identifierToken(node) {
|
|
459
|
+
const parent = node.parent;
|
|
460
|
+
if (is(parent, k.PropertyAccessExpression) && parent.name === node)
|
|
461
|
+
return `.${node.text}`;
|
|
462
|
+
const key = is(parent, k.PropertyAssignment) || is(parent, k.ShorthandPropertyAssignment);
|
|
463
|
+
if (key && parent.name === node)
|
|
464
|
+
return `k.${node.text}`;
|
|
465
|
+
const slot = lookup(scope, node.text);
|
|
466
|
+
return slot === undefined ? node.text : `$${slot}`;
|
|
467
|
+
}
|
|
468
|
+
function groupsOperator(expression) {
|
|
469
|
+
let node = expression;
|
|
470
|
+
while (words.assertions.has(node.kind))
|
|
471
|
+
node = node.expression;
|
|
472
|
+
return words.operators.has(node.kind);
|
|
473
|
+
}
|
|
474
|
+
function emitText(node) {
|
|
475
|
+
if (words.quoted.has(node.kind))
|
|
476
|
+
tokens.push(JSON.stringify(node.text));
|
|
477
|
+
else if (words.verbatim.has(node.kind))
|
|
478
|
+
tokens.push(node.text);
|
|
479
|
+
else
|
|
480
|
+
return false;
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
function emitCall(node) {
|
|
484
|
+
if (!is(node, k.CallExpression) && !is(node, k.NewExpression))
|
|
485
|
+
return false;
|
|
486
|
+
if (node.kind === k.NewExpression)
|
|
487
|
+
tokens.push("new");
|
|
488
|
+
walk(node.expression);
|
|
489
|
+
walk(node.questionDotToken);
|
|
490
|
+
tokens.push("call");
|
|
491
|
+
for (const argument of node.arguments ?? [])
|
|
492
|
+
walk(argument);
|
|
493
|
+
return true;
|
|
494
|
+
}
|
|
495
|
+
function emitOperator(node) {
|
|
496
|
+
if (is(node, k.BinaryExpression)) {
|
|
497
|
+
walk(node.left);
|
|
498
|
+
tokens.push(node.operatorToken.getText());
|
|
499
|
+
walk(node.right);
|
|
500
|
+
} else if (is(node, k.PrefixUnaryExpression)) {
|
|
501
|
+
tokens.push(words.unary.get(node.operator));
|
|
502
|
+
walk(node.operand);
|
|
503
|
+
} else if (is(node, k.PostfixUnaryExpression)) {
|
|
504
|
+
walk(node.operand);
|
|
505
|
+
tokens.push(words.unary.get(node.operator));
|
|
506
|
+
} else if (is(node, k.ConditionalExpression)) {
|
|
507
|
+
walk(node.condition);
|
|
508
|
+
tokens.push("?");
|
|
509
|
+
walk(node.whenTrue);
|
|
510
|
+
tokens.push(":");
|
|
511
|
+
walk(node.whenFalse);
|
|
512
|
+
} else if (is(node, k.ParenthesizedExpression) && groupsOperator(node.expression)) {
|
|
513
|
+
tokens.push("(");
|
|
514
|
+
walk(node.expression);
|
|
515
|
+
tokens.push(")");
|
|
516
|
+
} else
|
|
517
|
+
return false;
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
function emitBranches(node) {
|
|
521
|
+
if (is(node, k.IfStatement)) {
|
|
522
|
+
tokens.push("if");
|
|
523
|
+
walk(node.expression);
|
|
524
|
+
walk(node.thenStatement);
|
|
525
|
+
branch("else", node.elseStatement);
|
|
526
|
+
} else if (is(node, k.TryStatement)) {
|
|
527
|
+
tokens.push("try");
|
|
528
|
+
walk(node.tryBlock);
|
|
529
|
+
walk(node.catchClause);
|
|
530
|
+
branch("finally", node.finallyBlock);
|
|
531
|
+
} else
|
|
532
|
+
return false;
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
function branch(keyword, statement) {
|
|
536
|
+
if (statement === undefined)
|
|
537
|
+
return;
|
|
538
|
+
tokens.push(keyword);
|
|
539
|
+
walk(statement);
|
|
540
|
+
}
|
|
541
|
+
function emitStructure(node) {
|
|
542
|
+
if (node.kind === k.Block)
|
|
543
|
+
inScope(() => node.forEachChild((child) => walk(child)));
|
|
544
|
+
else if (is(node, k.VariableDeclaration) || is(node, k.Parameter))
|
|
545
|
+
declare(node);
|
|
546
|
+
else if (words.functions.has(node.kind)) {
|
|
547
|
+
tokens.push("fn");
|
|
548
|
+
inScope(() => {
|
|
549
|
+
declareParameters(node);
|
|
550
|
+
walk(node.body);
|
|
551
|
+
});
|
|
552
|
+
} else
|
|
553
|
+
return false;
|
|
554
|
+
return true;
|
|
555
|
+
}
|
|
556
|
+
function walk(node) {
|
|
557
|
+
if (node === undefined || words.types.has(node.kind) || emitStructure(node))
|
|
558
|
+
return;
|
|
559
|
+
if (is(node, k.Identifier) || is(node, k.PrivateIdentifier)) {
|
|
560
|
+
tokens.push(identifierToken(node));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (emitText(node) || emitCall(node) || emitOperator(node) || emitBranches(node))
|
|
564
|
+
return;
|
|
565
|
+
const leading = words.leading.get(node.kind);
|
|
566
|
+
if (leading !== undefined)
|
|
567
|
+
tokens.push(leading);
|
|
568
|
+
node.forEachChild((child) => walk(child));
|
|
569
|
+
}
|
|
570
|
+
declareParameters(operation);
|
|
571
|
+
walk(operation.body);
|
|
572
|
+
return tokens;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// plugins/scanners/javascript/src/declarations.ts
|
|
576
|
+
import ts from "./typescript.cjs";
|
|
577
|
+
function isFunctionValue(node) {
|
|
578
|
+
return node !== undefined && (ts.isArrowFunction(node) || ts.isFunctionExpression(node));
|
|
579
|
+
}
|
|
580
|
+
function topLevelDeclarations(source) {
|
|
581
|
+
return source.statements.flatMap((statement) => {
|
|
582
|
+
if (ts.isFunctionDeclaration(statement))
|
|
583
|
+
return statement.name ? [{ name: statement.name.text, kind: "function" }] : [];
|
|
584
|
+
if (ts.isClassDeclaration(statement))
|
|
585
|
+
return statement.name ? [{ name: statement.name.text, kind: "class" }] : [];
|
|
586
|
+
if (!ts.isVariableStatement(statement))
|
|
587
|
+
return [];
|
|
588
|
+
return statement.declarationList.declarations.flatMap((declaration) => ts.isIdentifier(declaration.name) && isFunctionValue(declaration.initializer) ? [{ name: declaration.name.text, kind: "function" }] : []);
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// plugins/scanners/javascript/src/evidence.ts
|
|
593
|
+
var { executable, operationFields } = typeScriptOperations(ts2);
|
|
594
|
+
function javaScriptEvidence(file, source) {
|
|
595
|
+
const operations = [];
|
|
596
|
+
const invocations = [];
|
|
597
|
+
const calls = [];
|
|
598
|
+
const owners = new Map;
|
|
599
|
+
const lineOf = (position) => source.getLineAndCharacterOfPosition(position).line + 1;
|
|
600
|
+
let moduleCode;
|
|
601
|
+
function operation(node) {
|
|
602
|
+
const position = node.getStart(source);
|
|
603
|
+
const recorded = { id: `${file}#${position}`, file, position, ...operationFields(node) };
|
|
604
|
+
operations.push(recorded);
|
|
605
|
+
owners.set(node, recorded.id);
|
|
606
|
+
return recorded;
|
|
607
|
+
}
|
|
608
|
+
function moduleOperation() {
|
|
609
|
+
if (moduleCode === undefined) {
|
|
610
|
+
moduleCode = { id: `${file}#module`, file, name: "(anonymous)", position: source.getStart(source) };
|
|
611
|
+
operations.push(moduleCode);
|
|
612
|
+
}
|
|
613
|
+
return moduleCode;
|
|
614
|
+
}
|
|
615
|
+
function invocation(node, owner) {
|
|
616
|
+
const position = node.getStart(source);
|
|
617
|
+
invocations.push({
|
|
618
|
+
source: owner.id,
|
|
619
|
+
targets: [],
|
|
620
|
+
unresolved: true,
|
|
621
|
+
line: lineOf(position),
|
|
622
|
+
position,
|
|
623
|
+
...ts2.isPropertyAccessExpression(node.expression) ? { member: node.expression.name.text } : {}
|
|
624
|
+
});
|
|
625
|
+
}
|
|
626
|
+
function visit(node, owner) {
|
|
627
|
+
const inside = executable(node) ? operation(node) : owner;
|
|
628
|
+
if (ts2.isCallExpression(node)) {
|
|
629
|
+
calls.push(node);
|
|
630
|
+
invocation(node, inside ?? moduleOperation());
|
|
631
|
+
}
|
|
632
|
+
node.forEachChild((child) => visit(child, inside));
|
|
633
|
+
}
|
|
634
|
+
function operationAt(node) {
|
|
635
|
+
for (let current = node;current; current = current.parent) {
|
|
636
|
+
const id = owners.get(current);
|
|
637
|
+
if (id !== undefined)
|
|
638
|
+
return id;
|
|
639
|
+
}
|
|
640
|
+
return moduleOperation().id;
|
|
641
|
+
}
|
|
642
|
+
source.forEachChild((child) => visit(child, undefined));
|
|
643
|
+
const symbols = new Map(topLevelDeclarations(source).map((declaration) => [`${file}#${declaration.name}`, { id: `${file}#${declaration.name}`, ...declaration }]));
|
|
644
|
+
return { symbols: [...symbols.values()], operations, invocations, calls, operationAt };
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// plugins/scanners/javascript/src/http.ts
|
|
648
|
+
import ts4 from "./typescript.cjs";
|
|
649
|
+
|
|
650
|
+
// plugins/scanners/http-checker.ts
|
|
651
|
+
function primitive(ts, type) {
|
|
652
|
+
if (type.isUnion())
|
|
653
|
+
return (type.types ?? []).every((member) => primitive(ts, member));
|
|
654
|
+
const flags = ts.TypeFlags;
|
|
655
|
+
const mask = flags.StringLike | flags.NumberLike | flags.BigIntLike | flags.BooleanLike | flags.EnumLike | flags.ESSymbolLike | flags.VoidLike | flags.Null;
|
|
656
|
+
return (type.flags & mask) !== 0;
|
|
657
|
+
}
|
|
658
|
+
function classicChecker(ts, checker) {
|
|
659
|
+
const symbol = (handle) => handle;
|
|
660
|
+
return {
|
|
661
|
+
symbolsAt: async (nodes) => nodes.map((node) => checker.getSymbolAtLocation(node)),
|
|
662
|
+
shorthandValue: async (property) => checker.getShorthandAssignmentValueSymbol(property),
|
|
663
|
+
aliased: async (handle) => symbol(handle).flags & ts.SymbolFlags.Alias ? checker.getAliasedSymbol(symbol(handle)) : handle,
|
|
664
|
+
module: (handle) => (symbol(handle).flags & ts.SymbolFlags.ValueModule) !== 0,
|
|
665
|
+
declaration: async (handle) => symbol(handle).valueDeclaration ?? symbol(handle).declarations?.[0],
|
|
666
|
+
declarationCount: (handle) => symbol(handle).declarations?.length ?? 0,
|
|
667
|
+
exportsOf: async (handle) => checker.getExportsOfModule(symbol(handle)),
|
|
668
|
+
primitiveAt: async (node) => primitive(ts, checker.getTypeAtLocation(node)),
|
|
669
|
+
same: (left, right) => left === right
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
// plugins/scanners/http-order.ts
|
|
674
|
+
function below(placement, prefix, ...indices) {
|
|
675
|
+
const known = placement.known && indices.every((index) => index !== undefined);
|
|
676
|
+
return {
|
|
677
|
+
...placement,
|
|
678
|
+
prefix: `${placement.prefix}/${prefix}`,
|
|
679
|
+
rank: known ? [...placement.rank, ...indices] : placement.rank,
|
|
680
|
+
known
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
function compare2(left, right) {
|
|
684
|
+
for (let index = 0;index < Math.min(left.length, right.length); index++) {
|
|
685
|
+
if (left[index] !== right[index])
|
|
686
|
+
return left[index] - right[index];
|
|
687
|
+
}
|
|
688
|
+
return 0;
|
|
689
|
+
}
|
|
690
|
+
function begins(rank, start) {
|
|
691
|
+
return start.every((index, at) => rank[at] === index);
|
|
692
|
+
}
|
|
693
|
+
function cut(rank, ranks) {
|
|
694
|
+
return ranks.reduce((shortest, other) => other.length < shortest.length && begins(rank, other) ? other : shortest, rank);
|
|
695
|
+
}
|
|
696
|
+
function withOrder(placed) {
|
|
697
|
+
const ranks = new Map;
|
|
698
|
+
for (const { placement } of placed) {
|
|
699
|
+
if (!placement.ordered)
|
|
700
|
+
continue;
|
|
701
|
+
const list = ranks.get(placement.application) ?? [];
|
|
702
|
+
list.push(placement.rank);
|
|
703
|
+
ranks.set(placement.application, list);
|
|
704
|
+
}
|
|
705
|
+
const positions = new Map([...ranks].map(([application, list]) => {
|
|
706
|
+
const distinct = new Map(list.map((rank) => cut(rank, list)).map((rank) => [rank.join("."), rank]));
|
|
707
|
+
return [application, [...distinct.values()].sort(compare2).map((rank) => rank.join("."))];
|
|
708
|
+
}));
|
|
709
|
+
return placed.map(({ endpoint, placement }) => {
|
|
710
|
+
const list = ranks.get(placement.application);
|
|
711
|
+
if (!placement.ordered || list === undefined)
|
|
712
|
+
return endpoint;
|
|
713
|
+
const position = positions.get(placement.application).indexOf(cut(placement.rank, list).join("."));
|
|
714
|
+
return { ...endpoint, order: { application: placement.application, position } };
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
// plugins/scanners/http-url.ts
|
|
719
|
+
var pathText2 = /^[A-Za-z0-9\-._~!$&'()*+,;=:@%]+$/;
|
|
720
|
+
var absolute = /^([A-Za-z][A-Za-z\d+.-]*:)?\/\//;
|
|
721
|
+
var HOLE = "\x00";
|
|
722
|
+
var computedPart = { kind: "hole", configured: false };
|
|
723
|
+
var configuredPart = { kind: "hole", configured: true };
|
|
724
|
+
var methodToken2 = /^[A-Z][A-Z-]*$/;
|
|
725
|
+
function methodText(text) {
|
|
726
|
+
const method = text?.toUpperCase();
|
|
727
|
+
return method !== undefined && methodToken2.test(method) ? method : undefined;
|
|
728
|
+
}
|
|
729
|
+
function requestSegment2(part) {
|
|
730
|
+
if (part === HOLE)
|
|
731
|
+
return { kind: "dynamic" };
|
|
732
|
+
if (part.includes(HOLE) || !pathText2.test(part))
|
|
733
|
+
return { kind: "unknown" };
|
|
734
|
+
return { kind: "literal", value: part };
|
|
735
|
+
}
|
|
736
|
+
function segmentsOf(parts) {
|
|
737
|
+
const url = parts.map((part) => part.kind === "text" ? part.text : HOLE).join("");
|
|
738
|
+
const end = Math.min(...["?", "#"].map((mark) => url.includes(mark) ? url.indexOf(mark) : url.length));
|
|
739
|
+
return url.slice(0, end).split("/").filter((part) => part !== "").map(requestSegment2);
|
|
740
|
+
}
|
|
741
|
+
function afterAuthority(text) {
|
|
742
|
+
const slash = text.indexOf("/", text.indexOf("//") + 2);
|
|
743
|
+
return slash < 0 ? "" : text.slice(slash);
|
|
744
|
+
}
|
|
745
|
+
function joinedText(input) {
|
|
746
|
+
const parts = [];
|
|
747
|
+
for (const part of input) {
|
|
748
|
+
const last = parts.at(-1);
|
|
749
|
+
if (part.kind === "text" && last?.kind === "text")
|
|
750
|
+
parts[parts.length - 1] = { kind: "text", text: last.text + part.text };
|
|
751
|
+
else if (part.kind === "hole" || part.text !== "")
|
|
752
|
+
parts.push(part);
|
|
753
|
+
}
|
|
754
|
+
return parts;
|
|
755
|
+
}
|
|
756
|
+
function requestUrl(input) {
|
|
757
|
+
const parts = joinedText(input);
|
|
758
|
+
const [first, ...rest] = parts;
|
|
759
|
+
if (first === undefined)
|
|
760
|
+
return { path: [{ kind: "unknown" }] };
|
|
761
|
+
if (first.kind === "hole") {
|
|
762
|
+
const path = segmentsOf(rest);
|
|
763
|
+
const next = rest[0];
|
|
764
|
+
const separated = next === undefined || next.kind === "text" && next.text.startsWith("/");
|
|
765
|
+
return first.configured && separated ? { configured: true, path } : { path: [{ kind: "unknown" }, ...path] };
|
|
766
|
+
}
|
|
767
|
+
if (absolute.test(first.text)) {
|
|
768
|
+
return { path: [{ kind: "unknown" }, ...segmentsOf([{ kind: "text", text: afterAuthority(first.text) }, ...rest])] };
|
|
769
|
+
}
|
|
770
|
+
if (!first.text.startsWith("/"))
|
|
771
|
+
return { path: [{ kind: "unknown" }, ...segmentsOf(parts)] };
|
|
772
|
+
return { path: segmentsOf(parts) };
|
|
773
|
+
}
|
|
774
|
+
function joinBase(base, url) {
|
|
775
|
+
const stated = joinedText(base);
|
|
776
|
+
const path = joinedText(url);
|
|
777
|
+
const [first] = path;
|
|
778
|
+
if (stated.length === 0)
|
|
779
|
+
return path;
|
|
780
|
+
if (first === undefined)
|
|
781
|
+
return stated;
|
|
782
|
+
if (first.kind === "hole")
|
|
783
|
+
return [computedPart, ...path.slice(1)];
|
|
784
|
+
return absolute.test(first.text) ? path : [...stated, { kind: "text", text: "/" }, ...path];
|
|
785
|
+
}
|
|
786
|
+
function withBase(base, url) {
|
|
787
|
+
const stated = joinedText(base);
|
|
788
|
+
const path = joinedText(url);
|
|
789
|
+
const [only] = stated;
|
|
790
|
+
const [first, ...rest] = path;
|
|
791
|
+
if (stated.length !== 1 || only?.kind !== "text" || first?.kind !== "text" || absolute.test(first.text)) {
|
|
792
|
+
return joinBase(stated, path);
|
|
793
|
+
}
|
|
794
|
+
const prefix = only.text.replace(/\/$/, "");
|
|
795
|
+
if (first.text.startsWith(prefix)) {
|
|
796
|
+
const next = first.text[prefix.length];
|
|
797
|
+
if (next === undefined)
|
|
798
|
+
return rest.length === 0 ? path : [computedPart, ...rest];
|
|
799
|
+
return next === "/" || next === "?" ? path : joinBase(stated, path);
|
|
800
|
+
}
|
|
801
|
+
return prefix.startsWith(first.text) && rest.length > 0 ? [computedPart, ...rest] : joinBase(stated, path);
|
|
802
|
+
}
|
|
803
|
+
|
|
804
|
+
// plugins/scanners/http-paths.ts
|
|
805
|
+
var PLACEHOLDER = /^:([A-Za-z_]\w*)(?:\((.*)\)|\{(.*)\})?(\?)?$/;
|
|
806
|
+
var FIRST_PLACEHOLDER = /:([A-Za-z_]\w*)/;
|
|
807
|
+
var WILDCARD = /^\*([A-Za-z_]\w*)?$/;
|
|
808
|
+
var OPTIONAL_GROUP = /\{\/:([A-Za-z_]\w*)\}$/;
|
|
809
|
+
var SYNTAX = /[:*(){}?+]/;
|
|
810
|
+
var ONE_SEGMENT = /^(?:\^|\$|\\[dw]|[A-Za-z0-9_-]|\[(?:\\[dw]|[A-Za-z0-9_]-[A-Za-z0-9_]|[A-Za-z0-9_-])+\]|[+*?|]|\{\d+(?:,\d*)?\}|\(\?:|[()])+$/;
|
|
811
|
+
var REMAINDER = new Set([".*", ".+"]);
|
|
812
|
+
function remainder(name = "*") {
|
|
813
|
+
return { kind: "catch-all", name, optional: true, constrained: true };
|
|
814
|
+
}
|
|
815
|
+
function placeholder(match, last) {
|
|
816
|
+
const [, name, round, curly, optional] = match;
|
|
817
|
+
const pattern = round ?? curly;
|
|
818
|
+
if (pattern === undefined)
|
|
819
|
+
return { kind: "parameter", name, ...optional ? { optional: true } : {} };
|
|
820
|
+
if (REMAINDER.has(pattern))
|
|
821
|
+
return last ? { kind: "catch-all", name } : undefined;
|
|
822
|
+
if (!ONE_SEGMENT.test(pattern))
|
|
823
|
+
return;
|
|
824
|
+
return { kind: "parameter", name, ...optional ? { optional: true } : {}, constrained: true };
|
|
825
|
+
}
|
|
826
|
+
function segment(part, last, bareWildcard) {
|
|
827
|
+
if (!SYNTAX.test(part))
|
|
828
|
+
return pathText2.test(part) ? { kind: "literal", value: part } : undefined;
|
|
829
|
+
const wildcard = WILDCARD.exec(part);
|
|
830
|
+
if (wildcard || part === "(.*)") {
|
|
831
|
+
const optional = bareWildcard && wildcard !== null ? { optional: true } : {};
|
|
832
|
+
return last ? { kind: "catch-all", name: wildcard?.[1] ?? "*", ...optional } : undefined;
|
|
833
|
+
}
|
|
834
|
+
const alone = PLACEHOLDER.exec(part);
|
|
835
|
+
if (alone)
|
|
836
|
+
return placeholder(alone, last);
|
|
837
|
+
const first = FIRST_PLACEHOLDER.exec(part);
|
|
838
|
+
if (first === null || /[*(){}?+]/.test(part) || !pathText2.test(part))
|
|
839
|
+
return;
|
|
840
|
+
return { kind: "parameter", name: first[1], constrained: true };
|
|
841
|
+
}
|
|
842
|
+
function endpointPath(pattern, bareWildcard = false) {
|
|
843
|
+
const group = OPTIONAL_GROUP.exec(pattern);
|
|
844
|
+
if (group !== null) {
|
|
845
|
+
const path = endpointPath(pattern.slice(0, group.index), bareWildcard);
|
|
846
|
+
return path.at(-1)?.kind === "catch-all" ? path : [...path, { kind: "parameter", name: group[1], optional: true }];
|
|
847
|
+
}
|
|
848
|
+
const pieces = pattern.split("/").filter((part) => part !== "");
|
|
849
|
+
const path = [];
|
|
850
|
+
for (const [index, part] of pieces.entries()) {
|
|
851
|
+
const stated = segment(part, index === pieces.length - 1, bareWildcard);
|
|
852
|
+
if (stated === undefined)
|
|
853
|
+
return [...path, remainder(FIRST_PLACEHOLDER.exec(part)?.[1])];
|
|
854
|
+
path.push(stated);
|
|
855
|
+
}
|
|
856
|
+
return path;
|
|
857
|
+
}
|
|
858
|
+
function blockedPath(prefix) {
|
|
859
|
+
const path = endpointPath(prefix);
|
|
860
|
+
return [...path.at(-1)?.kind === "catch-all" ? path.slice(0, -1) : path, remainder()];
|
|
861
|
+
}
|
|
862
|
+
function readablePrefix(parts) {
|
|
863
|
+
const [first, second] = parts;
|
|
864
|
+
if (first?.kind !== "text")
|
|
865
|
+
return "";
|
|
866
|
+
if (second === undefined)
|
|
867
|
+
return first.text;
|
|
868
|
+
return first.text.slice(0, first.text.lastIndexOf("/") + 1);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// plugins/scanners/http-syntax.ts
|
|
872
|
+
function wrapper(ts, node) {
|
|
873
|
+
return ts.isParenthesizedExpression(node) || ts.isAsExpression(node) || ts.isSatisfiesExpression(node) || ts.isNonNullExpression(node) || ts.isTypeAssertionExpression(node);
|
|
874
|
+
}
|
|
875
|
+
function unwrapped(ts, node) {
|
|
876
|
+
let current = node;
|
|
877
|
+
while (wrapper(ts, current))
|
|
878
|
+
current = current.expression;
|
|
879
|
+
return current;
|
|
880
|
+
}
|
|
881
|
+
function propertyKey(ts, property) {
|
|
882
|
+
const name = property.name;
|
|
883
|
+
if (name === undefined)
|
|
884
|
+
return;
|
|
885
|
+
return ts.isIdentifier(name) || ts.isStringLiteral(name) || ts.isNumericLiteral(name) ? name.text : undefined;
|
|
886
|
+
}
|
|
887
|
+
function ownProperty(ts, object, name) {
|
|
888
|
+
let found = "absent";
|
|
889
|
+
for (const property of object.properties) {
|
|
890
|
+
const key = propertyKey(ts, property);
|
|
891
|
+
if (key === undefined || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property))
|
|
892
|
+
return "unknown";
|
|
893
|
+
if (key === name)
|
|
894
|
+
found = ts.isPropertyAssignment(property) ? { node: property.initializer } : "unknown";
|
|
895
|
+
}
|
|
896
|
+
return found;
|
|
897
|
+
}
|
|
898
|
+
function holderOf(ts, node) {
|
|
899
|
+
let current = node;
|
|
900
|
+
while (wrapper(ts, current.parent))
|
|
901
|
+
current = current.parent;
|
|
902
|
+
return ts.isVariableDeclaration(current.parent) ? current.parent : undefined;
|
|
903
|
+
}
|
|
904
|
+
function urlText(ts, node) {
|
|
905
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isTemplateExpression(node))
|
|
906
|
+
return true;
|
|
907
|
+
return ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken;
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
// plugins/scanners/http-uses.ts
|
|
911
|
+
function patternPart(ts, parent, target) {
|
|
912
|
+
if (ts.isPropertyAssignment(parent))
|
|
913
|
+
return parent.initializer === target;
|
|
914
|
+
return ts.isArrayLiteralExpression(parent) || ts.isObjectLiteralExpression(parent) || ts.isSpreadElement(parent) || ts.isSpreadAssignment(parent) || ts.isShorthandPropertyAssignment(parent) || wrapper(ts, parent);
|
|
915
|
+
}
|
|
916
|
+
function assignment(ts, parent, direct) {
|
|
917
|
+
const operator = parent.operatorToken.kind;
|
|
918
|
+
if (operator < ts.SyntaxKind.FirstAssignment || operator > ts.SyntaxKind.LastAssignment)
|
|
919
|
+
return;
|
|
920
|
+
return operator === ts.SyntaxKind.EqualsToken && direct ? { assigned: parent.right } : {};
|
|
921
|
+
}
|
|
922
|
+
function counted(ts, parent) {
|
|
923
|
+
return (ts.isPrefixUnaryExpression(parent) || ts.isPostfixUnaryExpression(parent)) && (parent.operator === ts.SyntaxKind.PlusPlusToken || parent.operator === ts.SyntaxKind.MinusMinusToken);
|
|
924
|
+
}
|
|
925
|
+
function writeOf(ts, node) {
|
|
926
|
+
let target = node;
|
|
927
|
+
while (patternPart(ts, target.parent, target))
|
|
928
|
+
target = target.parent;
|
|
929
|
+
const parent = target.parent;
|
|
930
|
+
const direct = target === node || wrapper(ts, target);
|
|
931
|
+
if (ts.isBinaryExpression(parent) && parent.left === target)
|
|
932
|
+
return assignment(ts, parent, direct);
|
|
933
|
+
if ((ts.isForInStatement(parent) || ts.isForOfStatement(parent)) && parent.initializer === target)
|
|
934
|
+
return {};
|
|
935
|
+
return direct && (counted(ts, parent) || ts.isDeleteExpression(parent)) ? {} : undefined;
|
|
936
|
+
}
|
|
937
|
+
function literalKey(ts, node) {
|
|
938
|
+
return ts.isStringLiteral(node) || ts.isNumericLiteral(node) ? node.text : undefined;
|
|
939
|
+
}
|
|
940
|
+
function calledThrough(ts, node) {
|
|
941
|
+
const parent = node.parent;
|
|
942
|
+
if (ts.isCallExpression(parent) || ts.isNewExpression(parent))
|
|
943
|
+
return parent.expression === node;
|
|
944
|
+
return ts.isTaggedTemplateExpression(parent) && parent.tag === node;
|
|
945
|
+
}
|
|
946
|
+
function accessKey(ts, parent, object) {
|
|
947
|
+
if (ts.isPropertyAccessExpression(parent)) {
|
|
948
|
+
return parent.expression === object && ts.isIdentifier(parent.name) ? parent.name.text : undefined;
|
|
949
|
+
}
|
|
950
|
+
if (!ts.isElementAccessExpression(parent) || parent.expression !== object)
|
|
951
|
+
return;
|
|
952
|
+
return literalKey(ts, parent.argumentExpression);
|
|
953
|
+
}
|
|
954
|
+
function useOf(ts, reference) {
|
|
955
|
+
let chain = reference;
|
|
956
|
+
let receiver;
|
|
957
|
+
const path = [];
|
|
958
|
+
for (;; ) {
|
|
959
|
+
const parent = chain.parent;
|
|
960
|
+
if (wrapper(ts, parent)) {
|
|
961
|
+
chain = parent;
|
|
962
|
+
continue;
|
|
963
|
+
}
|
|
964
|
+
const key = accessKey(ts, parent, chain);
|
|
965
|
+
if (key === undefined)
|
|
966
|
+
break;
|
|
967
|
+
receiver = chain;
|
|
968
|
+
path.push(key);
|
|
969
|
+
chain = parent;
|
|
970
|
+
}
|
|
971
|
+
const write = writeOf(ts, chain);
|
|
972
|
+
if (write !== undefined)
|
|
973
|
+
return { kind: "write", path, reference, value: chain, ...write };
|
|
974
|
+
if (receiver !== undefined && calledThrough(ts, chain)) {
|
|
975
|
+
return { kind: path.length > 1 ? "read" : "escape", path: path.slice(0, -1), reference, value: receiver };
|
|
976
|
+
}
|
|
977
|
+
return { kind: path.length > 0 ? "read" : "escape", path, reference, value: chain };
|
|
978
|
+
}
|
|
979
|
+
function importedName(ts, node) {
|
|
980
|
+
const parent = node.parent;
|
|
981
|
+
if (ts.isImportClause(parent))
|
|
982
|
+
return parent.name === node;
|
|
983
|
+
return (ts.isImportSpecifier(parent) || ts.isNamespaceImport(parent) || ts.isImportEqualsDeclaration(parent)) && parent.name === node;
|
|
984
|
+
}
|
|
985
|
+
function typePosition(ts, node) {
|
|
986
|
+
let current = node;
|
|
987
|
+
while (ts.isQualifiedName(current.parent))
|
|
988
|
+
current = current.parent;
|
|
989
|
+
return ts.isTypeQueryNode(current.parent);
|
|
990
|
+
}
|
|
991
|
+
function exportStatement(ts, node) {
|
|
992
|
+
const parent = node.parent;
|
|
993
|
+
return ts.isExportSpecifier(parent) || ts.isExportAssignment(parent) && parent.expression === node;
|
|
994
|
+
}
|
|
995
|
+
function exported(ts, declaration) {
|
|
996
|
+
const statement = declaration.parent.parent;
|
|
997
|
+
return statement?.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
|
|
998
|
+
}
|
|
999
|
+
function topLevel(ts, declaration) {
|
|
1000
|
+
const statement = declaration.parent?.parent;
|
|
1001
|
+
return statement?.parent !== undefined && ts.isSourceFile(statement.parent);
|
|
1002
|
+
}
|
|
1003
|
+
function scriptFile(ts, source, names) {
|
|
1004
|
+
if (ts.isExternalModule(source) || /\.[cm]js$/.test(source.fileName))
|
|
1005
|
+
return false;
|
|
1006
|
+
const required = (names.get("require") ?? []).some((node) => ts.isCallExpression(node.parent) && node.parent.expression === node);
|
|
1007
|
+
const exporting = ["module", "exports"].some((name) => (names.get(name) ?? []).some((node) => ts.isPropertyAccessExpression(node.parent) && node.parent.expression === node));
|
|
1008
|
+
return !required && !exporting;
|
|
1009
|
+
}
|
|
1010
|
+
function dynamicBinding(ts, call) {
|
|
1011
|
+
let holder = call;
|
|
1012
|
+
let awaited = false;
|
|
1013
|
+
while (ts.isAwaitExpression(holder.parent) || ts.isParenthesizedExpression(holder.parent)) {
|
|
1014
|
+
awaited ||= ts.isAwaitExpression(holder.parent);
|
|
1015
|
+
holder = holder.parent;
|
|
1016
|
+
}
|
|
1017
|
+
const parent = holder.parent;
|
|
1018
|
+
if (ts.isVariableDeclaration(parent))
|
|
1019
|
+
return awaited && ts.isIdentifier(parent.name) ? parent.name : undefined;
|
|
1020
|
+
const then = ts.isPropertyAccessExpression(parent) && parent.expression === holder && !awaited ? parent.parent : undefined;
|
|
1021
|
+
const callback = then !== undefined && ts.isCallExpression(then) ? then.arguments[0] : undefined;
|
|
1022
|
+
if (callback === undefined || !ts.isArrowFunction(callback) && !ts.isFunctionExpression(callback))
|
|
1023
|
+
return;
|
|
1024
|
+
const [first] = callback.parameters;
|
|
1025
|
+
return first !== undefined && ts.isIdentifier(first.name) ? first.name : undefined;
|
|
1026
|
+
}
|
|
1027
|
+
function requiredModule(ts, node) {
|
|
1028
|
+
if (node !== undefined && ts.isExternalModuleReference(node))
|
|
1029
|
+
return ts.isStringLiteral(node.expression) ? node.expression.text : undefined;
|
|
1030
|
+
if (node === undefined || !ts.isCallExpression(node) || !ts.isIdentifier(node.expression) || node.expression.text !== "require")
|
|
1031
|
+
return;
|
|
1032
|
+
const [specifier] = node.arguments;
|
|
1033
|
+
return specifier !== undefined && ts.isStringLiteral(specifier) ? specifier.text : undefined;
|
|
1034
|
+
}
|
|
1035
|
+
function importModule(ts, statement, name) {
|
|
1036
|
+
return ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier) ? { module: statement.moduleSpecifier.text, name } : undefined;
|
|
1037
|
+
}
|
|
1038
|
+
function requiredValue(ts, initializer) {
|
|
1039
|
+
const value = initializer === undefined ? undefined : unwrapped(ts, initializer);
|
|
1040
|
+
const direct = requiredModule(ts, value);
|
|
1041
|
+
if (direct !== undefined)
|
|
1042
|
+
return { module: direct, name: "default" };
|
|
1043
|
+
if (value === undefined || !ts.isPropertyAccessExpression(value) || !ts.isIdentifier(value.name))
|
|
1044
|
+
return;
|
|
1045
|
+
const module = requiredModule(ts, unwrapped(ts, value.expression));
|
|
1046
|
+
return module === undefined ? undefined : { module, name: value.name.text };
|
|
1047
|
+
}
|
|
1048
|
+
function boundExport(ts, declaration) {
|
|
1049
|
+
if (ts.isImportClause(declaration))
|
|
1050
|
+
return importModule(ts, declaration.parent, "default");
|
|
1051
|
+
if (ts.isNamespaceImport(declaration))
|
|
1052
|
+
return importModule(ts, declaration.parent.parent, "*");
|
|
1053
|
+
if (ts.isImportSpecifier(declaration)) {
|
|
1054
|
+
return importModule(ts, declaration.parent.parent.parent, (declaration.propertyName ?? declaration.name).text);
|
|
1055
|
+
}
|
|
1056
|
+
if (ts.isImportEqualsDeclaration(declaration))
|
|
1057
|
+
return requiredValue(ts, declaration.moduleReference);
|
|
1058
|
+
if (ts.isVariableDeclaration(declaration))
|
|
1059
|
+
return requiredValue(ts, declaration.initializer);
|
|
1060
|
+
if (!ts.isBindingElement(declaration))
|
|
1061
|
+
return;
|
|
1062
|
+
const variable = declaration.parent.parent;
|
|
1063
|
+
const module = ts.isVariableDeclaration(variable) ? requiredModule(ts, variable.initializer) : undefined;
|
|
1064
|
+
const key = declaration.propertyName ?? declaration.name;
|
|
1065
|
+
return module !== undefined && key !== undefined && ts.isIdentifier(key) ? { module, name: key.text } : undefined;
|
|
1066
|
+
}
|
|
1067
|
+
function defaultImport(ts, name) {
|
|
1068
|
+
const declaration = name.parent;
|
|
1069
|
+
const origin = declaration.name === name ? boundExport(ts, declaration) : undefined;
|
|
1070
|
+
return origin?.name === "default" ? { declaration, module: origin.module } : undefined;
|
|
1071
|
+
}
|
|
1072
|
+
function indexName(ts, index, node) {
|
|
1073
|
+
const same = index.names.get(node.text);
|
|
1074
|
+
if (same === undefined)
|
|
1075
|
+
index.names.set(node.text, [node]);
|
|
1076
|
+
else
|
|
1077
|
+
same.push(node);
|
|
1078
|
+
if (importedName(ts, node))
|
|
1079
|
+
index.imported.add(node.text);
|
|
1080
|
+
const imported = defaultImport(ts, node);
|
|
1081
|
+
if (imported !== undefined)
|
|
1082
|
+
index.defaults.set(imported.module, [...index.defaults.get(imported.module) ?? [], imported.declaration]);
|
|
1083
|
+
}
|
|
1084
|
+
function indexSources(ts, sources) {
|
|
1085
|
+
const index = { names: new Map, imported: new Set, dynamic: [], defaults: new Map };
|
|
1086
|
+
const visit = (node) => {
|
|
1087
|
+
if (ts.isIdentifier(node))
|
|
1088
|
+
indexName(ts, index, node);
|
|
1089
|
+
if (ts.isCallExpression(node) && node.expression.kind === ts.SyntaxKind.ImportKeyword) {
|
|
1090
|
+
const binding = dynamicBinding(ts, node);
|
|
1091
|
+
index.dynamic.push({ call: node, ...binding === undefined ? {} : { binding } });
|
|
1092
|
+
}
|
|
1093
|
+
ts.forEachChild(node, visit);
|
|
1094
|
+
};
|
|
1095
|
+
for (const source of sources)
|
|
1096
|
+
visit(source);
|
|
1097
|
+
return index;
|
|
1098
|
+
}
|
|
1099
|
+
function startsWith(path, prefix) {
|
|
1100
|
+
return prefix.length <= path.length && prefix.every((name, index) => path[index] === name);
|
|
1101
|
+
}
|
|
1102
|
+
function useAt(ts, node) {
|
|
1103
|
+
const parent = node.parent;
|
|
1104
|
+
if (ts.isVariableDeclaration(parent) && parent.name === node)
|
|
1105
|
+
return { kind: "write", path: [], reference: node, value: node };
|
|
1106
|
+
if (exportStatement(ts, node))
|
|
1107
|
+
return { kind: "export", path: [], reference: node, value: node };
|
|
1108
|
+
const accessed = ts.isPropertyAccessExpression(parent) && parent.name === node;
|
|
1109
|
+
return useOf(ts, accessed ? parent : node);
|
|
1110
|
+
}
|
|
1111
|
+
function objectUse(ts, node) {
|
|
1112
|
+
const parent = node.parent;
|
|
1113
|
+
const named = ts.isPropertyAccessExpression(parent) && parent.expression === node && ts.isIdentifier(parent.name);
|
|
1114
|
+
const declaring = (ts.isVariableDeclaration(parent) || ts.isParameter(parent)) && parent.name === node;
|
|
1115
|
+
return !named && !declaring && !importedName(ts, node) && !typePosition(ts, node);
|
|
1116
|
+
}
|
|
1117
|
+
function settingEffect(use, path, keys) {
|
|
1118
|
+
if (use.path.length === 0 || use.kind === "export")
|
|
1119
|
+
return { kind: "none" };
|
|
1120
|
+
if (startsWith(path, use.path))
|
|
1121
|
+
return { kind: "changes" };
|
|
1122
|
+
const key = startsWith(use.path, path) ? use.path[path.length] : undefined;
|
|
1123
|
+
if (key === undefined || !keys.includes(key))
|
|
1124
|
+
return { kind: "none" };
|
|
1125
|
+
if (use.assigned !== undefined && use.path.length === path.length + 1)
|
|
1126
|
+
return { kind: "assigns", key, value: use.assigned };
|
|
1127
|
+
return { kind: "changes" };
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// plugins/scanners/http-bindings.ts
|
|
1131
|
+
function bindingUses(ts, checker, sources, options) {
|
|
1132
|
+
let index;
|
|
1133
|
+
const indexed = () => {
|
|
1134
|
+
index ??= indexSources(ts, sources);
|
|
1135
|
+
return index;
|
|
1136
|
+
};
|
|
1137
|
+
const uses = new Map;
|
|
1138
|
+
const symbols = new Map;
|
|
1139
|
+
const shorthand = (node) => ts.isShorthandPropertyAssignment(node.parent) && node.parent.name === node;
|
|
1140
|
+
const resolveAll = (names) => {
|
|
1141
|
+
const pending = names.filter((node) => !symbols.has(node) && !shorthand(node));
|
|
1142
|
+
const batch = pending.length === 0 ? Promise.resolve([]) : checker.symbolsAt(pending);
|
|
1143
|
+
for (const [position, node] of pending.entries())
|
|
1144
|
+
symbols.set(node, batch.then((found) => found[position]));
|
|
1145
|
+
};
|
|
1146
|
+
const symbolOf = (node) => {
|
|
1147
|
+
let found = symbols.get(node);
|
|
1148
|
+
if (found === undefined) {
|
|
1149
|
+
found = shorthand(node) ? checker.shorthandValue(node.parent) : checker.symbolsAt([node]).then(([symbol]) => symbol);
|
|
1150
|
+
symbols.set(node, found);
|
|
1151
|
+
}
|
|
1152
|
+
return found;
|
|
1153
|
+
};
|
|
1154
|
+
const refersTo = async (node, declaration) => {
|
|
1155
|
+
const symbol = await symbolOf(node);
|
|
1156
|
+
if (symbol === undefined || node.parent === declaration)
|
|
1157
|
+
return false;
|
|
1158
|
+
if (await checker.declaration(symbol) === declaration)
|
|
1159
|
+
return true;
|
|
1160
|
+
if (importedName(ts, node))
|
|
1161
|
+
return false;
|
|
1162
|
+
return await checker.declaration(await checker.aliased(symbol)) === declaration;
|
|
1163
|
+
};
|
|
1164
|
+
const usesOf = (declaration) => {
|
|
1165
|
+
let found = uses.get(declaration);
|
|
1166
|
+
if (found !== undefined)
|
|
1167
|
+
return found;
|
|
1168
|
+
found = (async () => {
|
|
1169
|
+
const { names, imported } = indexed();
|
|
1170
|
+
const name = declaration.name !== undefined && ts.isIdentifier(declaration.name) ? declaration.name.text : undefined;
|
|
1171
|
+
const texts = name === undefined ? [] : [name, "default", ...imported];
|
|
1172
|
+
const candidates = [...new Set(texts.flatMap((text) => names.get(text) ?? []))].filter((node) => !typePosition(ts, node));
|
|
1173
|
+
resolveAll(candidates);
|
|
1174
|
+
const referring = await Promise.all(candidates.map((node) => refersTo(node, declaration)));
|
|
1175
|
+
return candidates.filter((_, position) => referring[position]).map((node) => useAt(ts, node));
|
|
1176
|
+
})();
|
|
1177
|
+
uses.set(declaration, found);
|
|
1178
|
+
return found;
|
|
1179
|
+
};
|
|
1180
|
+
const dynamicEscapes = async ({ binding }) => {
|
|
1181
|
+
if (binding === undefined)
|
|
1182
|
+
return true;
|
|
1183
|
+
const bound = await symbolOf(binding);
|
|
1184
|
+
const references = indexed().names.get(binding.text) ?? [];
|
|
1185
|
+
resolveAll(references);
|
|
1186
|
+
for (const node of references) {
|
|
1187
|
+
const symbol = await symbolOf(node);
|
|
1188
|
+
if (bound !== undefined && symbol !== undefined && checker.same(symbol, bound) && objectUse(ts, node))
|
|
1189
|
+
return true;
|
|
1190
|
+
}
|
|
1191
|
+
return false;
|
|
1192
|
+
};
|
|
1193
|
+
const moduleOf = async (node) => {
|
|
1194
|
+
const symbol = await symbolOf(node);
|
|
1195
|
+
const module = symbol === undefined ? undefined : await checker.aliased(symbol);
|
|
1196
|
+
return module !== undefined && checker.module(module) ? module : undefined;
|
|
1197
|
+
};
|
|
1198
|
+
const loadedModule = async (entry) => {
|
|
1199
|
+
const [argument] = entry.call.arguments;
|
|
1200
|
+
const [module] = argument === undefined ? [] : await checker.symbolsAt([argument]);
|
|
1201
|
+
return module !== undefined && await dynamicEscapes(entry) ? module : undefined;
|
|
1202
|
+
};
|
|
1203
|
+
const escapedModules = async () => {
|
|
1204
|
+
const { names, imported, dynamic } = indexed();
|
|
1205
|
+
const importedNames = [...imported].flatMap((text) => names.get(text) ?? []).filter((node) => objectUse(ts, node));
|
|
1206
|
+
resolveAll(importedNames);
|
|
1207
|
+
const found = await Promise.all([...importedNames.map(moduleOf), ...dynamic.map(loadedModule)]);
|
|
1208
|
+
const modules = [];
|
|
1209
|
+
for (const module of found) {
|
|
1210
|
+
if (module !== undefined && !modules.some((known) => checker.same(known, module)))
|
|
1211
|
+
modules.push(module);
|
|
1212
|
+
}
|
|
1213
|
+
return modules;
|
|
1214
|
+
};
|
|
1215
|
+
let reachable;
|
|
1216
|
+
const escapedExports = () => {
|
|
1217
|
+
reachable ??= (async () => {
|
|
1218
|
+
const exports = (await Promise.all((await escapedModules()).map((module) => checker.exportsOf(module)))).flat();
|
|
1219
|
+
const declarations = await Promise.all(exports.map(async (symbol) => checker.declaration(await checker.aliased(symbol))));
|
|
1220
|
+
return new Set(declarations.filter((declaration) => declaration !== undefined));
|
|
1221
|
+
})();
|
|
1222
|
+
return reachable;
|
|
1223
|
+
};
|
|
1224
|
+
const script = () => options.fileAlone && sources.some((source) => scriptFile(ts, source, indexed().names));
|
|
1225
|
+
const leaves = async (declaration) => {
|
|
1226
|
+
if (!options.fileAlone)
|
|
1227
|
+
return (await escapedExports()).has(declaration);
|
|
1228
|
+
return exported(ts, declaration) || topLevel(ts, declaration) && script();
|
|
1229
|
+
};
|
|
1230
|
+
const readChanges = async (use) => use.kind !== "read" || !await checker.primitiveAt(use.value);
|
|
1231
|
+
const changes = async (use, path, reference) => {
|
|
1232
|
+
if (use.reference === reference || !startsWith(path, use.path))
|
|
1233
|
+
return false;
|
|
1234
|
+
return use.kind === "export" ? options.fileAlone : readChanges(use);
|
|
1235
|
+
};
|
|
1236
|
+
const settingAt = async (settings, use, path, keys) => {
|
|
1237
|
+
const effect = settingEffect(use, path, keys);
|
|
1238
|
+
if (effect.kind === "assigns")
|
|
1239
|
+
settings.assigned.set(effect.key, [...settings.assigned.get(effect.key) ?? [], effect.value]);
|
|
1240
|
+
else if (effect.kind === "changes")
|
|
1241
|
+
settings.changed ||= await readChanges(use);
|
|
1242
|
+
};
|
|
1243
|
+
return {
|
|
1244
|
+
async unchanged(declaration, path, reference) {
|
|
1245
|
+
if (await leaves(declaration))
|
|
1246
|
+
return false;
|
|
1247
|
+
for (const use of await usesOf(declaration))
|
|
1248
|
+
if (await changes(use, path, reference))
|
|
1249
|
+
return false;
|
|
1250
|
+
return true;
|
|
1251
|
+
},
|
|
1252
|
+
async reassigned(declaration) {
|
|
1253
|
+
if (options.fileAlone && topLevel(ts, declaration) && script())
|
|
1254
|
+
return true;
|
|
1255
|
+
return (await usesOf(declaration)).some((use) => use.kind === "write" && use.path.length === 0);
|
|
1256
|
+
},
|
|
1257
|
+
async settings(declaration, path, keys) {
|
|
1258
|
+
const settings = { assigned: new Map, changed: await leaves(declaration) };
|
|
1259
|
+
for (const use of await usesOf(declaration))
|
|
1260
|
+
await settingAt(settings, use, path, keys);
|
|
1261
|
+
return settings;
|
|
1262
|
+
},
|
|
1263
|
+
defaultImports: (module) => indexed().defaults.get(module) ?? [],
|
|
1264
|
+
references: async (declaration) => (await usesOf(declaration)).map((use) => use.reference),
|
|
1265
|
+
named: (text) => indexed().names.get(text) ?? [],
|
|
1266
|
+
fileAlone: options.fileAlone
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// plugins/scanners/http-values.ts
|
|
1271
|
+
function urlContext(ts, checker, sources, fileAlone = false) {
|
|
1272
|
+
return { ts, checker, bindings: bindingUses(ts, checker, sources, { fileAlone }) };
|
|
1273
|
+
}
|
|
1274
|
+
var MAX_DEPTH = 8;
|
|
1275
|
+
function declarationFile(node) {
|
|
1276
|
+
return /\.d\.[cm]?ts$/.test(node.getSourceFile().fileName);
|
|
1277
|
+
}
|
|
1278
|
+
async function declarationOf(context, node) {
|
|
1279
|
+
const { checker } = context;
|
|
1280
|
+
const [symbol] = await checker.symbolsAt([node]);
|
|
1281
|
+
return symbol === undefined ? undefined : checker.declaration(await checker.aliased(symbol));
|
|
1282
|
+
}
|
|
1283
|
+
var moduleNamesByFile = new WeakMap;
|
|
1284
|
+
function moduleNames(ts, source) {
|
|
1285
|
+
let names = moduleNamesByFile.get(source);
|
|
1286
|
+
if (names !== undefined)
|
|
1287
|
+
return names;
|
|
1288
|
+
const found = new Set;
|
|
1289
|
+
const visit = (node) => {
|
|
1290
|
+
const name = node.name;
|
|
1291
|
+
if (name !== undefined && ts.isIdentifier(name) && boundExport(ts, node) !== undefined)
|
|
1292
|
+
found.add(name.text);
|
|
1293
|
+
ts.forEachChild(node, visit);
|
|
1294
|
+
};
|
|
1295
|
+
visit(source);
|
|
1296
|
+
names = found;
|
|
1297
|
+
moduleNamesByFile.set(source, names);
|
|
1298
|
+
return names;
|
|
1299
|
+
}
|
|
1300
|
+
async function importOrigin(context, node) {
|
|
1301
|
+
const { ts, checker } = context;
|
|
1302
|
+
const required = requiredModule(ts, node);
|
|
1303
|
+
if (required !== undefined)
|
|
1304
|
+
return { module: required, name: "default" };
|
|
1305
|
+
if (!ts.isIdentifier(node) || !moduleNames(ts, node.getSourceFile()).has(node.text))
|
|
1306
|
+
return;
|
|
1307
|
+
const [symbol] = await checker.symbolsAt([node]);
|
|
1308
|
+
const declaration = symbol === undefined ? undefined : await checker.declaration(symbol);
|
|
1309
|
+
const origin = declaration === undefined ? undefined : boundExport(ts, declaration);
|
|
1310
|
+
if (origin === undefined || !ts.isVariableDeclaration(declaration) && !ts.isBindingElement(declaration))
|
|
1311
|
+
return origin;
|
|
1312
|
+
return await unassigned(context, declaration) ? origin : undefined;
|
|
1313
|
+
}
|
|
1314
|
+
async function runtimeGlobal(context, node) {
|
|
1315
|
+
if (await importOrigin(context, node) !== undefined)
|
|
1316
|
+
return false;
|
|
1317
|
+
const declaration = await declarationOf(context, node);
|
|
1318
|
+
return declaration === undefined || declarationFile(declaration);
|
|
1319
|
+
}
|
|
1320
|
+
function ambient(ts, declaration) {
|
|
1321
|
+
const flag = ts.NodeFlags.Ambient ?? 0;
|
|
1322
|
+
return ((declaration.flags ?? 0) & flag) !== 0;
|
|
1323
|
+
}
|
|
1324
|
+
async function unassigned(context, declaration) {
|
|
1325
|
+
const { ts, checker } = context;
|
|
1326
|
+
const list = ts.isVariableDeclaration(declaration) ? declaration.parent : declaration.parent.parent.parent;
|
|
1327
|
+
if (((list.flags ?? 0) & ts.NodeFlags.Const) !== 0)
|
|
1328
|
+
return true;
|
|
1329
|
+
if (declaration.name === undefined)
|
|
1330
|
+
return false;
|
|
1331
|
+
const [symbol] = await checker.symbolsAt([declaration.name]);
|
|
1332
|
+
return symbol !== undefined && checker.declarationCount(symbol) === 1 && !await context.bindings.reassigned(declaration);
|
|
1333
|
+
}
|
|
1334
|
+
async function variableHeld(context, name, path, depth) {
|
|
1335
|
+
const { ts } = context;
|
|
1336
|
+
const declaration = await declarationOf(context, name);
|
|
1337
|
+
if (declaration === undefined || declarationFile(declaration) || ambient(ts, declaration))
|
|
1338
|
+
return "unseen";
|
|
1339
|
+
if (path.length === 0 && ts.isFunctionDeclaration(declaration))
|
|
1340
|
+
return { node: declaration };
|
|
1341
|
+
if (!ts.isVariableDeclaration(declaration) || declaration.initializer === undefined)
|
|
1342
|
+
return "unknown";
|
|
1343
|
+
if (!ts.isIdentifier(declaration.name) || !await unassigned(context, declaration))
|
|
1344
|
+
return "unknown";
|
|
1345
|
+
if (path.length > 0 && !await context.bindings.unchanged(declaration, path, name))
|
|
1346
|
+
return "unknown";
|
|
1347
|
+
return held(context, declaration.initializer, path, depth + 1);
|
|
1348
|
+
}
|
|
1349
|
+
async function extendsClass(context, klass, base, depth = 0) {
|
|
1350
|
+
const { ts } = context;
|
|
1351
|
+
if (!ts.isClassLike(klass) || depth > MAX_DEPTH)
|
|
1352
|
+
return false;
|
|
1353
|
+
const clauses = klass.heritageClauses ?? [];
|
|
1354
|
+
const clause = Array.from(clauses).find((entry) => entry.token === ts.SyntaxKind.ExtendsKeyword);
|
|
1355
|
+
const parent = clause?.types[0];
|
|
1356
|
+
const declaration = parent === undefined ? undefined : await declarationOf(context, parent.expression);
|
|
1357
|
+
return declaration !== undefined && (declaration === base || extendsClass(context, declaration, base, depth + 1));
|
|
1358
|
+
}
|
|
1359
|
+
async function assignmentAt(context, node, same, declaration) {
|
|
1360
|
+
const { ts } = context;
|
|
1361
|
+
const parent = node.parent;
|
|
1362
|
+
if (ts.isPropertyAccessExpression(parent) && parent.name === node) {
|
|
1363
|
+
const use = same ? useAt(ts, node) : undefined;
|
|
1364
|
+
return use?.kind === "write" ? { value: use.path.length === 0 ? use.assigned : undefined } : undefined;
|
|
1365
|
+
}
|
|
1366
|
+
if (!ts.isPropertyDeclaration(parent) || parent.name !== node || declaration === undefined || parent === declaration)
|
|
1367
|
+
return;
|
|
1368
|
+
return await extendsClass(context, parent.parent, declaration.parent) ? { value: parent.initializer } : undefined;
|
|
1369
|
+
}
|
|
1370
|
+
async function fieldAssignments(context, name, declaration) {
|
|
1371
|
+
const { ts, checker } = context;
|
|
1372
|
+
const [symbol] = await checker.symbolsAt([name]);
|
|
1373
|
+
const initialized = declaration !== undefined && (ts.isPropertyDeclaration(declaration) || ts.isPropertyAssignment(declaration)) ? declaration.initializer : undefined;
|
|
1374
|
+
const assignments = initialized === undefined ? [] : [initialized];
|
|
1375
|
+
const others = context.bindings.named(name.text).filter((node) => node !== name);
|
|
1376
|
+
const symbols = await checker.symbolsAt(others);
|
|
1377
|
+
for (const [position, node] of others.entries()) {
|
|
1378
|
+
const found = symbols[position];
|
|
1379
|
+
const same = found !== undefined && symbol !== undefined && checker.same(found, symbol);
|
|
1380
|
+
const assignment = await assignmentAt(context, node, same, declaration);
|
|
1381
|
+
if (assignment !== undefined)
|
|
1382
|
+
assignments.push(assignment.value);
|
|
1383
|
+
}
|
|
1384
|
+
return assignments;
|
|
1385
|
+
}
|
|
1386
|
+
async function fieldHeld(context, access, path, depth) {
|
|
1387
|
+
const { ts, checker } = context;
|
|
1388
|
+
const [symbol] = await checker.symbolsAt([access.name]);
|
|
1389
|
+
const declaration = symbol === undefined ? undefined : await checker.declaration(symbol);
|
|
1390
|
+
const assignments = await fieldAssignments(context, access.name, declaration);
|
|
1391
|
+
const injected = declaration !== undefined && ts.isParameter(declaration);
|
|
1392
|
+
if (assignments.length === 0)
|
|
1393
|
+
return "unseen";
|
|
1394
|
+
const [only] = assignments;
|
|
1395
|
+
if (injected || assignments.length > 1 || only === undefined || path.length > 0 || context.bindings.fileAlone)
|
|
1396
|
+
return "unknown";
|
|
1397
|
+
return held(context, only, path, depth + 1);
|
|
1398
|
+
}
|
|
1399
|
+
async function held(context, node, path, depth) {
|
|
1400
|
+
const { ts } = context;
|
|
1401
|
+
if (depth > MAX_DEPTH)
|
|
1402
|
+
return "unknown";
|
|
1403
|
+
const value = unwrapped(ts, node);
|
|
1404
|
+
const [name, ...rest] = path;
|
|
1405
|
+
if (ts.isObjectLiteralExpression(value) && name !== undefined) {
|
|
1406
|
+
const property = ownProperty(ts, value, name);
|
|
1407
|
+
if (typeof property !== "string")
|
|
1408
|
+
return held(context, property.node, rest, depth + 1);
|
|
1409
|
+
return property === "absent" && rest.length === 0 ? "absent" : "unknown";
|
|
1410
|
+
}
|
|
1411
|
+
if (ts.isPropertyAccessExpression(value) && ts.isIdentifier(value.name)) {
|
|
1412
|
+
if (value.expression.kind === ts.SyntaxKind.ThisKeyword)
|
|
1413
|
+
return fieldHeld(context, value, path, depth);
|
|
1414
|
+
return held(context, value.expression, [value.name.text, ...path], depth + 1);
|
|
1415
|
+
}
|
|
1416
|
+
if (ts.isIdentifier(value))
|
|
1417
|
+
return variableHeld(context, value, path, depth);
|
|
1418
|
+
if (name === undefined)
|
|
1419
|
+
return { node: value };
|
|
1420
|
+
return ts.isMetaProperty(value) ? "unseen" : "unknown";
|
|
1421
|
+
}
|
|
1422
|
+
function heldAt(context, node, ...path) {
|
|
1423
|
+
return held(context, node, path, 0);
|
|
1424
|
+
}
|
|
1425
|
+
async function heldParts(context, value) {
|
|
1426
|
+
if (value === "unseen")
|
|
1427
|
+
return [configuredPart];
|
|
1428
|
+
return typeof value === "string" ? [computedPart] : urlParts(context, value.node);
|
|
1429
|
+
}
|
|
1430
|
+
async function urlParts(context, node, depth = 0) {
|
|
1431
|
+
const { ts } = context;
|
|
1432
|
+
if (depth > MAX_DEPTH)
|
|
1433
|
+
return [computedPart];
|
|
1434
|
+
const value = unwrapped(ts, node);
|
|
1435
|
+
if (ts.isStringLiteral(value) || ts.isNoSubstitutionTemplateLiteral(value))
|
|
1436
|
+
return [{ kind: "text", text: value.text }];
|
|
1437
|
+
if (ts.isTemplateExpression(value)) {
|
|
1438
|
+
const parts = [{ kind: "text", text: value.head.text }];
|
|
1439
|
+
for (const span of value.templateSpans) {
|
|
1440
|
+
parts.push(...await urlParts(context, span.expression, depth + 1), { kind: "text", text: span.literal.text });
|
|
1441
|
+
}
|
|
1442
|
+
return parts;
|
|
1443
|
+
}
|
|
1444
|
+
if (ts.isBinaryExpression(value) && value.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
1445
|
+
return [...await urlParts(context, value.left, depth + 1), ...await urlParts(context, value.right, depth + 1)];
|
|
1446
|
+
}
|
|
1447
|
+
if (!ts.isIdentifier(value) && !ts.isPropertyAccessExpression(value))
|
|
1448
|
+
return [computedPart];
|
|
1449
|
+
const resolved = await held(context, value, [], depth + 1);
|
|
1450
|
+
return typeof resolved === "string" ? heldParts(context, resolved) : urlParts(context, resolved.node, depth + 1);
|
|
1451
|
+
}
|
|
1452
|
+
async function literalText(context, node) {
|
|
1453
|
+
if (node === undefined)
|
|
1454
|
+
return;
|
|
1455
|
+
const [first, ...rest] = await urlParts(context, node);
|
|
1456
|
+
return rest.length === 0 && first?.kind === "text" ? first.text : undefined;
|
|
1457
|
+
}
|
|
1458
|
+
async function methodName(context, node) {
|
|
1459
|
+
return methodText(await literalText(context, node));
|
|
1460
|
+
}
|
|
1461
|
+
async function objectEntries(context, node) {
|
|
1462
|
+
const { ts } = context;
|
|
1463
|
+
const value = node === undefined ? undefined : await heldAt(context, node);
|
|
1464
|
+
if (typeof value !== "object" || !ts.isObjectLiteralExpression(value.node))
|
|
1465
|
+
return;
|
|
1466
|
+
const entries = new Map;
|
|
1467
|
+
for (const property of value.node.properties) {
|
|
1468
|
+
const key = propertyKey(ts, property);
|
|
1469
|
+
if (key === undefined || ts.isGetAccessorDeclaration(property) || ts.isSetAccessorDeclaration(property))
|
|
1470
|
+
return;
|
|
1471
|
+
if (ts.isPropertyAssignment(property))
|
|
1472
|
+
entries.set(key, property.initializer);
|
|
1473
|
+
else
|
|
1474
|
+
entries.delete(key);
|
|
1475
|
+
}
|
|
1476
|
+
return entries;
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
// plugins/scanners/http-routers.ts
|
|
1480
|
+
var routeMembers = new Map([
|
|
1481
|
+
["get", "GET"],
|
|
1482
|
+
["post", "POST"],
|
|
1483
|
+
["put", "PUT"],
|
|
1484
|
+
["patch", "PATCH"],
|
|
1485
|
+
["delete", "DELETE"],
|
|
1486
|
+
["head", "HEAD"],
|
|
1487
|
+
["options", "OPTIONS"],
|
|
1488
|
+
["all", "*"]
|
|
1489
|
+
]);
|
|
1490
|
+
var koaMembers = new Map([["del", "DELETE"], ["redirect", "*"]]);
|
|
1491
|
+
function routeMethod(member, framework) {
|
|
1492
|
+
return routeMembers.get(member) ?? (framework === "koa" ? koaMembers.get(member) : undefined);
|
|
1493
|
+
}
|
|
1494
|
+
var unreadHonoMembers = new Set(["on", "basePath", "mount"]);
|
|
1495
|
+
var expressSettings = new Set(["set", "enable", "disable", "engine"]);
|
|
1496
|
+
var orderedFrameworks = new Set(["express", "hono", "koa"]);
|
|
1497
|
+
var expressApp = { kind: "app", framework: "express" };
|
|
1498
|
+
var fastifyApp = { kind: "app", framework: "fastify" };
|
|
1499
|
+
var koaApp = { kind: "app", framework: "koa" };
|
|
1500
|
+
var koaRouter = { kind: "router", framework: "koa" };
|
|
1501
|
+
var constructors = new Map([
|
|
1502
|
+
["express", { default: expressApp, "*": expressApp, Router: { kind: "router", framework: "express" } }],
|
|
1503
|
+
["fastify", { default: fastifyApp, "*": fastifyApp, fastify: fastifyApp }],
|
|
1504
|
+
["hono", { Hono: { kind: "app", framework: "hono" } }],
|
|
1505
|
+
["koa", { default: koaApp, "*": koaApp }],
|
|
1506
|
+
["@koa/router", { default: koaRouter, "*": koaRouter }],
|
|
1507
|
+
["koa-router", { default: koaRouter, "*": koaRouter }]
|
|
1508
|
+
]);
|
|
1509
|
+
async function routerPrefix(context, initializer) {
|
|
1510
|
+
const [options] = initializer.arguments ?? [];
|
|
1511
|
+
if (options === undefined)
|
|
1512
|
+
return "";
|
|
1513
|
+
const prefix = await heldAt(context, options, "prefix");
|
|
1514
|
+
if (prefix === "absent")
|
|
1515
|
+
return "";
|
|
1516
|
+
return typeof prefix === "object" ? literalText(context, prefix.node) : undefined;
|
|
1517
|
+
}
|
|
1518
|
+
async function built(context, found, initializer, variable) {
|
|
1519
|
+
if (found === undefined || !context.frameworks.has(found.framework))
|
|
1520
|
+
return;
|
|
1521
|
+
return { ...found, variable, prefix: found === koaRouter ? await routerPrefix(context, initializer) : "" };
|
|
1522
|
+
}
|
|
1523
|
+
async function registrarOf(context, initializer, variable) {
|
|
1524
|
+
const { ts } = context;
|
|
1525
|
+
if (!ts.isCallExpression(initializer) && !ts.isNewExpression(initializer))
|
|
1526
|
+
return;
|
|
1527
|
+
const callee = initializer.expression;
|
|
1528
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
1529
|
+
const holder = await importOrigin(context, callee.expression);
|
|
1530
|
+
const router = holder?.module === "express" && callee.name.text === "Router";
|
|
1531
|
+
return built(context, router ? constructors.get("express").Router : undefined, initializer, variable);
|
|
1532
|
+
}
|
|
1533
|
+
const origin = await importOrigin(context, callee);
|
|
1534
|
+
return built(context, origin === undefined ? undefined : constructors.get(origin.module)?.[origin.name], initializer, variable);
|
|
1535
|
+
}
|
|
1536
|
+
function walk(ts, node, visitor) {
|
|
1537
|
+
visitor(node);
|
|
1538
|
+
ts.forEachChild(node, (child) => walk(ts, child, visitor));
|
|
1539
|
+
}
|
|
1540
|
+
async function collectRegistrars(context, sources) {
|
|
1541
|
+
const { ts } = context;
|
|
1542
|
+
const declarations = [];
|
|
1543
|
+
for (const source of sources) {
|
|
1544
|
+
walk(ts, source, (node) => {
|
|
1545
|
+
if (ts.isVariableDeclaration(node) && node.initializer !== undefined && ts.isIdentifier(node.name)) {
|
|
1546
|
+
declarations.push(node);
|
|
1547
|
+
}
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
const registrars = new Map;
|
|
1551
|
+
for (const declaration of declarations) {
|
|
1552
|
+
const registrar = await registrarOf(context, declaration.initializer, declaration.name.text);
|
|
1553
|
+
if (registrar !== undefined && await unassigned(context, declaration))
|
|
1554
|
+
registrars.set(declaration, registrar);
|
|
1555
|
+
}
|
|
1556
|
+
return registrars;
|
|
1557
|
+
}
|
|
1558
|
+
function candidate(member) {
|
|
1559
|
+
return ["hono", "koa", "fastify"].some((framework) => registers(member, framework));
|
|
1560
|
+
}
|
|
1561
|
+
function memberOf(ts, call) {
|
|
1562
|
+
return ts.isPropertyAccessExpression(call.expression) ? call.expression.name.text : undefined;
|
|
1563
|
+
}
|
|
1564
|
+
function registers(member, framework) {
|
|
1565
|
+
if (unreadHonoMembers.has(member))
|
|
1566
|
+
return framework === "hono";
|
|
1567
|
+
if (koaMembers.has(member))
|
|
1568
|
+
return framework === "koa";
|
|
1569
|
+
if (member === "register")
|
|
1570
|
+
return framework === "fastify";
|
|
1571
|
+
if (member === "route")
|
|
1572
|
+
return framework !== "koa";
|
|
1573
|
+
return routeMembers.has(member) || member === "use";
|
|
1574
|
+
}
|
|
1575
|
+
function returnsRegistrar(member, framework) {
|
|
1576
|
+
if (framework === "express" && expressSettings.has(member))
|
|
1577
|
+
return true;
|
|
1578
|
+
if (framework === "koa" && member === "prefix")
|
|
1579
|
+
return true;
|
|
1580
|
+
const builder = member === "route" && framework === "express" || member === "basePath";
|
|
1581
|
+
return registers(member, framework) && !builder;
|
|
1582
|
+
}
|
|
1583
|
+
async function receiverDeclaration(context, call, registrars) {
|
|
1584
|
+
const { ts } = context;
|
|
1585
|
+
const receiver = ts.isPropertyAccessExpression(call.expression) ? call.expression.expression : undefined;
|
|
1586
|
+
if (receiver !== undefined && ts.isCallExpression(receiver)) {
|
|
1587
|
+
const member = memberOf(ts, receiver);
|
|
1588
|
+
const declaration = member === undefined ? undefined : await receiverDeclaration(context, receiver, registrars);
|
|
1589
|
+
if (member === undefined || declaration === undefined)
|
|
1590
|
+
return;
|
|
1591
|
+
return returnsRegistrar(member, registrars.get(declaration).framework) ? declaration : undefined;
|
|
1592
|
+
}
|
|
1593
|
+
if (receiver === undefined || !ts.isIdentifier(receiver))
|
|
1594
|
+
return;
|
|
1595
|
+
const declaration = await declarationOf(context, receiver);
|
|
1596
|
+
return declaration !== undefined && registrars.has(declaration) ? declaration : undefined;
|
|
1597
|
+
}
|
|
1598
|
+
async function collectRegistrations(context, calls, registrars) {
|
|
1599
|
+
const registrations = [];
|
|
1600
|
+
for (const call of calls) {
|
|
1601
|
+
const member = memberOf(context.ts, call);
|
|
1602
|
+
if (member === undefined || !candidate(member))
|
|
1603
|
+
continue;
|
|
1604
|
+
const declaration = await receiverDeclaration(context, call, registrars);
|
|
1605
|
+
if (declaration !== undefined && registers(member, registrars.get(declaration).framework)) {
|
|
1606
|
+
registrations.push({ call, declaration, member });
|
|
1607
|
+
}
|
|
1608
|
+
}
|
|
1609
|
+
return registrations;
|
|
1610
|
+
}
|
|
1611
|
+
async function applyPrefixes(context, calls, registrars) {
|
|
1612
|
+
for (const call of calls) {
|
|
1613
|
+
if (memberOf(context.ts, call) !== "prefix")
|
|
1614
|
+
continue;
|
|
1615
|
+
const declaration = await receiverDeclaration(context, call, registrars);
|
|
1616
|
+
const registrar = declaration === undefined ? undefined : registrars.get(declaration);
|
|
1617
|
+
if (registrar?.framework !== "koa" || registrar.kind !== "router")
|
|
1618
|
+
continue;
|
|
1619
|
+
const stated = await literalText(context, call.arguments[0]);
|
|
1620
|
+
registrars.set(declaration, { ...registrar, prefix: registrar.prefix === "" ? stated : undefined });
|
|
1621
|
+
}
|
|
1622
|
+
}
|
|
1623
|
+
function mounting(registration) {
|
|
1624
|
+
return registration.member === "use" || registration.member === "route" && registration.call.arguments.length > 1;
|
|
1625
|
+
}
|
|
1626
|
+
function atTopLevel(ts, node) {
|
|
1627
|
+
let current = node;
|
|
1628
|
+
while (!ts.isSourceFile(current.parent)) {
|
|
1629
|
+
current = current.parent;
|
|
1630
|
+
if (ts.isFunctionLike(current) || ts.isClassLike(current))
|
|
1631
|
+
return false;
|
|
1632
|
+
}
|
|
1633
|
+
return ts.isExpressionStatement(current) || ts.isVariableStatement(current);
|
|
1634
|
+
}
|
|
1635
|
+
function handedOn(ts, reference, mounts) {
|
|
1636
|
+
const parent = reference.parent;
|
|
1637
|
+
if (ts.isPropertyAccessExpression(parent) && parent.expression === reference)
|
|
1638
|
+
return false;
|
|
1639
|
+
return !mounts.has(parent);
|
|
1640
|
+
}
|
|
1641
|
+
function exportOf(ts, reference) {
|
|
1642
|
+
const parent = reference.parent;
|
|
1643
|
+
if (ts.isExportAssignment(parent) || ts.isExportSpecifier(parent))
|
|
1644
|
+
return "last";
|
|
1645
|
+
const assigned = ts.isBinaryExpression(parent) && parent.right === reference && parent.operatorToken.kind === ts.SyntaxKind.EqualsToken && commonJsExport(ts, parent.left);
|
|
1646
|
+
if (!assigned)
|
|
1647
|
+
return;
|
|
1648
|
+
return atTopLevel(ts, parent) ? "last" : "anywhere";
|
|
1649
|
+
}
|
|
1650
|
+
function exportedDeclaration(ts, declaration) {
|
|
1651
|
+
if (!ts.isVariableDeclaration(declaration))
|
|
1652
|
+
return false;
|
|
1653
|
+
return declaration.parent.parent.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) === true;
|
|
1654
|
+
}
|
|
1655
|
+
var httpModules = new Set(["http", "https", "node:http", "node:https"]);
|
|
1656
|
+
function commonJsExport(ts, node) {
|
|
1657
|
+
if (!ts.isPropertyAccessExpression(node))
|
|
1658
|
+
return false;
|
|
1659
|
+
const root = node.expression;
|
|
1660
|
+
if (!ts.isIdentifier(root))
|
|
1661
|
+
return commonJsExport(ts, root);
|
|
1662
|
+
return root.text === "exports" || root.text === "module" && node.name.text === "exports";
|
|
1663
|
+
}
|
|
1664
|
+
async function serving(context, reference) {
|
|
1665
|
+
const { ts } = context;
|
|
1666
|
+
const parent = reference.parent;
|
|
1667
|
+
if (!ts.isCallExpression(parent) || !parent.arguments.some((argument) => argument === reference))
|
|
1668
|
+
return false;
|
|
1669
|
+
const callee = parent.expression;
|
|
1670
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
1671
|
+
const holder = await importOrigin(context, callee.expression);
|
|
1672
|
+
return callee.name.text === "createServer" && httpModules.has(holder?.module ?? "");
|
|
1673
|
+
}
|
|
1674
|
+
const origin = await importOrigin(context, callee);
|
|
1675
|
+
if (origin === undefined)
|
|
1676
|
+
return false;
|
|
1677
|
+
return origin.name === "serve" || origin.name === "createServer" && httpModules.has(origin.module);
|
|
1678
|
+
}
|
|
1679
|
+
function registeringFiles(declaration, registrations) {
|
|
1680
|
+
const files = new Set([declaration.getSourceFile()]);
|
|
1681
|
+
for (const registration of registrations) {
|
|
1682
|
+
if (registration.declaration === declaration)
|
|
1683
|
+
files.add(registration.call.getSourceFile());
|
|
1684
|
+
}
|
|
1685
|
+
return files;
|
|
1686
|
+
}
|
|
1687
|
+
async function handOffAt(context, reference, declaration, mounts) {
|
|
1688
|
+
const { ts } = context;
|
|
1689
|
+
const exported = exportOf(ts, reference);
|
|
1690
|
+
if (exported !== undefined)
|
|
1691
|
+
return context.exportsHandOff ? { reference, declaration, last: exported === "last" } : undefined;
|
|
1692
|
+
if (!handedOn(ts, reference, mounts) || await serving(context, reference))
|
|
1693
|
+
return;
|
|
1694
|
+
return { reference, declaration, last: false };
|
|
1695
|
+
}
|
|
1696
|
+
async function collectHandOffs(context, registrars, registrations) {
|
|
1697
|
+
const mounts = new Set(registrations.filter(mounting).map(({ call }) => call));
|
|
1698
|
+
const handOffs = [];
|
|
1699
|
+
for (const declaration of registrars.keys()) {
|
|
1700
|
+
if (context.exportsHandOff && exportedDeclaration(context.ts, declaration))
|
|
1701
|
+
handOffs.push({ reference: declaration, declaration, last: true });
|
|
1702
|
+
const files = registeringFiles(declaration, registrations);
|
|
1703
|
+
for (const reference of await context.bindings.references(declaration)) {
|
|
1704
|
+
const found = files.has(reference.getSourceFile()) ? await handOffAt(context, reference, declaration, mounts) : undefined;
|
|
1705
|
+
if (found !== undefined)
|
|
1706
|
+
handOffs.push(found);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
return handOffs;
|
|
1710
|
+
}
|
|
1711
|
+
function entryStart(ts, node) {
|
|
1712
|
+
return ts.isCallExpression(node) && ts.isPropertyAccessExpression(node.expression) ? node.expression.name.getStart() : node.getStart();
|
|
1713
|
+
}
|
|
1714
|
+
function orderIndices(ts, registrations, handOffs) {
|
|
1715
|
+
const entries = [
|
|
1716
|
+
...registrations.map(({ call, declaration }) => ({ node: call, declaration, start: entryStart(ts, call), last: false })),
|
|
1717
|
+
...handOffs.map(({ reference, declaration, last }) => ({
|
|
1718
|
+
node: reference,
|
|
1719
|
+
declaration,
|
|
1720
|
+
start: last ? Number.POSITIVE_INFINITY : reference.getStart(),
|
|
1721
|
+
last
|
|
1722
|
+
}))
|
|
1723
|
+
];
|
|
1724
|
+
const byRegistrar = new Map;
|
|
1725
|
+
for (const entry of entries)
|
|
1726
|
+
byRegistrar.set(entry.declaration, [...byRegistrar.get(entry.declaration) ?? [], entry]);
|
|
1727
|
+
const indices = new Map;
|
|
1728
|
+
for (const list of byRegistrar.values()) {
|
|
1729
|
+
const files = new Set(list.map(({ node }) => node.getSourceFile()));
|
|
1730
|
+
if (files.size !== 1 || !list.every(({ node, last }) => last || atTopLevel(ts, node)))
|
|
1731
|
+
continue;
|
|
1732
|
+
const sorted = [...list].sort((left, right) => left.start - right.start);
|
|
1733
|
+
for (const [index, { node }] of sorted.entries())
|
|
1734
|
+
indices.set(node, index);
|
|
1735
|
+
}
|
|
1736
|
+
return indices;
|
|
1737
|
+
}
|
|
1738
|
+
async function routerRegistrations(context, sources, calls) {
|
|
1739
|
+
const registrars = await collectRegistrars(context, sources);
|
|
1740
|
+
await applyPrefixes(context, calls, registrars);
|
|
1741
|
+
const registrations = await collectRegistrations(context, calls, registrars);
|
|
1742
|
+
const handOffs = await collectHandOffs(context, registrars, registrations);
|
|
1743
|
+
return { registrars, registrations, handOffs, indices: orderIndices(context.ts, registrations, handOffs) };
|
|
1744
|
+
}
|
|
1745
|
+
|
|
1746
|
+
// plugins/scanners/http-routes.ts
|
|
1747
|
+
async function registrarArgument(reading, argument) {
|
|
1748
|
+
const { ts } = reading.context;
|
|
1749
|
+
const named = ts.isCallExpression(argument) && ts.isPropertyAccessExpression(argument.expression) && argument.expression.name.text === "routes" ? argument.expression.expression : argument;
|
|
1750
|
+
if (!ts.isIdentifier(named))
|
|
1751
|
+
return;
|
|
1752
|
+
const declaration = await declarationOf(reading.context, named);
|
|
1753
|
+
return declaration !== undefined && reading.registrars.has(declaration) ? declaration : undefined;
|
|
1754
|
+
}
|
|
1755
|
+
async function foreignRouter(reading, argument) {
|
|
1756
|
+
const { context } = reading;
|
|
1757
|
+
const { ts } = context;
|
|
1758
|
+
if (await registrarArgument(reading, argument) !== undefined)
|
|
1759
|
+
return false;
|
|
1760
|
+
if (ts.isCallExpression(argument) && ts.isPropertyAccessExpression(argument.expression)) {
|
|
1761
|
+
return argument.expression.name.text === "routes";
|
|
1762
|
+
}
|
|
1763
|
+
const origin = await importOrigin(context, argument);
|
|
1764
|
+
if (origin?.module.startsWith(".") !== true)
|
|
1765
|
+
return false;
|
|
1766
|
+
const held = await heldAt(context, argument);
|
|
1767
|
+
return typeof held !== "object" || !ts.isFunctionLike(held.node);
|
|
1768
|
+
}
|
|
1769
|
+
async function middleware(context, argument) {
|
|
1770
|
+
const { ts } = context;
|
|
1771
|
+
const held = await heldAt(context, argument);
|
|
1772
|
+
if (typeof held === "object" && ts.isFunctionLike(held.node))
|
|
1773
|
+
return true;
|
|
1774
|
+
const callee = ts.isCallExpression(argument) ? argument.expression : argument;
|
|
1775
|
+
const holder = ts.isPropertyAccessExpression(callee) ? callee.expression : callee;
|
|
1776
|
+
const origin = await importOrigin(context, holder);
|
|
1777
|
+
return origin !== undefined && !origin.module.startsWith(".");
|
|
1778
|
+
}
|
|
1779
|
+
async function readMount(reading, registration) {
|
|
1780
|
+
const [first, ...rest] = registration.call.arguments;
|
|
1781
|
+
if (first === undefined)
|
|
1782
|
+
return { children: [], prefix: "", pathless: true, foreign: false };
|
|
1783
|
+
const leading = await registrarArgument(reading, first);
|
|
1784
|
+
const pathless = leading !== undefined || await foreignRouter(reading, first) || await middleware(reading.context, first);
|
|
1785
|
+
const children = leading === undefined ? [] : [leading];
|
|
1786
|
+
for (const argument of rest) {
|
|
1787
|
+
const child = await registrarArgument(reading, argument);
|
|
1788
|
+
if (child !== undefined)
|
|
1789
|
+
children.push(child);
|
|
1790
|
+
}
|
|
1791
|
+
const prefix = pathless ? "" : await literalText(reading.context, first);
|
|
1792
|
+
const foreign = children.length === 0 && (await Promise.all(registration.call.arguments.map((argument) => foreignRouter(reading, argument)))).some(Boolean);
|
|
1793
|
+
return { children, pathless, foreign, ...prefix === undefined ? {} : { prefix } };
|
|
1794
|
+
}
|
|
1795
|
+
async function collectMounts(reading) {
|
|
1796
|
+
const mounts = new Map;
|
|
1797
|
+
const calls = new Map;
|
|
1798
|
+
for (const registration of reading.registrations) {
|
|
1799
|
+
if (!mounting(registration))
|
|
1800
|
+
continue;
|
|
1801
|
+
const mount = await readMount(reading, registration);
|
|
1802
|
+
calls.set(registration, mount);
|
|
1803
|
+
for (const [position, child] of mount.children.entries()) {
|
|
1804
|
+
const entry = { registration, position, ...mount.prefix === undefined ? {} : { prefix: mount.prefix } };
|
|
1805
|
+
mounts.set(child, [...mounts.get(child) ?? [], entry]);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
return { mounts, calls };
|
|
1809
|
+
}
|
|
1810
|
+
function withPrefix(placement, registrar) {
|
|
1811
|
+
return registrar.prefix === "" ? placement : { ...placement, prefix: `${placement.prefix}/${registrar.prefix}` };
|
|
1812
|
+
}
|
|
1813
|
+
function underMount(routing, mount, entry) {
|
|
1814
|
+
const { ts } = routing.context;
|
|
1815
|
+
const parent = routing.registrars.get(mount.declaration);
|
|
1816
|
+
const copies = parent.framework === "hono" && mount.member === "route" || parent.framework === "koa" && parent.kind === "router" && mount.member === "use";
|
|
1817
|
+
if (!copies)
|
|
1818
|
+
return "yes";
|
|
1819
|
+
const ordered = mount.call.getSourceFile() === entry.getSourceFile() && atTopLevel(ts, mount.call) && atTopLevel(ts, entry);
|
|
1820
|
+
if (!ordered)
|
|
1821
|
+
return "unknown";
|
|
1822
|
+
return entryStart(ts, entry) < entryStart(ts, mount.call) ? "yes" : "no";
|
|
1823
|
+
}
|
|
1824
|
+
function placements(routing, declaration, entry, seen = new Set) {
|
|
1825
|
+
const registrar = routing.registrars.get(declaration);
|
|
1826
|
+
if (seen.has(declaration) || registrar.prefix === undefined)
|
|
1827
|
+
return [];
|
|
1828
|
+
const entries = routing.mounts.get(declaration) ?? [];
|
|
1829
|
+
if (entries.length === 0) {
|
|
1830
|
+
if (registrar.kind === "router")
|
|
1831
|
+
return [];
|
|
1832
|
+
const application = `${routing.context.file(declaration)}#${registrar.variable}`;
|
|
1833
|
+
const ordered = orderedFrameworks.has(registrar.framework);
|
|
1834
|
+
return [withPrefix({ prefix: "", application, rank: [], known: true, ordered, certain: true }, registrar)];
|
|
1835
|
+
}
|
|
1836
|
+
return entries.flatMap(({ registration, prefix, position }) => {
|
|
1837
|
+
const under = prefix === undefined ? "no" : underMount(routing, registration, entry);
|
|
1838
|
+
if (under === "no")
|
|
1839
|
+
return [];
|
|
1840
|
+
return placements(routing, registration.declaration, registration.call, new Set([...seen, declaration])).map((parent) => {
|
|
1841
|
+
const at = below(parent, prefix, routing.indices.get(registration.call), position);
|
|
1842
|
+
return { ...withPrefix(at, registrar), certain: parent.certain && under === "yes" };
|
|
1843
|
+
});
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
async function methodList(context, node) {
|
|
1847
|
+
if (node === undefined)
|
|
1848
|
+
return [];
|
|
1849
|
+
const values = context.ts.isArrayLiteralExpression(node) ? [...node.elements] : [node];
|
|
1850
|
+
const methods = await Promise.all(values.map(async (value) => methodText(await literalText(context, value))));
|
|
1851
|
+
return methods.every((method) => method !== undefined) ? methods : [];
|
|
1852
|
+
}
|
|
1853
|
+
function blocked(routing, placement, prefix, method, entry) {
|
|
1854
|
+
if (!placement.ordered)
|
|
1855
|
+
return [];
|
|
1856
|
+
const at = below(placement, prefix, routing.indices.get(entry));
|
|
1857
|
+
const operation = routing.context.callerOperation(entry);
|
|
1858
|
+
return [{ endpoint: { operation, method, path: blockedPath(at.prefix) }, placement: at }];
|
|
1859
|
+
}
|
|
1860
|
+
function served(routing, placement, route, methods, operation, registration) {
|
|
1861
|
+
if (!placement.certain)
|
|
1862
|
+
return methods.flatMap((method) => blocked(routing, placement, route, method, registration.call));
|
|
1863
|
+
const at = below(placement, route, routing.indices.get(registration.call));
|
|
1864
|
+
const path = endpointPath(at.prefix, routing.registrars.get(registration.declaration).framework === "hono");
|
|
1865
|
+
return methods.map((method) => ({ endpoint: { operation, method, path }, placement: at }));
|
|
1866
|
+
}
|
|
1867
|
+
async function option(context, options, name) {
|
|
1868
|
+
const value = await heldAt(context, options, name);
|
|
1869
|
+
return typeof value === "object" ? value.node : undefined;
|
|
1870
|
+
}
|
|
1871
|
+
async function routeOptions(routing, registration, placement) {
|
|
1872
|
+
const { context } = routing;
|
|
1873
|
+
const { call } = registration;
|
|
1874
|
+
const argument = call.arguments[0];
|
|
1875
|
+
const options = await heldAt(context, argument);
|
|
1876
|
+
if (typeof options !== "object" || !context.ts.isObjectLiteralExpression(options.node)) {
|
|
1877
|
+
return blocked(routing, placement, readablePrefix(await urlParts(context, argument)), "*", call);
|
|
1878
|
+
}
|
|
1879
|
+
const url = await literalText(context, await option(context, argument, "url"));
|
|
1880
|
+
const methods = await methodList(context, await option(context, argument, "method"));
|
|
1881
|
+
if (url === undefined || methods.length === 0)
|
|
1882
|
+
return blocked(routing, placement, url ?? "", "*", call);
|
|
1883
|
+
const operation = await context.handlerOperation(await option(context, argument, "handler"), call);
|
|
1884
|
+
return served(routing, placement, url, methods, operation, registration);
|
|
1885
|
+
}
|
|
1886
|
+
async function unreadCall(routing, registration, placement) {
|
|
1887
|
+
const { call, member } = registration;
|
|
1888
|
+
const { context } = routing;
|
|
1889
|
+
const [first, second] = call.arguments;
|
|
1890
|
+
const path = member === "on" ? second : first;
|
|
1891
|
+
const prefix = path === undefined ? "" : readablePrefix(await urlParts(context, path));
|
|
1892
|
+
const method = member === "on" ? methodText(await literalText(context, first)) : undefined;
|
|
1893
|
+
return blocked(routing, placement, prefix, method ?? "*", call);
|
|
1894
|
+
}
|
|
1895
|
+
async function registerCall(routing, registration, placement) {
|
|
1896
|
+
const { call } = registration;
|
|
1897
|
+
const options = call.arguments[1];
|
|
1898
|
+
const prefix = options === undefined ? "absent" : await heldAt(routing.context, options, "prefix");
|
|
1899
|
+
const readable = typeof prefix === "object" ? readablePrefix(await urlParts(routing.context, prefix.node)) : "";
|
|
1900
|
+
const at = below(placement, readable);
|
|
1901
|
+
return [{ endpoint: { operation: routing.context.callerOperation(call), method: "*", path: blockedPath(at.prefix) }, placement: at }];
|
|
1902
|
+
}
|
|
1903
|
+
async function pathArgument(routing, registration) {
|
|
1904
|
+
const [first, second] = registration.call.arguments;
|
|
1905
|
+
const named = registration.member !== "redirect" && registration.call.arguments.length > 2 && routing.registrars.get(registration.declaration).framework === "koa";
|
|
1906
|
+
const name = named ? await literalText(routing.context, first) : undefined;
|
|
1907
|
+
return name !== undefined && !name.startsWith("/") ? second : first;
|
|
1908
|
+
}
|
|
1909
|
+
async function routeCall(routing, registration, placement) {
|
|
1910
|
+
const { call, member } = registration;
|
|
1911
|
+
const framework = routing.registrars.get(registration.declaration).framework;
|
|
1912
|
+
if (unreadHonoMembers.has(member))
|
|
1913
|
+
return unreadCall(routing, registration, placement);
|
|
1914
|
+
if (member === "register")
|
|
1915
|
+
return registerCall(routing, registration, placement);
|
|
1916
|
+
if (member === "route" && call.arguments.length === 1)
|
|
1917
|
+
return routeOptions(routing, registration, placement);
|
|
1918
|
+
const method = routeMethod(member, framework);
|
|
1919
|
+
if (method === undefined || call.arguments.length < 2)
|
|
1920
|
+
return [];
|
|
1921
|
+
const parts = await urlParts(routing.context, await pathArgument(routing, registration));
|
|
1922
|
+
const route = parts.length === 1 && parts[0].kind === "text" ? parts[0].text : undefined;
|
|
1923
|
+
if (route === undefined || member === "redirect" && !route.startsWith("/")) {
|
|
1924
|
+
return blocked(routing, placement, route === undefined ? readablePrefix(parts) : "", method, call);
|
|
1925
|
+
}
|
|
1926
|
+
const handler = member === "redirect" ? undefined : call.arguments[call.arguments.length - 1];
|
|
1927
|
+
return served(routing, placement, route, [method], await routing.context.handlerOperation(handler, call), registration);
|
|
1928
|
+
}
|
|
1929
|
+
async function mountCall(routing, registration, placement) {
|
|
1930
|
+
const { call } = registration;
|
|
1931
|
+
const mount = routing.calls.get(registration);
|
|
1932
|
+
const hidden = mount.children.some((child) => routing.registrars.get(child).prefix === undefined);
|
|
1933
|
+
const unfollowed = mount.prefix === undefined ? mount.children.length > 0 : !mount.pathless && mount.children.length === 0 && call.arguments.length > 1;
|
|
1934
|
+
if (!hidden && !mount.foreign && !unfollowed)
|
|
1935
|
+
return [];
|
|
1936
|
+
const parts = await urlParts(routing.context, call.arguments[0]);
|
|
1937
|
+
return blocked(routing, placement, mount.prefix ?? readablePrefix(parts), "*", call);
|
|
1938
|
+
}
|
|
1939
|
+
async function registrationEndpoints(routing, registration) {
|
|
1940
|
+
const placed = [];
|
|
1941
|
+
for (const placement of placements(routing, registration.declaration, registration.call)) {
|
|
1942
|
+
placed.push(...await (mounting(registration) ? mountCall : routeCall)(routing, registration, placement));
|
|
1943
|
+
}
|
|
1944
|
+
return placed;
|
|
1945
|
+
}
|
|
1946
|
+
async function routerEndpoints(context, sources, calls) {
|
|
1947
|
+
const reading = { ...await routerRegistrations(context, sources, calls), context };
|
|
1948
|
+
const routing = { ...reading, ...await collectMounts(reading) };
|
|
1949
|
+
const placed = routing.handOffs.flatMap(({ reference, declaration }) => placements(routing, declaration, reference).flatMap((placement) => blocked(routing, placement, "", "*", reference)));
|
|
1950
|
+
for (const registration of routing.registrations)
|
|
1951
|
+
placed.push(...await registrationEndpoints(routing, registration));
|
|
1952
|
+
return placed;
|
|
1953
|
+
}
|
|
1954
|
+
async function bunServe(context, call) {
|
|
1955
|
+
const { ts } = context;
|
|
1956
|
+
const callee = call.expression;
|
|
1957
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
1958
|
+
const receiver = callee.expression;
|
|
1959
|
+
return ts.isIdentifier(receiver) && receiver.text === "Bun" && callee.name.text === "serve" && await runtimeGlobal(context, receiver);
|
|
1960
|
+
}
|
|
1961
|
+
const origin = await importOrigin(context, callee);
|
|
1962
|
+
return origin?.module === "bun" && origin.name === "serve";
|
|
1963
|
+
}
|
|
1964
|
+
function serveMethod(key) {
|
|
1965
|
+
const method = methodText(key);
|
|
1966
|
+
return method !== undefined && [...routeMembers.values()].includes(method) ? method : undefined;
|
|
1967
|
+
}
|
|
1968
|
+
async function serveRoute(context, route, value, call) {
|
|
1969
|
+
const path = endpointPath(route);
|
|
1970
|
+
const byMethod = await objectEntries(context, value);
|
|
1971
|
+
if (byMethod === undefined) {
|
|
1972
|
+
const held = await heldAt(context, value);
|
|
1973
|
+
const handler = typeof held === "object" && context.ts.isFunctionLike(held.node);
|
|
1974
|
+
return handler ? [{ operation: await context.handlerOperation(value, call), method: "*", path }] : [];
|
|
1975
|
+
}
|
|
1976
|
+
const endpoints = [];
|
|
1977
|
+
for (const [key, handler] of byMethod) {
|
|
1978
|
+
const method = serveMethod(key);
|
|
1979
|
+
if (method !== undefined)
|
|
1980
|
+
endpoints.push({ operation: await context.handlerOperation(handler, call), method, path });
|
|
1981
|
+
}
|
|
1982
|
+
return endpoints;
|
|
1983
|
+
}
|
|
1984
|
+
async function serveEndpoints(context, calls) {
|
|
1985
|
+
const endpoints = [];
|
|
1986
|
+
for (const call of calls) {
|
|
1987
|
+
const [options] = call.arguments;
|
|
1988
|
+
if (options === undefined || !await bunServe(context, call))
|
|
1989
|
+
continue;
|
|
1990
|
+
const routes = await heldAt(context, options, "routes");
|
|
1991
|
+
const entries = typeof routes === "object" ? await objectEntries(context, routes.node) : undefined;
|
|
1992
|
+
for (const [route, value] of entries ?? [])
|
|
1993
|
+
endpoints.push(...await serveRoute(context, route, value, call));
|
|
1994
|
+
}
|
|
1995
|
+
return endpoints;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// plugins/scanners/javascript/src/http-requests.ts
|
|
1999
|
+
import ts3 from "./typescript.cjs";
|
|
2000
|
+
|
|
2001
|
+
// plugins/scanners/http-clients.ts
|
|
2002
|
+
async function declaredMethod(context, options, fallback) {
|
|
2003
|
+
if (options === undefined)
|
|
2004
|
+
return fallback;
|
|
2005
|
+
const declared = await heldAt(context, options, "method");
|
|
2006
|
+
if (declared === "absent")
|
|
2007
|
+
return fallback;
|
|
2008
|
+
return typeof declared === "string" ? undefined : methodName(context, declared.node);
|
|
2009
|
+
}
|
|
2010
|
+
async function optionsBase(context, config, fallback) {
|
|
2011
|
+
if (config === undefined)
|
|
2012
|
+
return fallback;
|
|
2013
|
+
const base = await heldAt(context, config, "baseURL");
|
|
2014
|
+
return base === "absent" ? fallback : heldParts(context, base);
|
|
2015
|
+
}
|
|
2016
|
+
async function runtimeFetch(context, callee) {
|
|
2017
|
+
if (!context.ts.isIdentifier(callee) || callee.text !== "fetch")
|
|
2018
|
+
return;
|
|
2019
|
+
const origin = await importOrigin(context, callee);
|
|
2020
|
+
const fetch = origin === undefined ? await runtimeGlobal(context, callee) : origin.module === "node-fetch" && origin.name === "default";
|
|
2021
|
+
return fetch ? "fetch" : undefined;
|
|
2022
|
+
}
|
|
2023
|
+
async function fetchRequest(context, call, client) {
|
|
2024
|
+
const [url, options] = call.arguments;
|
|
2025
|
+
const kind = url === undefined ? undefined : await client(call.expression);
|
|
2026
|
+
if (url === undefined || kind === undefined)
|
|
2027
|
+
return;
|
|
2028
|
+
const input = await heldAt(context, url);
|
|
2029
|
+
const method = input === "unseen" || typeof input === "object" && urlText(context.ts, input.node) ? await declaredMethod(context, options, "GET") : undefined;
|
|
2030
|
+
const parts = await urlParts(context, url);
|
|
2031
|
+
const target = kind === "ofetch" ? withBase(await optionsBase(context, options, []), parts) : parts;
|
|
2032
|
+
return { ...method === undefined ? {} : { method }, ...requestUrl(target) };
|
|
2033
|
+
}
|
|
2034
|
+
async function withDefaults(context, client, holders) {
|
|
2035
|
+
const found = await Promise.all(holders.map((holder) => context.bindings.settings(holder, ["defaults"], ["baseURL", "method"])));
|
|
2036
|
+
const bases = found.flatMap((settings) => settings.assigned.get("baseURL") ?? []);
|
|
2037
|
+
const methods = found.flatMap((settings) => settings.assigned.get("method") ?? []);
|
|
2038
|
+
if (found.some((settings) => settings.changed) || bases.length > 1 || methods.length > 1)
|
|
2039
|
+
return { base: [computedPart], method: undefined };
|
|
2040
|
+
return {
|
|
2041
|
+
base: bases[0] === undefined ? client.base : await urlParts(context, bases[0]),
|
|
2042
|
+
method: methods[0] === undefined ? client.method : methodText(await literalText(context, methods[0]))
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
function defaultClient(context) {
|
|
2046
|
+
return withDefaults(context, { base: [], method: "GET" }, context.bindings.defaultImports("axios"));
|
|
2047
|
+
}
|
|
2048
|
+
async function sent(context, client, url, method, config) {
|
|
2049
|
+
const effective = method ?? await declaredMethod(context, config, client.method);
|
|
2050
|
+
const base = await optionsBase(context, config, client.base);
|
|
2051
|
+
return { ...effective === undefined ? {} : { method: effective }, ...requestUrl(joinBase(base, url)) };
|
|
2052
|
+
}
|
|
2053
|
+
async function configRequest(context, client, config) {
|
|
2054
|
+
const url = config === undefined ? "absent" : await heldAt(context, config, "url");
|
|
2055
|
+
return url === "absent" ? undefined : sent(context, client, await heldParts(context, url), undefined, config);
|
|
2056
|
+
}
|
|
2057
|
+
async function holdsObject(context, node) {
|
|
2058
|
+
const value = await heldAt(context, node);
|
|
2059
|
+
return typeof value === "object" && context.ts.isObjectLiteralExpression(value.node);
|
|
2060
|
+
}
|
|
2061
|
+
var SHORTHAND = new Map([
|
|
2062
|
+
["get", "GET"],
|
|
2063
|
+
["post", "POST"],
|
|
2064
|
+
["put", "PUT"],
|
|
2065
|
+
["patch", "PATCH"],
|
|
2066
|
+
["delete", "DELETE"],
|
|
2067
|
+
["head", "HEAD"],
|
|
2068
|
+
["options", "OPTIONS"]
|
|
2069
|
+
]);
|
|
2070
|
+
var WITH_BODY = new Set(["post", "put", "patch"]);
|
|
2071
|
+
async function isAxios(context, node) {
|
|
2072
|
+
const origin = await importOrigin(context, node);
|
|
2073
|
+
return origin?.module === "axios" && origin.name === "default";
|
|
2074
|
+
}
|
|
2075
|
+
async function createdClient(context, node) {
|
|
2076
|
+
const { ts } = context;
|
|
2077
|
+
const created = await heldAt(context, node);
|
|
2078
|
+
const call = typeof created === "object" && ts.isCallExpression(created.node) ? created.node : undefined;
|
|
2079
|
+
const callee = call?.expression;
|
|
2080
|
+
if (call === undefined || callee === undefined || !ts.isPropertyAccessExpression(callee) || !ts.isIdentifier(callee.name))
|
|
2081
|
+
return;
|
|
2082
|
+
if (callee.name.text !== "create" || !await isAxios(context, callee.expression))
|
|
2083
|
+
return;
|
|
2084
|
+
const parent = await defaultClient(context);
|
|
2085
|
+
const inherited = { base: parent.base.length === 0 ? [] : [computedPart], method: parent.method === "GET" ? "GET" : undefined };
|
|
2086
|
+
const [config] = call.arguments;
|
|
2087
|
+
const client = { base: await optionsBase(context, config, inherited.base), method: await declaredMethod(context, config, inherited.method) };
|
|
2088
|
+
const holders = [await declarationOf(context, node), holderOf(ts, call)].filter((holder) => holder !== undefined);
|
|
2089
|
+
return withDefaults(context, client, [...new Set(holders)]);
|
|
2090
|
+
}
|
|
2091
|
+
async function axiosClient(context, node) {
|
|
2092
|
+
if (!context.ts.isIdentifier(node))
|
|
2093
|
+
return;
|
|
2094
|
+
return await isAxios(context, node) ? defaultClient(context) : createdClient(context, node);
|
|
2095
|
+
}
|
|
2096
|
+
async function axiosRequest(context, call) {
|
|
2097
|
+
const { ts } = context;
|
|
2098
|
+
const callee = call.expression;
|
|
2099
|
+
const [first, second, third] = call.arguments;
|
|
2100
|
+
if (ts.isPropertyAccessExpression(callee) && ts.isIdentifier(callee.name)) {
|
|
2101
|
+
const client = await axiosClient(context, callee.expression);
|
|
2102
|
+
if (client === undefined)
|
|
2103
|
+
return;
|
|
2104
|
+
const member = callee.name.text;
|
|
2105
|
+
const method = SHORTHAND.get(member);
|
|
2106
|
+
if (method !== undefined && first !== undefined) {
|
|
2107
|
+
return sent(context, client, await urlParts(context, first), method, WITH_BODY.has(member) ? third : second);
|
|
2108
|
+
}
|
|
2109
|
+
return member === "request" ? configRequest(context, client, first) : undefined;
|
|
2110
|
+
}
|
|
2111
|
+
const client = await axiosClient(context, callee);
|
|
2112
|
+
if (client === undefined || first === undefined)
|
|
2113
|
+
return;
|
|
2114
|
+
if (await holdsObject(context, first))
|
|
2115
|
+
return configRequest(context, client, first);
|
|
2116
|
+
return sent(context, client, await urlParts(context, first), undefined, second);
|
|
2117
|
+
}
|
|
2118
|
+
|
|
2119
|
+
// plugins/scanners/javascript/src/http-requests.ts
|
|
2120
|
+
var jqueryHelpers = new Map([
|
|
2121
|
+
["get", "GET"],
|
|
2122
|
+
["getJSON", "GET"],
|
|
2123
|
+
["getScript", "GET"],
|
|
2124
|
+
["post", "POST"]
|
|
2125
|
+
]);
|
|
2126
|
+
async function isJQuery(context, node) {
|
|
2127
|
+
if (!ts3.isIdentifier(node) || node.text !== "$" && node.text !== "jQuery")
|
|
2128
|
+
return false;
|
|
2129
|
+
const origin = await importOrigin(context, node);
|
|
2130
|
+
return origin === undefined ? runtimeGlobal(context, node) : origin.module === "jquery";
|
|
2131
|
+
}
|
|
2132
|
+
async function ajaxMethod(context, settings) {
|
|
2133
|
+
if (settings === undefined)
|
|
2134
|
+
return { method: "GET" };
|
|
2135
|
+
for (const key of ["method", "type"]) {
|
|
2136
|
+
const value = await heldAt(context, settings, key);
|
|
2137
|
+
if (value === "absent")
|
|
2138
|
+
continue;
|
|
2139
|
+
const method = typeof value === "object" ? await methodName(context, value.node) : undefined;
|
|
2140
|
+
return method === undefined ? {} : { method };
|
|
2141
|
+
}
|
|
2142
|
+
return { method: "GET" };
|
|
2143
|
+
}
|
|
2144
|
+
async function ajaxRequest(context, call) {
|
|
2145
|
+
const [first, second] = call.arguments;
|
|
2146
|
+
if (first === undefined)
|
|
2147
|
+
return;
|
|
2148
|
+
if (second !== undefined)
|
|
2149
|
+
return { ...await ajaxMethod(context, second), ...requestUrl(await urlParts(context, first)) };
|
|
2150
|
+
const url = await heldAt(context, first, "url");
|
|
2151
|
+
return url === "absent" ? undefined : { ...await ajaxMethod(context, first), ...requestUrl(await heldParts(context, url)) };
|
|
2152
|
+
}
|
|
2153
|
+
async function jqueryRequest(context, call) {
|
|
2154
|
+
const callee = call.expression;
|
|
2155
|
+
if (!ts3.isPropertyAccessExpression(callee) || !await isJQuery(context, callee.expression))
|
|
2156
|
+
return;
|
|
2157
|
+
const member = callee.name.text;
|
|
2158
|
+
const method = jqueryHelpers.get(member);
|
|
2159
|
+
const [url] = call.arguments;
|
|
2160
|
+
if (method !== undefined)
|
|
2161
|
+
return url === undefined ? undefined : { method, ...requestUrl(await urlParts(context, url)) };
|
|
2162
|
+
return member === "ajax" ? ajaxRequest(context, call) : undefined;
|
|
2163
|
+
}
|
|
2164
|
+
async function httpRequest(context, call) {
|
|
2165
|
+
return await fetchRequest(context, call, (callee) => runtimeFetch(context, callee)) ?? await axiosRequest(context, call) ?? jqueryRequest(context, call);
|
|
2166
|
+
}
|
|
2167
|
+
|
|
2168
|
+
// plugins/scanners/javascript/src/http.ts
|
|
2169
|
+
function fileChecker(source) {
|
|
2170
|
+
const options = { allowJs: true, noLib: true, noResolve: true, types: [], noEmit: true };
|
|
2171
|
+
const host = {
|
|
2172
|
+
getSourceFile: (name) => name === source.fileName ? source : undefined,
|
|
2173
|
+
getDefaultLibFileName: () => "lib.d.ts",
|
|
2174
|
+
writeFile: () => {
|
|
2175
|
+
return;
|
|
2176
|
+
},
|
|
2177
|
+
getCurrentDirectory: () => "",
|
|
2178
|
+
getCanonicalFileName: (name) => name,
|
|
2179
|
+
useCaseSensitiveFileNames: () => true,
|
|
2180
|
+
getNewLine: () => `
|
|
2181
|
+
`,
|
|
2182
|
+
fileExists: (name) => name === source.fileName,
|
|
2183
|
+
readFile: () => {
|
|
2184
|
+
return;
|
|
2185
|
+
}
|
|
2186
|
+
};
|
|
2187
|
+
return ts4.createProgram([source.fileName], options, host).getTypeChecker();
|
|
2188
|
+
}
|
|
2189
|
+
function classic(node) {
|
|
2190
|
+
return node;
|
|
2191
|
+
}
|
|
2192
|
+
function fileContext(source, evidence) {
|
|
2193
|
+
const context = urlContext(ts4, classicChecker(ts4, fileChecker(source)), [source], true);
|
|
2194
|
+
const operationAt = (node) => evidence.operationAt(classic(node));
|
|
2195
|
+
return {
|
|
2196
|
+
...context,
|
|
2197
|
+
frameworks: new Set(["express", "fastify", "hono", "koa"]),
|
|
2198
|
+
exportsHandOff: true,
|
|
2199
|
+
async handlerOperation(handler, registration) {
|
|
2200
|
+
const held = handler === undefined ? undefined : await heldAt(context, handler);
|
|
2201
|
+
return typeof held === "object" && ts4.isFunctionLike(classic(held.node)) ? operationAt(held.node) : operationAt(registration);
|
|
2202
|
+
},
|
|
2203
|
+
callerOperation: operationAt,
|
|
2204
|
+
file: (node) => node.getSourceFile().fileName
|
|
2205
|
+
};
|
|
2206
|
+
}
|
|
2207
|
+
async function javaScriptHttpFacts(source, evidence) {
|
|
2208
|
+
const context = fileContext(source, evidence);
|
|
2209
|
+
const httpRequests = [];
|
|
2210
|
+
for (const call of evidence.calls) {
|
|
2211
|
+
const request = await httpRequest(context, call);
|
|
2212
|
+
if (request !== undefined)
|
|
2213
|
+
httpRequests.push({ operation: evidence.operationAt(call), ...request });
|
|
2214
|
+
}
|
|
2215
|
+
const placed = await routerEndpoints(context, [source], evidence.calls);
|
|
2216
|
+
return { httpEndpoints: [...withOrder(placed), ...await serveEndpoints(context, evidence.calls)], httpRequests };
|
|
2217
|
+
}
|
|
2218
|
+
|
|
2219
|
+
// plugins/scanners/javascript/src/outline.ts
|
|
2220
|
+
import { readFile } from "fs/promises";
|
|
2221
|
+
import path from "path";
|
|
2222
|
+
import ts5 from "./typescript.cjs";
|
|
2223
|
+
|
|
2224
|
+
// plugins/scanners/typescript-outline.ts
|
|
2225
|
+
function is2(node, kind) {
|
|
2226
|
+
return node !== undefined && node.kind === kind;
|
|
2227
|
+
}
|
|
2228
|
+
var NO_EXPORTS = new Set;
|
|
2229
|
+
function hasModifier(node, kind) {
|
|
2230
|
+
return node.modifiers?.some((modifier) => modifier.kind === kind) ?? false;
|
|
2231
|
+
}
|
|
2232
|
+
function listedExports(k, statement) {
|
|
2233
|
+
if (is2(statement, k.ExportAssignment)) {
|
|
2234
|
+
return is2(statement.expression, k.Identifier) ? [statement.expression.text] : [];
|
|
2235
|
+
}
|
|
2236
|
+
if (!is2(statement, k.ExportDeclaration) || statement.moduleSpecifier !== undefined)
|
|
2237
|
+
return [];
|
|
2238
|
+
const clause = statement.exportClause;
|
|
2239
|
+
if (!is2(clause, k.NamedExports))
|
|
2240
|
+
return [];
|
|
2241
|
+
return clause.elements.map((element) => (element.propertyName ?? element.name).text);
|
|
2242
|
+
}
|
|
2243
|
+
function topLevelVisibility(scope, name, statement) {
|
|
2244
|
+
if (scope.topLevelPrivate)
|
|
2245
|
+
return "private";
|
|
2246
|
+
return hasModifier(statement, scope.k.ExportKeyword) || scope.exported.has(name) ? "public" : "private";
|
|
2247
|
+
}
|
|
2248
|
+
function memberVisibility(scope, member) {
|
|
2249
|
+
const { k } = scope;
|
|
2250
|
+
if (is2(member.name, k.PrivateIdentifier) || hasModifier(member, k.PrivateKeyword))
|
|
2251
|
+
return "private";
|
|
2252
|
+
return hasModifier(member, k.ProtectedKeyword) ? "protected" : "public";
|
|
2253
|
+
}
|
|
2254
|
+
function symbolAt(scope, name, node, visibility) {
|
|
2255
|
+
const line = scope.source.getLineAndCharacterOfPosition(node.getStart()).line + 1;
|
|
2256
|
+
return { name, line, visibility, entry: scope.symbols.includes(name) };
|
|
2257
|
+
}
|
|
2258
|
+
function functionDeclarations(scope, statement) {
|
|
2259
|
+
const { k } = scope;
|
|
2260
|
+
if (is2(statement, k.FunctionDeclaration) && is2(statement.name, k.Identifier)) {
|
|
2261
|
+
const name = statement.name.text;
|
|
2262
|
+
return [{ kind: "function", ...symbolAt(scope, name, statement.name, topLevelVisibility(scope, name, statement)) }];
|
|
2263
|
+
}
|
|
2264
|
+
if (!is2(statement, k.VariableStatement))
|
|
2265
|
+
return [];
|
|
2266
|
+
return statement.declarationList.declarations.flatMap((declaration) => {
|
|
2267
|
+
const { name, initializer } = declaration;
|
|
2268
|
+
if (!is2(name, k.Identifier))
|
|
2269
|
+
return [];
|
|
2270
|
+
if (!is2(initializer, k.ArrowFunction) && !is2(initializer, k.FunctionExpression))
|
|
2271
|
+
return [];
|
|
2272
|
+
return [{ kind: "function", ...symbolAt(scope, name.text, name, topLevelVisibility(scope, name.text, statement)) }];
|
|
2273
|
+
});
|
|
2274
|
+
}
|
|
2275
|
+
function memberName(scope, name) {
|
|
2276
|
+
const { k } = scope;
|
|
2277
|
+
if (is2(name, k.Identifier) || is2(name, k.PrivateIdentifier))
|
|
2278
|
+
return name;
|
|
2279
|
+
return is2(name, k.StringLiteral) || is2(name, k.NumericLiteral) ? name : undefined;
|
|
2280
|
+
}
|
|
2281
|
+
function typeMember(scope, member) {
|
|
2282
|
+
const { k } = scope;
|
|
2283
|
+
if (member.kind === k.Constructor)
|
|
2284
|
+
return [symbolAt(scope, "constructor", member, memberVisibility(scope, member))];
|
|
2285
|
+
if (!is2(member, k.MethodDeclaration) && !is2(member, k.MethodSignature))
|
|
2286
|
+
return [];
|
|
2287
|
+
const name = memberName(scope, member.name);
|
|
2288
|
+
return name === undefined ? [] : [symbolAt(scope, name.text, name, memberVisibility(scope, member))];
|
|
2289
|
+
}
|
|
2290
|
+
function typeMembers(k, statement) {
|
|
2291
|
+
if (is2(statement, k.ClassDeclaration) || is2(statement, k.InterfaceDeclaration))
|
|
2292
|
+
return statement.members;
|
|
2293
|
+
return statement.kind === k.EnumDeclaration ? [] : undefined;
|
|
2294
|
+
}
|
|
2295
|
+
function typeDeclaration(scope, statement) {
|
|
2296
|
+
const members = typeMembers(scope.k, statement);
|
|
2297
|
+
const { name } = statement;
|
|
2298
|
+
if (members === undefined || !is2(name, scope.k.Identifier))
|
|
2299
|
+
return [];
|
|
2300
|
+
return [{
|
|
2301
|
+
kind: "type",
|
|
2302
|
+
...symbolAt(scope, name.text, name, topLevelVisibility(scope, name.text, statement)),
|
|
2303
|
+
members: members.flatMap((member) => typeMember(scope, member))
|
|
2304
|
+
}];
|
|
2305
|
+
}
|
|
2306
|
+
function moduleStatements(k, statement) {
|
|
2307
|
+
let body = statement.body;
|
|
2308
|
+
while (is2(body, k.ModuleDeclaration))
|
|
2309
|
+
body = body.body;
|
|
2310
|
+
return is2(body, k.ModuleBlock) ? body.statements : [];
|
|
2311
|
+
}
|
|
2312
|
+
function declarationsIn(scope, statements) {
|
|
2313
|
+
return statements.flatMap((statement) => {
|
|
2314
|
+
if (is2(statement, scope.k.ModuleDeclaration)) {
|
|
2315
|
+
return declarationsIn({ ...scope, exported: NO_EXPORTS }, moduleStatements(scope.k, statement));
|
|
2316
|
+
}
|
|
2317
|
+
return [...functionDeclarations(scope, statement), ...typeDeclaration(scope, statement)];
|
|
2318
|
+
});
|
|
2319
|
+
}
|
|
2320
|
+
function outlineSource(ts, source, context) {
|
|
2321
|
+
const k = ts.SyntaxKind;
|
|
2322
|
+
const exported = new Set([...source.statements.flatMap((statement) => listedExports(k, statement)), ...context.exported ?? []]);
|
|
2323
|
+
const scope = { k, source, symbols: context.symbols, exported, topLevelPrivate: context.topLevelPrivate ?? false };
|
|
2324
|
+
return declarationsIn(scope, source.statements);
|
|
2325
|
+
}
|
|
2326
|
+
|
|
2327
|
+
// plugins/scanners/javascript/src/outline.ts
|
|
2328
|
+
var MODULE_EXTENSION = /\.(?:mjs|cjs)$/;
|
|
2329
|
+
function isExportsReference(node) {
|
|
2330
|
+
if (ts5.isIdentifier(node))
|
|
2331
|
+
return node.text === "exports";
|
|
2332
|
+
return ts5.isPropertyAccessExpression(node) && ts5.isIdentifier(node.expression) && node.expression.text === "module" && node.name.text === "exports";
|
|
2333
|
+
}
|
|
2334
|
+
function assignedNames(value) {
|
|
2335
|
+
if (ts5.isIdentifier(value))
|
|
2336
|
+
return [value.text];
|
|
2337
|
+
if (!ts5.isObjectLiteralExpression(value))
|
|
2338
|
+
return [];
|
|
2339
|
+
return value.properties.flatMap((property) => {
|
|
2340
|
+
if (ts5.isShorthandPropertyAssignment(property))
|
|
2341
|
+
return [property.name.text];
|
|
2342
|
+
return ts5.isPropertyAssignment(property) && ts5.isIdentifier(property.initializer) ? [property.initializer.text] : [];
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
function commonJsExport2(statement) {
|
|
2346
|
+
if (!ts5.isExpressionStatement(statement))
|
|
2347
|
+
return;
|
|
2348
|
+
const assignment = statement.expression;
|
|
2349
|
+
if (!ts5.isBinaryExpression(assignment) || assignment.operatorToken.kind !== ts5.SyntaxKind.EqualsToken)
|
|
2350
|
+
return;
|
|
2351
|
+
const { left } = assignment;
|
|
2352
|
+
const target = ts5.isPropertyAccessExpression(left) && isExportsReference(left.expression) ? left.expression : left;
|
|
2353
|
+
return isExportsReference(target) ? assignment : undefined;
|
|
2354
|
+
}
|
|
2355
|
+
function publishedBy(assignment) {
|
|
2356
|
+
const { left, right } = assignment;
|
|
2357
|
+
if (ts5.isPropertyAccessExpression(left) && isExportsReference(left.expression)) {
|
|
2358
|
+
return ts5.isIdentifier(right) ? [right.text] : [];
|
|
2359
|
+
}
|
|
2360
|
+
return assignedNames(right);
|
|
2361
|
+
}
|
|
2362
|
+
function statesEcmaScriptModule(source) {
|
|
2363
|
+
return source.statements.some((statement) => ts5.isImportDeclaration(statement) || ts5.isExportDeclaration(statement) || ts5.isExportAssignment(statement) || ts5.canHaveModifiers(statement) && (ts5.getModifiers(statement) ?? []).some((modifier) => modifier.kind === ts5.SyntaxKind.ExportKeyword));
|
|
2364
|
+
}
|
|
2365
|
+
function publishedNames(file, source) {
|
|
2366
|
+
const exports = source.statements.flatMap((statement) => commonJsExport2(statement) ?? []);
|
|
2367
|
+
if (exports.length > 0 || MODULE_EXTENSION.test(file) || statesEcmaScriptModule(source)) {
|
|
2368
|
+
return exports.flatMap(publishedBy);
|
|
2369
|
+
}
|
|
2370
|
+
return topLevelDeclarations(source).map((declaration) => declaration.name);
|
|
2371
|
+
}
|
|
2372
|
+
async function readJavaScriptOutline(repositoryRoot, references) {
|
|
2373
|
+
const files = [];
|
|
2374
|
+
for (const reference of references) {
|
|
2375
|
+
const fileName = path.join(repositoryRoot, reference.file);
|
|
2376
|
+
const text = await readFile(fileName, "utf8");
|
|
2377
|
+
const source = ts5.createSourceFile(fileName, text, ts5.ScriptTarget.Latest, true);
|
|
2378
|
+
const declarations = outlineSource(ts5, source, { symbols: reference.symbols, exported: publishedNames(reference.file, source) });
|
|
2379
|
+
if (declarations.length > 0)
|
|
2380
|
+
files.push({ file: reference.file, declarations });
|
|
2381
|
+
}
|
|
2382
|
+
return files;
|
|
2383
|
+
}
|
|
2384
|
+
|
|
2385
|
+
// plugins/scanners/javascript/src/sources.ts
|
|
2386
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
2387
|
+
import path3 from "path";
|
|
2388
|
+
|
|
2389
|
+
// plugins/scanners/projects.ts
|
|
2390
|
+
import { execFile } from "child_process";
|
|
2391
|
+
import { existsSync } from "fs";
|
|
2392
|
+
import path2 from "path";
|
|
2393
|
+
import { promisify } from "util";
|
|
2394
|
+
var execute = promisify(execFile);
|
|
2395
|
+
var excluded = new Set([
|
|
2396
|
+
".git",
|
|
2397
|
+
"node_modules",
|
|
2398
|
+
"vendor",
|
|
2399
|
+
"target",
|
|
2400
|
+
"dist",
|
|
2401
|
+
"build",
|
|
2402
|
+
"bin",
|
|
2403
|
+
"obj",
|
|
2404
|
+
".gradle",
|
|
2405
|
+
".angular",
|
|
2406
|
+
"coverage",
|
|
2407
|
+
"generated",
|
|
2408
|
+
"groma",
|
|
2409
|
+
".groma"
|
|
2410
|
+
]);
|
|
2411
|
+
async function repositoryFiles(root, matches) {
|
|
2412
|
+
const { stdout } = await execute("git", ["-C", root, "ls-files", "-z", "--cached", "--others", "--exclude-standard"], { maxBuffer: 64 * 1024 * 1024 });
|
|
2413
|
+
return [...new Set(stdout.split("\x00").filter((file) => file && matches(file) && existsSync(path2.join(root, file))))].sort();
|
|
2414
|
+
}
|
|
2415
|
+
function projectFiles(root, matches) {
|
|
2416
|
+
return repositoryFiles(root, (file) => matches(file) && !file.split("/").some((part) => excluded.has(part)));
|
|
2417
|
+
}
|
|
2418
|
+
|
|
2419
|
+
// plugins/scanners/javascript/src/sources.ts
|
|
2420
|
+
var SOURCE = /\.(?:js|mjs|cjs|jsx)$/;
|
|
2421
|
+
var MINIFIED_NAME = /\.min\.(?:js|mjs|cjs|jsx)$/;
|
|
2422
|
+
var MINIFIED_LINE_LENGTH = 500;
|
|
2423
|
+
function isJavaScriptFile(file) {
|
|
2424
|
+
return SOURCE.test(file);
|
|
2425
|
+
}
|
|
2426
|
+
function hasMinifiedName(file) {
|
|
2427
|
+
return MINIFIED_NAME.test(path3.posix.basename(file));
|
|
2428
|
+
}
|
|
2429
|
+
function isMinifiedText(text) {
|
|
2430
|
+
const code = text.trimEnd();
|
|
2431
|
+
return code.length / code.split(`
|
|
2432
|
+
`).length >= MINIFIED_LINE_LENGTH;
|
|
2433
|
+
}
|
|
2434
|
+
async function javaScriptSources(root) {
|
|
2435
|
+
const files = await projectFiles(root, (file) => isJavaScriptFile(file) && !hasMinifiedName(file));
|
|
2436
|
+
const sources = [];
|
|
2437
|
+
for (const file of files) {
|
|
2438
|
+
const text = await readFile2(path3.join(root, file), "utf8");
|
|
2439
|
+
if (!isMinifiedText(text))
|
|
2440
|
+
sources.push({ file, text });
|
|
2441
|
+
}
|
|
2442
|
+
return sources;
|
|
2443
|
+
}
|
|
2444
|
+
|
|
2445
|
+
// plugins/scanners/javascript/src/index.ts
|
|
2446
|
+
function parse(source) {
|
|
2447
|
+
return ts6.createSourceFile(source.file, source.text, ts6.ScriptTarget.Latest, true);
|
|
2448
|
+
}
|
|
2449
|
+
var SLOPPY_MODE_ONLY = new Set([1121, 1487, 1488, 1489]);
|
|
2450
|
+
function parseWarning(source) {
|
|
2451
|
+
const { parseDiagnostics } = source;
|
|
2452
|
+
const first = parseDiagnostics.find((diagnostic) => !SLOPPY_MODE_ONLY.has(diagnostic.code));
|
|
2453
|
+
if (first === undefined)
|
|
2454
|
+
return;
|
|
2455
|
+
return {
|
|
2456
|
+
severity: "warning",
|
|
2457
|
+
code: "JAVASCRIPT_SOURCE_INVALID",
|
|
2458
|
+
file: source.fileName,
|
|
2459
|
+
line: source.getLineAndCharacterOfPosition(first.start).line + 1,
|
|
2460
|
+
message: `The file does not parse, so it contributes no evidence: ${ts6.flattenDiagnosticMessageText(first.messageText, " ")}`
|
|
2461
|
+
};
|
|
2462
|
+
}
|
|
2463
|
+
var src_default = {
|
|
2464
|
+
id: "javascript",
|
|
2465
|
+
watch: {
|
|
2466
|
+
include: ["**/*.js", "**/*.mjs", "**/*.cjs", "**/*.jsx"],
|
|
2467
|
+
exclude: ["**/*.min.js", "**/*.min.mjs", "**/*.min.cjs", "**/*.min.jsx"]
|
|
2468
|
+
},
|
|
2469
|
+
listSourceFiles: async (root) => (await javaScriptSources(root)).map((source) => source.file),
|
|
2470
|
+
async checkReadiness(root) {
|
|
2471
|
+
if (!(await javaScriptSources(root)).length) {
|
|
2472
|
+
throw new Error("javascript: No authored JavaScript source files were found in the Git repository.");
|
|
2473
|
+
}
|
|
2474
|
+
},
|
|
2475
|
+
async scan(root) {
|
|
2476
|
+
const sources = await javaScriptSources(root);
|
|
2477
|
+
if (!sources.length)
|
|
2478
|
+
return;
|
|
2479
|
+
const parsed = sources.map(parse);
|
|
2480
|
+
const warnings = parsed.map(parseWarning);
|
|
2481
|
+
const readable = parsed.filter((_, index) => warnings[index] === undefined);
|
|
2482
|
+
const scanned = await Promise.all(readable.map(async (source) => {
|
|
2483
|
+
const evidence = javaScriptEvidence(source.fileName, source);
|
|
2484
|
+
return { file: source.fileName, evidence, facts: await javaScriptHttpFacts(source, evidence) };
|
|
2485
|
+
}));
|
|
2486
|
+
const symbols = new Map(scanned.map(({ file, evidence }) => [file, evidence.symbols]));
|
|
2487
|
+
return createScanObservation({
|
|
2488
|
+
scanner: { id: "javascript", technology: "javascript", engine: "typescript-sdk", engineVersion: ts6.version },
|
|
2489
|
+
roots: [{ id: "javascript-source", kind: "source-group", name: path4.basename(root) }],
|
|
2490
|
+
files: parsed.map(({ fileName: file }) => ({ file, symbols: symbols.get(file) ?? [], roots: ["javascript-source"] })),
|
|
2491
|
+
operations: scanned.flatMap(({ evidence }) => evidence.operations),
|
|
2492
|
+
invocations: scanned.flatMap(({ evidence }) => evidence.invocations),
|
|
2493
|
+
httpEndpoints: scanned.flatMap(({ facts }) => facts.httpEndpoints),
|
|
2494
|
+
httpRequests: scanned.flatMap(({ facts }) => facts.httpRequests),
|
|
2495
|
+
diagnostics: [
|
|
2496
|
+
{
|
|
2497
|
+
severity: "info",
|
|
2498
|
+
code: "JAVASCRIPT_SOURCE_SCOPE",
|
|
2499
|
+
message: "Source syntax only. Module loading, dynamic dispatch, framework wiring and external symbols remain unresolved."
|
|
2500
|
+
},
|
|
2501
|
+
...warnings.filter((warning) => warning !== undefined)
|
|
2502
|
+
]
|
|
2503
|
+
});
|
|
2504
|
+
},
|
|
2505
|
+
readCodeStructure: readJavaScriptOutline
|
|
2506
|
+
};
|
|
2507
|
+
export {
|
|
2508
|
+
src_default as default
|
|
2509
|
+
};
|