@ankhorage/doctor 0.14.11 → 0.14.13
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/CHANGELOG.md +12 -0
- package/README.md +1 -1
- package/dist/analysis.d.ts.map +1 -1
- package/dist/analysis.js +12 -3
- package/dist/analysis.js.map +1 -1
- package/dist/capabilityCatalog.d.ts +1 -1
- package/dist/capabilityCatalog.d.ts.map +1 -1
- package/dist/capabilityCatalog.js +2 -2
- package/dist/capabilityCatalog.js.map +1 -1
- package/dist/capabilityCatalogAst.d.ts +1 -1
- package/dist/capabilityCatalogAst.d.ts.map +1 -1
- package/dist/capabilityCatalogAst.js +54 -83
- package/dist/capabilityCatalogAst.js.map +1 -1
- package/dist/capabilityCatalogAstSource.d.ts +12 -0
- package/dist/capabilityCatalogAstSource.d.ts.map +1 -0
- package/dist/capabilityCatalogAstSource.js +38 -0
- package/dist/capabilityCatalogAstSource.js.map +1 -0
- package/dist/capabilityCatalogImport.d.ts +9 -0
- package/dist/capabilityCatalogImport.d.ts.map +1 -0
- package/dist/capabilityCatalogImport.js +55 -0
- package/dist/capabilityCatalogImport.js.map +1 -0
- package/dist/capabilityCatalogStaticValue.d.ts +13 -0
- package/dist/capabilityCatalogStaticValue.d.ts.map +1 -0
- package/dist/capabilityCatalogStaticValue.js +35 -0
- package/dist/capabilityCatalogStaticValue.js.map +1 -0
- package/package.json +2 -2
- package/src/analysis.ts +23 -4
- package/src/capabilityCatalog.ts +7 -1
- package/src/capabilityCatalogAst.ts +92 -116
- package/src/capabilityCatalogAstSource.ts +52 -0
- package/src/capabilityCatalogImport.ts +69 -0
- package/src/capabilityCatalogStaticValue.ts +48 -0
|
@@ -1,22 +1,35 @@
|
|
|
1
1
|
import { promises as fs } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
2
|
|
|
4
3
|
import * as ts from 'typescript';
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
5
|
+
import {
|
|
6
|
+
findImportBinding,
|
|
7
|
+
findStaticDeclaration,
|
|
8
|
+
hasExportModifier,
|
|
9
|
+
isConstDeclaration,
|
|
10
|
+
unwrapExpression,
|
|
11
|
+
} from './capabilityCatalogAstSource.js';
|
|
12
|
+
import { resolveImportPathAsync, resolveLocalImportPathAsync } from './capabilityCatalogImport.js';
|
|
13
|
+
import {
|
|
14
|
+
isStaticArray,
|
|
15
|
+
isStaticRecord,
|
|
16
|
+
readStaticArray,
|
|
17
|
+
readStaticProperty,
|
|
18
|
+
type StaticRecord,
|
|
19
|
+
type StaticValue,
|
|
20
|
+
} from './capabilityCatalogStaticValue.js';
|
|
12
21
|
|
|
13
22
|
/*** Evaluate one named catalog export from the supported static TypeScript subset. */
|
|
14
23
|
export async function capabilityCatalogAstAsync(
|
|
15
24
|
packageRoot: string,
|
|
16
25
|
modulePath: string,
|
|
17
26
|
exportName: string,
|
|
27
|
+
materializedCatalog?: unknown,
|
|
18
28
|
): Promise<unknown> {
|
|
19
|
-
return new StaticCatalogResolver(packageRoot).resolveAsync(
|
|
29
|
+
return new StaticCatalogResolver(packageRoot, materializedCatalog).resolveAsync(
|
|
30
|
+
modulePath,
|
|
31
|
+
exportName,
|
|
32
|
+
);
|
|
20
33
|
}
|
|
21
34
|
|
|
22
35
|
/*** Resolve static local imports and expressions without loading target modules. */
|
|
@@ -24,7 +37,14 @@ class StaticCatalogResolver {
|
|
|
24
37
|
readonly #symbols = new Map<string, StaticValue>();
|
|
25
38
|
readonly #activeSymbols: string[] = [];
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
private readonly materializedCatalog: readonly StaticValue[] | null;
|
|
41
|
+
|
|
42
|
+
constructor(
|
|
43
|
+
private readonly packageRoot: string,
|
|
44
|
+
materializedCatalog?: unknown,
|
|
45
|
+
) {
|
|
46
|
+
this.materializedCatalog = readStaticArray(materializedCatalog);
|
|
47
|
+
}
|
|
28
48
|
|
|
29
49
|
/*** Resolve a named local export without importing or running its module. */
|
|
30
50
|
async resolveAsync(
|
|
@@ -98,6 +118,7 @@ class StaticCatalogResolver {
|
|
|
98
118
|
if (node.kind === ts.SyntaxKind.FalseKeyword) return false;
|
|
99
119
|
if (node.kind === ts.SyntaxKind.NullKeyword) return null;
|
|
100
120
|
if (ts.isIdentifier(node)) return environment.resolveAsync(node.text);
|
|
121
|
+
if (ts.isPropertyAccessExpression(node)) return this.propertyAccessAsync(node, environment);
|
|
101
122
|
if (ts.isArrayLiteralExpression(node)) return this.arrayAsync(node, environment);
|
|
102
123
|
if (ts.isObjectLiteralExpression(node)) return this.objectAsync(node, environment);
|
|
103
124
|
if (ts.isTemplateExpression(node)) return this.templateAsync(node, environment);
|
|
@@ -105,6 +126,29 @@ class StaticCatalogResolver {
|
|
|
105
126
|
throw new Error(`Unsupported static capability expression: ${node.getText()}.`);
|
|
106
127
|
}
|
|
107
128
|
|
|
129
|
+
/*** Evaluate one static object property while preserving dynamic-expression diagnostics. */
|
|
130
|
+
async propertyAccessAsync(
|
|
131
|
+
node: ts.PropertyAccessExpression,
|
|
132
|
+
environment: StaticCatalogScope,
|
|
133
|
+
): Promise<StaticValue> {
|
|
134
|
+
try {
|
|
135
|
+
return readStaticProperty(
|
|
136
|
+
await this.evaluateAsync(node.expression, environment),
|
|
137
|
+
node.name.text,
|
|
138
|
+
node.getText(),
|
|
139
|
+
);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (
|
|
142
|
+
error instanceof Error &&
|
|
143
|
+
error.message.startsWith('Unsupported dynamic capability value:')
|
|
144
|
+
)
|
|
145
|
+
throw new Error(`Unsupported static capability expression: ${node.getText()}.`, {
|
|
146
|
+
cause: error,
|
|
147
|
+
});
|
|
148
|
+
throw error;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
108
152
|
/*** Evaluate a static array literal. */
|
|
109
153
|
arrayAsync(
|
|
110
154
|
node: ts.ArrayLiteralExpression,
|
|
@@ -180,9 +224,21 @@ class StaticCatalogResolver {
|
|
|
180
224
|
environment: StaticCatalogScope,
|
|
181
225
|
): Promise<readonly StaticValue[]> {
|
|
182
226
|
const result: StaticValue[] = [];
|
|
183
|
-
for (const element of elements) {
|
|
184
|
-
if (ts.isSpreadElement(element))
|
|
185
|
-
|
|
227
|
+
for (const [index, element] of elements.entries()) {
|
|
228
|
+
if (ts.isSpreadElement(element)) {
|
|
229
|
+
if (await environment.isPackageLocalDerivedCallAsync(element.expression)) {
|
|
230
|
+
if (index !== elements.length - 1 || this.materializedCatalog === null)
|
|
231
|
+
throw new Error(
|
|
232
|
+
'Derived catalog function spreads must be trailing and have materialized package metadata.',
|
|
233
|
+
);
|
|
234
|
+
result.push(...this.materializedCatalog.slice(result.length));
|
|
235
|
+
continue;
|
|
236
|
+
}
|
|
237
|
+
const spread = await this.evaluateAsync(element.expression, environment);
|
|
238
|
+
if (!isStaticArray(spread))
|
|
239
|
+
throw new Error('Catalog array spreads must resolve to static arrays.');
|
|
240
|
+
result.push(...spread);
|
|
241
|
+
} else result.push(await this.evaluateAsync(element, environment));
|
|
186
242
|
}
|
|
187
243
|
return result;
|
|
188
244
|
}
|
|
@@ -233,6 +289,30 @@ class StaticCatalogScope {
|
|
|
233
289
|
);
|
|
234
290
|
}
|
|
235
291
|
|
|
292
|
+
/*** Detect one zero-argument function imported from a package-local source module. */
|
|
293
|
+
async isPackageLocalDerivedCallAsync(expression: ts.Expression): Promise<boolean> {
|
|
294
|
+
const node = unwrapExpression(expression);
|
|
295
|
+
if (
|
|
296
|
+
!ts.isCallExpression(node) ||
|
|
297
|
+
node.arguments.length !== 0 ||
|
|
298
|
+
!ts.isIdentifier(node.expression)
|
|
299
|
+
)
|
|
300
|
+
return false;
|
|
301
|
+
const imported = findImportBinding(this.source, node.expression.text);
|
|
302
|
+
if (
|
|
303
|
+
imported === undefined ||
|
|
304
|
+
!ts.isStringLiteral(imported.moduleSpecifier) ||
|
|
305
|
+
!imported.moduleSpecifier.text.startsWith('.')
|
|
306
|
+
)
|
|
307
|
+
return false;
|
|
308
|
+
await resolveLocalImportPathAsync(
|
|
309
|
+
this.packageRoot,
|
|
310
|
+
this.modulePath,
|
|
311
|
+
imported.moduleSpecifier.text,
|
|
312
|
+
);
|
|
313
|
+
return true;
|
|
314
|
+
}
|
|
315
|
+
|
|
236
316
|
/*** Add a static callback parameter without evaluating unrelated module symbols. */
|
|
237
317
|
withValue(name: string, value: StaticValue): StaticCatalogScope {
|
|
238
318
|
return new StaticCatalogScope(
|
|
@@ -244,107 +324,3 @@ class StaticCatalogScope {
|
|
|
244
324
|
);
|
|
245
325
|
}
|
|
246
326
|
}
|
|
247
|
-
|
|
248
|
-
/*** Find the static top-level declaration that owns a local identifier. */
|
|
249
|
-
function findStaticDeclaration(
|
|
250
|
-
source: ts.SourceFile,
|
|
251
|
-
name: string,
|
|
252
|
-
): ts.VariableDeclaration | undefined {
|
|
253
|
-
return source.statements
|
|
254
|
-
.filter((statement): statement is ts.VariableStatement => ts.isVariableStatement(statement))
|
|
255
|
-
.filter((statement) => isConstDeclaration(statement.declarationList))
|
|
256
|
-
.flatMap((statement) => statement.declarationList.declarations)
|
|
257
|
-
.find((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name);
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
/*** Find the import declaration that introduces a referenced local identifier. */
|
|
261
|
-
function findImportBinding(source: ts.SourceFile, name: string): ts.ImportDeclaration | undefined {
|
|
262
|
-
return source.statements
|
|
263
|
-
.filter((statement): statement is ts.ImportDeclaration => ts.isImportDeclaration(statement))
|
|
264
|
-
.find((statement) => {
|
|
265
|
-
const clause = statement.importClause;
|
|
266
|
-
return (
|
|
267
|
-
clause?.name?.text === name ||
|
|
268
|
-
(clause?.namedBindings !== undefined &&
|
|
269
|
-
ts.isNamedImports(clause.namedBindings) &&
|
|
270
|
-
clause.namedBindings.elements.some((element) => element.name.text === name))
|
|
271
|
-
);
|
|
272
|
-
});
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
/*** Determine whether a declaration list is immutable static catalog input. */
|
|
276
|
-
function isConstDeclaration(list: ts.VariableDeclarationList): boolean {
|
|
277
|
-
return (list.flags & ts.NodeFlags.Const) !== 0;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
interface ResolvedStaticImport {
|
|
281
|
-
readonly packageRoot: string;
|
|
282
|
-
readonly path: string;
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
/*** Resolve an allowed static import without executing its target package. */
|
|
286
|
-
async function resolveImportPathAsync(
|
|
287
|
-
packageRoot: string,
|
|
288
|
-
fromPath: string,
|
|
289
|
-
specifier: string,
|
|
290
|
-
): Promise<ResolvedStaticImport> {
|
|
291
|
-
if (specifier.startsWith('.'))
|
|
292
|
-
return resolveLocalImportPathAsync(packageRoot, fromPath, specifier);
|
|
293
|
-
return resolveContractsCatalogImportAsync(packageRoot, fromPath, specifier);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/*** Resolve a relative static import that remains within its owning package. */
|
|
297
|
-
async function resolveLocalImportPathAsync(
|
|
298
|
-
packageRoot: string,
|
|
299
|
-
fromPath: string,
|
|
300
|
-
specifier: string,
|
|
301
|
-
): Promise<ResolvedStaticImport> {
|
|
302
|
-
const candidate = path.resolve(path.dirname(fromPath), specifier);
|
|
303
|
-
if (!isInsidePackage(packageRoot, candidate) && candidate !== packageRoot)
|
|
304
|
-
throw new Error('Capability catalogs may not import outside their package.');
|
|
305
|
-
for (const extension of ['', '.ts', '.tsx', '.js', '.mjs', '/index.ts'])
|
|
306
|
-
if (await isFileAsync(`${candidate}${extension}`))
|
|
307
|
-
return { packageRoot, path: `${candidate}${extension}` };
|
|
308
|
-
throw new Error(`Unable to resolve static capability import ${specifier}.`);
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
/*** Unwrap TypeScript syntax which does not alter a runtime value. */
|
|
312
|
-
function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
313
|
-
return ts.isAsExpression(expression) ||
|
|
314
|
-
ts.isSatisfiesExpression(expression) ||
|
|
315
|
-
ts.isParenthesizedExpression(expression)
|
|
316
|
-
? unwrapExpression(expression.expression)
|
|
317
|
-
: expression;
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
/*** Narrow a static value to a descriptor-shaped record. */
|
|
321
|
-
function isStaticRecord(value: StaticValue): value is StaticRecord {
|
|
322
|
-
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
/*** Narrow a static value to an array. */
|
|
326
|
-
function isStaticArray(value: StaticValue): value is readonly StaticValue[] {
|
|
327
|
-
return Array.isArray(value);
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
/*** Detect an exported variable statement. */
|
|
331
|
-
function hasExportModifier(statement: ts.VariableStatement): boolean {
|
|
332
|
-
return (
|
|
333
|
-
statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
/*** Check whether a resolved path remains within the inspected package root. */
|
|
338
|
-
function isInsidePackage(packageRoot: string, candidate: string): boolean {
|
|
339
|
-
const relativePath = path.relative(packageRoot, candidate);
|
|
340
|
-
return relativePath !== '' && !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..';
|
|
341
|
-
}
|
|
342
|
-
|
|
343
|
-
/*** Check whether one candidate is a regular file. */
|
|
344
|
-
async function isFileAsync(candidate: string): Promise<boolean> {
|
|
345
|
-
try {
|
|
346
|
-
return (await fs.stat(candidate)).isFile();
|
|
347
|
-
} catch {
|
|
348
|
-
return false;
|
|
349
|
-
}
|
|
350
|
-
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import * as ts from 'typescript';
|
|
2
|
+
|
|
3
|
+
/*** Find the static top-level declaration that owns a local identifier. */
|
|
4
|
+
export function findStaticDeclaration(
|
|
5
|
+
source: ts.SourceFile,
|
|
6
|
+
name: string,
|
|
7
|
+
): ts.VariableDeclaration | undefined {
|
|
8
|
+
return source.statements
|
|
9
|
+
.filter((statement): statement is ts.VariableStatement => ts.isVariableStatement(statement))
|
|
10
|
+
.filter((statement) => isConstDeclaration(statement.declarationList))
|
|
11
|
+
.flatMap((statement) => statement.declarationList.declarations)
|
|
12
|
+
.find((declaration) => ts.isIdentifier(declaration.name) && declaration.name.text === name);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/*** Find the import declaration that introduces a referenced local identifier. */
|
|
16
|
+
export function findImportBinding(
|
|
17
|
+
source: ts.SourceFile,
|
|
18
|
+
name: string,
|
|
19
|
+
): ts.ImportDeclaration | undefined {
|
|
20
|
+
return source.statements
|
|
21
|
+
.filter((statement): statement is ts.ImportDeclaration => ts.isImportDeclaration(statement))
|
|
22
|
+
.find((statement) => {
|
|
23
|
+
const clause = statement.importClause;
|
|
24
|
+
return (
|
|
25
|
+
clause?.name?.text === name ||
|
|
26
|
+
(clause?.namedBindings !== undefined &&
|
|
27
|
+
ts.isNamedImports(clause.namedBindings) &&
|
|
28
|
+
clause.namedBindings.elements.some((element) => element.name.text === name))
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/*** Determine whether a declaration list is immutable static catalog input. */
|
|
34
|
+
export function isConstDeclaration(list: ts.VariableDeclarationList): boolean {
|
|
35
|
+
return (list.flags & ts.NodeFlags.Const) !== 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/*** Unwrap TypeScript syntax which does not alter a runtime value. */
|
|
39
|
+
export function unwrapExpression(expression: ts.Expression): ts.Expression {
|
|
40
|
+
return ts.isAsExpression(expression) ||
|
|
41
|
+
ts.isSatisfiesExpression(expression) ||
|
|
42
|
+
ts.isParenthesizedExpression(expression)
|
|
43
|
+
? unwrapExpression(expression.expression)
|
|
44
|
+
: expression;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/*** Detect an exported variable statement. */
|
|
48
|
+
export function hasExportModifier(statement: ts.VariableStatement): boolean {
|
|
49
|
+
return (
|
|
50
|
+
statement.modifiers?.some((modifier) => modifier.kind === ts.SyntaxKind.ExportKeyword) ?? false
|
|
51
|
+
);
|
|
52
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { promises as fs } from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
|
|
4
|
+
import { resolveContractsCatalogImportAsync } from './capabilityCatalogContractsImport.js';
|
|
5
|
+
|
|
6
|
+
export interface ResolvedStaticImport {
|
|
7
|
+
readonly packageRoot: string;
|
|
8
|
+
readonly path: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/*** Resolve an allowed static import without executing its target package. */
|
|
12
|
+
export async function resolveImportPathAsync(
|
|
13
|
+
packageRoot: string,
|
|
14
|
+
fromPath: string,
|
|
15
|
+
specifier: string,
|
|
16
|
+
): Promise<ResolvedStaticImport> {
|
|
17
|
+
if (specifier.startsWith('.'))
|
|
18
|
+
return resolveLocalImportPathAsync(packageRoot, fromPath, specifier);
|
|
19
|
+
return resolveContractsCatalogImportAsync(packageRoot, fromPath, specifier);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/*** Resolve a relative static import that remains within its owning package. */
|
|
23
|
+
export async function resolveLocalImportPathAsync(
|
|
24
|
+
packageRoot: string,
|
|
25
|
+
fromPath: string,
|
|
26
|
+
specifier: string,
|
|
27
|
+
): Promise<ResolvedStaticImport> {
|
|
28
|
+
const candidate = path.resolve(path.dirname(fromPath), specifier);
|
|
29
|
+
if (!isInsidePackage(packageRoot, candidate) && candidate !== packageRoot)
|
|
30
|
+
throw new Error('Capability catalogs may not import outside their package.');
|
|
31
|
+
for (const sourcePath of localSourceCandidates(candidate))
|
|
32
|
+
if (await isFileAsync(sourcePath)) return { packageRoot, path: sourcePath };
|
|
33
|
+
throw new Error(`Unable to resolve static capability import ${specifier}.`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*** Enumerate source files represented by one package-local ESM import specifier. */
|
|
37
|
+
function localSourceCandidates(candidate: string): readonly string[] {
|
|
38
|
+
const extension = path.extname(candidate);
|
|
39
|
+
if (extension === '.js')
|
|
40
|
+
return [candidate.replace(/\.js$/u, '.ts'), candidate.replace(/\.js$/u, '.tsx'), candidate];
|
|
41
|
+
if (extension === '.mjs') return [candidate.replace(/\.mjs$/u, '.mts'), candidate];
|
|
42
|
+
if (extension === '.cjs') return [candidate.replace(/\.cjs$/u, '.cts'), candidate];
|
|
43
|
+
return [
|
|
44
|
+
candidate,
|
|
45
|
+
`${candidate}.ts`,
|
|
46
|
+
`${candidate}.tsx`,
|
|
47
|
+
`${candidate}.js`,
|
|
48
|
+
`${candidate}.mjs`,
|
|
49
|
+
`${candidate}.cjs`,
|
|
50
|
+
`${candidate}/index.ts`,
|
|
51
|
+
`${candidate}/index.tsx`,
|
|
52
|
+
`${candidate}/index.js`,
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/*** Check whether a resolved path remains within the inspected package root. */
|
|
57
|
+
function isInsidePackage(packageRoot: string, candidate: string): boolean {
|
|
58
|
+
const relativePath = path.relative(packageRoot, candidate);
|
|
59
|
+
return relativePath !== '' && !relativePath.startsWith(`..${path.sep}`) && relativePath !== '..';
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/*** Check whether one candidate is a regular file. */
|
|
63
|
+
async function isFileAsync(candidate: string): Promise<boolean> {
|
|
64
|
+
try {
|
|
65
|
+
return (await fs.stat(candidate)).isFile();
|
|
66
|
+
} catch {
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type StaticValue = string | number | boolean | null | readonly StaticValue[] | StaticRecord;
|
|
2
|
+
|
|
3
|
+
export interface StaticRecord {
|
|
4
|
+
readonly [key: string]: StaticValue;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
/*** Narrow a static value to a descriptor-shaped record. */
|
|
8
|
+
export function isStaticRecord(value: StaticValue): value is StaticRecord {
|
|
9
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/*** Narrow a static value to an array. */
|
|
13
|
+
export function isStaticArray(value: StaticValue): value is readonly StaticValue[] {
|
|
14
|
+
return Array.isArray(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/*** Read one property from a statically resolved object. */
|
|
18
|
+
export function readStaticProperty(
|
|
19
|
+
value: StaticValue,
|
|
20
|
+
key: string,
|
|
21
|
+
expression: string,
|
|
22
|
+
): StaticValue {
|
|
23
|
+
const property = isStaticRecord(value)
|
|
24
|
+
? Object.entries(value).find(([propertyKey]) => propertyKey === key)?.[1]
|
|
25
|
+
: undefined;
|
|
26
|
+
if (property === undefined)
|
|
27
|
+
throw new Error(`Unsupported static capability property access: ${expression}.`);
|
|
28
|
+
return property;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/*** Read one JSON-compatible array as static catalog materialization input. */
|
|
32
|
+
export function readStaticArray(value: unknown): readonly StaticValue[] | null {
|
|
33
|
+
return Array.isArray(value) && value.every(isStaticValue) ? value : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/*** Narrow untrusted JSON-compatible input to the static evaluator value model. */
|
|
37
|
+
function isStaticValue(value: unknown): value is StaticValue {
|
|
38
|
+
if (
|
|
39
|
+
value === null ||
|
|
40
|
+
typeof value === 'string' ||
|
|
41
|
+
typeof value === 'number' ||
|
|
42
|
+
typeof value === 'boolean'
|
|
43
|
+
)
|
|
44
|
+
return true;
|
|
45
|
+
if (Array.isArray(value)) return value.every(isStaticValue);
|
|
46
|
+
if (typeof value !== 'object') return false;
|
|
47
|
+
return Object.values(value).every(isStaticValue);
|
|
48
|
+
}
|