@rytass/bpm-core-nestjs-module 0.19.0 → 0.20.1
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 +55 -0
- package/package.json +3 -2
- package/src/index.d.ts +1 -0
- package/src/index.js +1 -0
- package/src/index.js.map +1 -1
- package/src/lib/bpm/bpm-root-options.d.ts +13 -0
- package/src/lib/bpm/bpm-root-options.js.map +1 -1
- package/src/lib/bpm/bpm-root.module.d.ts +12 -0
- package/src/lib/bpm/bpm-root.module.js +5 -0
- package/src/lib/bpm/bpm-root.module.js.map +1 -1
- package/src/lib/condition/adapters/cel-legacy.adapter.d.ts +62 -0
- package/src/lib/condition/adapters/cel-legacy.adapter.js +141 -0
- package/src/lib/condition/adapters/cel-legacy.adapter.js.map +1 -0
- package/src/lib/condition/adapters/cel-v2.adapter.d.ts +76 -0
- package/src/lib/condition/adapters/cel-v2.adapter.js +371 -0
- package/src/lib/condition/adapters/cel-v2.adapter.js.map +1 -0
- package/src/lib/condition/condition.service.d.ts +90 -5
- package/src/lib/condition/condition.service.js +287 -71
- package/src/lib/condition/condition.service.js.map +1 -1
- package/src/lib/condition/expression-ast.d.ts +176 -0
- package/src/lib/condition/expression-ast.js +1254 -0
- package/src/lib/condition/expression-ast.js.map +1 -0
- package/src/lib/condition/expression-budget.d.ts +44 -0
- package/src/lib/condition/expression-budget.js +61 -0
- package/src/lib/condition/expression-budget.js.map +1 -0
- package/src/lib/condition/expression-dialect.d.ts +29 -0
- package/src/lib/condition/expression-dialect.js +44 -0
- package/src/lib/condition/expression-dialect.js.map +1 -0
- package/src/lib/condition/expression-differential.corpus.d.ts +34 -0
- package/src/lib/condition/expression-differential.corpus.js +995 -0
- package/src/lib/condition/expression-differential.corpus.js.map +1 -0
- package/src/lib/condition/expression-differential.d.ts +33 -0
- package/src/lib/condition/expression-differential.js +48 -0
- package/src/lib/condition/expression-differential.js.map +1 -0
- package/src/lib/condition/expression-engine-failure.d.ts +15 -0
- package/src/lib/condition/expression-engine-failure.js +22 -0
- package/src/lib/condition/expression-engine-failure.js.map +1 -0
- package/src/lib/condition/expression-errors.d.ts +40 -0
- package/src/lib/condition/expression-errors.js +121 -0
- package/src/lib/condition/expression-errors.js.map +1 -0
- package/src/lib/condition/expression-function-invocation.d.ts +50 -0
- package/src/lib/condition/expression-function-invocation.js +290 -0
- package/src/lib/condition/expression-function-invocation.js.map +1 -0
- package/src/lib/condition/expression-result.walker.d.ts +18 -0
- package/src/lib/condition/expression-result.walker.js +152 -0
- package/src/lib/condition/expression-result.walker.js.map +1 -0
- package/src/lib/condition/expression-runtime.d.ts +70 -0
- package/src/lib/condition/expression-runtime.js +134 -0
- package/src/lib/condition/expression-runtime.js.map +1 -0
- package/src/lib/condition/index.d.ts +4 -0
- package/src/lib/condition/index.js +8 -0
- package/src/lib/condition/index.js.map +1 -1
- package/src/lib/delegation/delegation-rule.entity.d.ts +7 -0
- package/src/lib/delegation/delegation-rule.entity.js +10 -0
- package/src/lib/delegation/delegation-rule.entity.js.map +1 -1
- package/src/lib/delegation/delegation.service.d.ts +7 -1
- package/src/lib/delegation/delegation.service.js +22 -5
- package/src/lib/delegation/delegation.service.js.map +1 -1
- package/src/lib/delegation/dto/delegation-rule.input.d.ts +2 -0
- package/src/lib/delegation/dto/delegation-rule.input.js +10 -0
- package/src/lib/delegation/dto/delegation-rule.input.js.map +1 -1
- package/src/lib/expression-function/expression-function.built-ins.d.ts +9 -0
- package/src/lib/expression-function/expression-function.built-ins.js +117 -0
- package/src/lib/expression-function/expression-function.built-ins.js.map +1 -0
- package/src/lib/expression-function/expression-function.module.d.ts +19 -0
- package/src/lib/expression-function/expression-function.module.js +34 -0
- package/src/lib/expression-function/expression-function.module.js.map +1 -0
- package/src/lib/expression-function/expression-function.provider.d.ts +17 -0
- package/src/lib/expression-function/expression-function.provider.js +39 -0
- package/src/lib/expression-function/expression-function.provider.js.map +1 -0
- package/src/lib/expression-function/expression-function.registry.d.ts +36 -0
- package/src/lib/expression-function/expression-function.registry.js +231 -0
- package/src/lib/expression-function/expression-function.registry.js.map +1 -0
- package/src/lib/expression-function/expression-function.types.d.ts +30 -0
- package/src/lib/expression-function/expression-function.types.js +8 -0
- package/src/lib/expression-function/expression-function.types.js.map +1 -0
- package/src/lib/expression-function/index.d.ts +5 -0
- package/src/lib/expression-function/index.js +9 -0
- package/src/lib/expression-function/index.js.map +1 -0
- package/src/lib/form/form-schema.validator.js +53 -6
- package/src/lib/form/form-schema.validator.js.map +1 -1
- package/src/lib/form-data-source/form-data-source-value-resolver.service.d.ts +12 -1
- package/src/lib/form-data-source/form-data-source-value-resolver.service.js +45 -26
- package/src/lib/form-data-source/form-data-source-value-resolver.service.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.expression.d.ts +95 -0
- package/src/lib/form-data-source/form-data-source.expression.js +267 -0
- package/src/lib/form-data-source/form-data-source.expression.js.map +1 -0
- package/src/lib/form-data-source/form-data-source.module.js +4 -0
- package/src/lib/form-data-source/form-data-source.module.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.service.d.ts +22 -3
- package/src/lib/form-data-source/form-data-source.service.js +78 -18
- package/src/lib/form-data-source/form-data-source.service.js.map +1 -1
- package/src/lib/form-data-source/form-data-source.validation.d.ts +47 -4
- package/src/lib/form-data-source/form-data-source.validation.js +151 -23
- package/src/lib/form-data-source/form-data-source.validation.js.map +1 -1
- package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.d.ts +22 -0
- package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.js +76 -0
- package/src/lib/migrations/0000000030000-delegation-scope-condition-dialect.js.map +1 -0
- package/src/lib/migrations/index.d.ts +1 -0
- package/src/lib/migrations/index.js +3 -0
- package/src/lib/migrations/index.js.map +1 -1
- package/src/lib/notification/notification.service.d.ts +1 -1
- package/src/lib/notification/notification.service.js +7 -3
- package/src/lib/notification/notification.service.js.map +1 -1
- package/src/lib/template/dto/expression-designer.dto.d.ts +107 -0
- package/src/lib/template/dto/expression-designer.dto.js +415 -0
- package/src/lib/template/dto/expression-designer.dto.js.map +1 -0
- package/src/lib/template/expression-designer.queries.d.ts +16 -0
- package/src/lib/template/expression-designer.queries.js +66 -0
- package/src/lib/template/expression-designer.queries.js.map +1 -0
- package/src/lib/template/expression-designer.service.d.ts +72 -0
- package/src/lib/template/expression-designer.service.js +543 -0
- package/src/lib/template/expression-designer.service.js.map +1 -0
- package/src/lib/template/index.d.ts +2 -0
- package/src/lib/template/index.js +2 -0
- package/src/lib/template/index.js.map +1 -1
- package/src/lib/template/template.module.js +4 -0
- package/src/lib/template/template.module.js.map +1 -1
- package/src/lib/template/template.service.d.ts +37 -1
- package/src/lib/template/template.service.js +317 -29
- package/src/lib/template/template.service.js.map +1 -1
- package/src/lib/template/workflow-definition.validator.d.ts +7 -0
- package/src/lib/template/workflow-definition.validator.js +26 -2
- package/src/lib/template/workflow-definition.validator.js.map +1 -1
- package/src/lib/workflow-engine/time-budget.d.ts +13 -0
- package/src/lib/workflow-engine/time-budget.js +58 -0
- package/src/lib/workflow-engine/time-budget.js.map +1 -0
- package/src/lib/workflow-engine/workflow-condition-evaluator.js +12 -12
- package/src/lib/workflow-engine/workflow-condition-evaluator.js.map +1 -1
- package/src/lib/workflow-engine/workflow-engine.service.d.ts +24 -0
- package/src/lib/workflow-engine/workflow-engine.service.js +311 -88
- package/src/lib/workflow-engine/workflow-engine.service.js.map +1 -1
- package/src/lib/workflow-engine/workflow-form-value.d.ts +11 -0
- package/src/lib/workflow-engine/workflow-form-value.js +33 -0
- package/src/lib/workflow-engine/workflow-form-value.js.map +1 -0
- package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js +4 -0
- package/src/lib/workflow-engine/workflow-service-task-dispatcher.token.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook-delivery.service.d.ts +3 -1
- package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js +11 -2
- package/src/lib/workflow-webhook/workflow-webhook-delivery.service.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook-enqueue.d.ts +8 -1
- package/src/lib/workflow-webhook/workflow-webhook-enqueue.js +70 -27
- package/src/lib/workflow-webhook/workflow-webhook-enqueue.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook-expression.d.ts +57 -0
- package/src/lib/workflow-webhook/workflow-webhook-expression.js +123 -0
- package/src/lib/workflow-webhook/workflow-webhook-expression.js.map +1 -0
- package/src/lib/workflow-webhook/workflow-webhook.module.js +4 -0
- package/src/lib/workflow-webhook/workflow-webhook.module.js.map +1 -1
- package/src/lib/workflow-webhook/workflow-webhook.validator.d.ts +7 -0
- package/src/lib/workflow-webhook/workflow-webhook.validator.js +29 -6
- package/src/lib/workflow-webhook/workflow-webhook.validator.js.map +1 -1
|
@@ -0,0 +1,1254 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readExpressionFieldType = exports.readsTableInternals = exports.readsFormField = exports.readFormFieldKeys = exports.EXPRESSION_MAX_COMPREHENSION_DEPTH = exports.RESERVED_PATH_SEGMENTS = void 0;
|
|
4
|
+
exports.readExpressionReferences = readExpressionReferences;
|
|
5
|
+
exports.readUnavailableExpressionFunctions = readUnavailableExpressionFunctions;
|
|
6
|
+
exports.lintCelV2Expression = lintCelV2Expression;
|
|
7
|
+
exports.readExpressionHostFunctionNames = readExpressionHostFunctionNames;
|
|
8
|
+
const common_1 = require("@nestjs/common");
|
|
9
|
+
const bpm_core_shared_1 = require("@rytass/bpm-core-shared");
|
|
10
|
+
const form_1 = require("@rytass/bpm-core-shared/form");
|
|
11
|
+
const expression_function_registry_1 = require("../expression-function/expression-function.registry");
|
|
12
|
+
const cel_legacy_adapter_1 = require("./adapters/cel-legacy.adapter");
|
|
13
|
+
const cel_v2_adapter_1 = require("./adapters/cel-v2.adapter");
|
|
14
|
+
const expression_function_invocation_1 = require("./expression-function-invocation");
|
|
15
|
+
const expression_dialect_1 = require("./expression-dialect");
|
|
16
|
+
exports.RESERVED_PATH_SEGMENTS = new Set([
|
|
17
|
+
'__proto__',
|
|
18
|
+
'constructor',
|
|
19
|
+
'prototype',
|
|
20
|
+
]);
|
|
21
|
+
/** Roots the lint refuses to see used as a whole value (ADR 22 §3.4). */
|
|
22
|
+
const WHOLE_REFERENCE_ROOTS = new Set([
|
|
23
|
+
'form',
|
|
24
|
+
'formData',
|
|
25
|
+
'initiator',
|
|
26
|
+
'row',
|
|
27
|
+
]);
|
|
28
|
+
const FORM_ROOTS = new Set(['form', 'formData']);
|
|
29
|
+
const COMPREHENSION_MACROS = new Set([
|
|
30
|
+
'all',
|
|
31
|
+
'exists',
|
|
32
|
+
'exists_one',
|
|
33
|
+
'filter',
|
|
34
|
+
'map',
|
|
35
|
+
]);
|
|
36
|
+
/** ADR 22 §3.4 / docs/24 §2.7. */
|
|
37
|
+
exports.EXPRESSION_MAX_COMPREHENSION_DEPTH = 2;
|
|
38
|
+
const createCollector = () => ({
|
|
39
|
+
comprehensionDepth: 0,
|
|
40
|
+
computedKeys: [],
|
|
41
|
+
divisorFieldPaths: [],
|
|
42
|
+
fieldPaths: [],
|
|
43
|
+
functionCalls: [],
|
|
44
|
+
guardedFieldPaths: [],
|
|
45
|
+
hasArguments: [],
|
|
46
|
+
nodeCount: 0,
|
|
47
|
+
orOperandFieldPaths: [],
|
|
48
|
+
receiverPaths: [],
|
|
49
|
+
reservedSegments: new Set(),
|
|
50
|
+
roots: new Set(),
|
|
51
|
+
wholeRootReferences: new Set(),
|
|
52
|
+
});
|
|
53
|
+
const toReferences = (collector, dialect) => ({
|
|
54
|
+
comprehensionDepth: collector.comprehensionDepth,
|
|
55
|
+
computedKeys: [...collector.computedKeys],
|
|
56
|
+
dialect,
|
|
57
|
+
divisorFieldPaths: [...collector.divisorFieldPaths],
|
|
58
|
+
fieldPaths: [...collector.fieldPaths],
|
|
59
|
+
formFieldKeys: (0, exports.readFormFieldKeys)(collector.fieldPaths),
|
|
60
|
+
functionCalls: [...collector.functionCalls],
|
|
61
|
+
guardedFieldPaths: [...collector.guardedFieldPaths],
|
|
62
|
+
hasArguments: [...collector.hasArguments],
|
|
63
|
+
nodeCount: collector.nodeCount,
|
|
64
|
+
orOperandFieldPaths: [...collector.orOperandFieldPaths],
|
|
65
|
+
receiverPaths: [...collector.receiverPaths],
|
|
66
|
+
reservedSegments: [...collector.reservedSegments].sort(),
|
|
67
|
+
roots: [...collector.roots].sort(),
|
|
68
|
+
wholeRootReferences: [...collector.wholeRootReferences].sort(),
|
|
69
|
+
});
|
|
70
|
+
/** Unique top-level keys of the paths rooted at `form` or `formData`, in first-seen order. */
|
|
71
|
+
const readFormFieldKeys = (paths) => [
|
|
72
|
+
...new Set(paths
|
|
73
|
+
.filter((path) => FORM_ROOTS.has(path.root) && path.segments.length > 0)
|
|
74
|
+
.map((path) => path.segments[0])),
|
|
75
|
+
];
|
|
76
|
+
exports.readFormFieldKeys = readFormFieldKeys;
|
|
77
|
+
const noteReceiver = (collector, name, path) => {
|
|
78
|
+
if (path !== null) {
|
|
79
|
+
collector.receiverPaths.push({ name, path });
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
const noteReserved = (collector, segment) => {
|
|
83
|
+
if (exports.RESERVED_PATH_SEGMENTS.has(segment)) {
|
|
84
|
+
collector.reservedSegments.add(segment);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* The roots, field paths, function calls and structure an expression uses,
|
|
89
|
+
* read from its AST without evaluating it. Names a comprehension binds (`x`
|
|
90
|
+
* in `form.items.exists(x, x.qty > 0)`) are only in scope inside it: the same
|
|
91
|
+
* name outside it is a root.
|
|
92
|
+
*
|
|
93
|
+
* Works for both dialects. cel-v2 reads the engine's AST; cel-legacy walks the
|
|
94
|
+
* CST of `cel-js` and answers `UNAVAILABLE` when there is none to walk.
|
|
95
|
+
*/
|
|
96
|
+
function readExpressionReferences(expression, dialect = expression_dialect_1.DEFAULT_EXPRESSION_DIALECT) {
|
|
97
|
+
return dialect === 'cel-v2'
|
|
98
|
+
? readCelV2References(expression)
|
|
99
|
+
: readCelLegacyReferences(expression);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Parses with the shared cel-v2 environment. The engine applies the AST node
|
|
103
|
+
* and depth limits while parsing; breaking one is a `limit_exceeded` error,
|
|
104
|
+
* told apart here so the lint can call it complexity rather than syntax. The
|
|
105
|
+
* error's message is never read: it can echo the expression.
|
|
106
|
+
*/
|
|
107
|
+
const parseCelV2Ast = (expression, environment = (0, cel_v2_adapter_1.loadCelV2Environment)()) => {
|
|
108
|
+
try {
|
|
109
|
+
return { ast: environment.parse(expression).ast, status: 'OK' };
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
return {
|
|
113
|
+
limitExceeded: typeof error === 'object' &&
|
|
114
|
+
error !== null &&
|
|
115
|
+
error.code === 'limit_exceeded',
|
|
116
|
+
status: 'PARSE_FAILED',
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
function readCelV2References(expression, environment) {
|
|
121
|
+
const parsed = parseCelV2Ast(expression, environment);
|
|
122
|
+
if (parsed.status !== 'OK') {
|
|
123
|
+
return parsed;
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
references: collectCelV2References(parsed.ast),
|
|
127
|
+
status: 'OK',
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
const collectCelV2References = (ast) => {
|
|
131
|
+
const collector = createCollector();
|
|
132
|
+
visitCelV2(ast, new Set(), 0, collector);
|
|
133
|
+
return toReferences(collector, 'cel-v2');
|
|
134
|
+
};
|
|
135
|
+
const readCelV2Chain = (node) => {
|
|
136
|
+
if (node.op === '.' || node.op === '.?') {
|
|
137
|
+
const inner = readCelV2Chain(node.args[0]);
|
|
138
|
+
return {
|
|
139
|
+
base: inner.base,
|
|
140
|
+
steps: [...inner.steps, { kind: 'member', name: node.args[1] }],
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
if (node.op === '[]' || node.op === '[?]') {
|
|
144
|
+
const inner = readCelV2Chain(node.args[0]);
|
|
145
|
+
return {
|
|
146
|
+
base: inner.base,
|
|
147
|
+
steps: [...inner.steps, { index: node.args[1], kind: 'index' }],
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
return { base: node, steps: [] };
|
|
151
|
+
};
|
|
152
|
+
/** A literal index or key as a path segment, or `null` when it is computed. */
|
|
153
|
+
const readCelV2LiteralKey = (node) => {
|
|
154
|
+
if (node.op !== 'value') {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
if (typeof node.args === 'string') {
|
|
158
|
+
return { key: node.args, string: true };
|
|
159
|
+
}
|
|
160
|
+
return typeof node.args === 'bigint' || typeof node.args === 'number'
|
|
161
|
+
? { key: String(node.args), string: false }
|
|
162
|
+
: null;
|
|
163
|
+
};
|
|
164
|
+
const isCelV2EmptyLiteral = (node) => (node.op === 'value' && (node.args === null || node.args === '')) ||
|
|
165
|
+
(node.op === 'list' && node.args.length === 0);
|
|
166
|
+
const visitCelV2Access = (node, scope, depth, collector) => {
|
|
167
|
+
const { base, steps } = readCelV2Chain(node);
|
|
168
|
+
const rootName = base.op === 'id' && !scope.has(base.args) ? base.args : null;
|
|
169
|
+
collector.nodeCount += steps.length;
|
|
170
|
+
if (base.op === 'id') {
|
|
171
|
+
collector.nodeCount += 1;
|
|
172
|
+
if (rootName !== null) {
|
|
173
|
+
collector.roots.add(rootName);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
else {
|
|
177
|
+
visitCelV2(base, scope, depth, collector);
|
|
178
|
+
}
|
|
179
|
+
const { complete, segments } = steps.reduce((state, step) => {
|
|
180
|
+
if (step.kind === 'member') {
|
|
181
|
+
noteReserved(collector, step.name);
|
|
182
|
+
return state.complete
|
|
183
|
+
? { complete: true, segments: [...state.segments, step.name] }
|
|
184
|
+
: state;
|
|
185
|
+
}
|
|
186
|
+
const literal = readCelV2LiteralKey(step.index);
|
|
187
|
+
if (literal !== null) {
|
|
188
|
+
collector.nodeCount += 1;
|
|
189
|
+
if (literal.string) {
|
|
190
|
+
noteReserved(collector, literal.key);
|
|
191
|
+
}
|
|
192
|
+
return state.complete
|
|
193
|
+
? { complete: true, segments: [...state.segments, literal.key] }
|
|
194
|
+
: state;
|
|
195
|
+
}
|
|
196
|
+
collector.computedKeys.push({ kind: 'INDEX', root: rootName });
|
|
197
|
+
visitCelV2(step.index, scope, depth, collector);
|
|
198
|
+
return { complete: false, segments: state.segments };
|
|
199
|
+
}, { complete: true, segments: [] });
|
|
200
|
+
if (rootName === null) {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
const path = { complete, root: rootName, segments };
|
|
204
|
+
collector.fieldPaths.push(path);
|
|
205
|
+
return path;
|
|
206
|
+
};
|
|
207
|
+
const visitCelV2Children = (nodes, scope, depth, collector) => {
|
|
208
|
+
nodes.forEach((child) => {
|
|
209
|
+
visitCelV2(child, scope, depth, collector);
|
|
210
|
+
});
|
|
211
|
+
};
|
|
212
|
+
/**
|
|
213
|
+
* Visits one node. Returns the field path when the node is a path from a
|
|
214
|
+
* root, so a caller can tell what `has(...)` or `== null` is about.
|
|
215
|
+
*/
|
|
216
|
+
function visitCelV2(node, scope, depth, collector) {
|
|
217
|
+
switch (node.op) {
|
|
218
|
+
case 'value':
|
|
219
|
+
collector.nodeCount += 1;
|
|
220
|
+
return null;
|
|
221
|
+
case 'id':
|
|
222
|
+
collector.nodeCount += 1;
|
|
223
|
+
if (!scope.has(node.args)) {
|
|
224
|
+
collector.roots.add(node.args);
|
|
225
|
+
collector.wholeRootReferences.add(node.args);
|
|
226
|
+
}
|
|
227
|
+
return null;
|
|
228
|
+
case '.':
|
|
229
|
+
case '.?':
|
|
230
|
+
case '[]':
|
|
231
|
+
case '[?]':
|
|
232
|
+
return visitCelV2Access(node, scope, depth, collector);
|
|
233
|
+
case 'call': {
|
|
234
|
+
const [name, args] = node.args;
|
|
235
|
+
collector.nodeCount += 1;
|
|
236
|
+
if (name === 'has' && args.length === 1) {
|
|
237
|
+
const path = visitCelV2(args[0], scope, depth, collector);
|
|
238
|
+
if (path !== null) {
|
|
239
|
+
collector.hasArguments.push(path);
|
|
240
|
+
collector.guardedFieldPaths.push(path);
|
|
241
|
+
}
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
collector.functionCalls.push({
|
|
245
|
+
argumentCount: args.length,
|
|
246
|
+
name,
|
|
247
|
+
receiver: false,
|
|
248
|
+
});
|
|
249
|
+
visitCelV2Children(args, scope, depth, collector);
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
case 'rcall': {
|
|
253
|
+
const [name, receiver, args] = node.args;
|
|
254
|
+
const bound = args[0];
|
|
255
|
+
collector.nodeCount += 1;
|
|
256
|
+
if (COMPREHENSION_MACROS.has(name) &&
|
|
257
|
+
bound?.op === 'id' &&
|
|
258
|
+
(args.length === 2 || args.length === 3)) {
|
|
259
|
+
const innerDepth = depth + 1;
|
|
260
|
+
collector.comprehensionDepth = Math.max(collector.comprehensionDepth, innerDepth);
|
|
261
|
+
collector.nodeCount += 1;
|
|
262
|
+
noteReceiver(collector, name, visitCelV2(receiver, scope, depth, collector));
|
|
263
|
+
visitCelV2Children(args.slice(1), new Set([...scope, bound.args]), innerDepth, collector);
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
// `cel.bind(v, init, body)` is deliberately not a scope here: the
|
|
267
|
+
// evaluation guard (`ConditionService`) refuses `cel` as a root, so the
|
|
268
|
+
// lint reports `cel` and `v` as roots too rather than pass what every
|
|
269
|
+
// case would then fail on.
|
|
270
|
+
collector.functionCalls.push({
|
|
271
|
+
argumentCount: args.length,
|
|
272
|
+
name,
|
|
273
|
+
receiver: true,
|
|
274
|
+
});
|
|
275
|
+
noteReceiver(collector, name, visitCelV2(receiver, scope, depth, collector));
|
|
276
|
+
visitCelV2Children(args, scope, depth, collector);
|
|
277
|
+
return null;
|
|
278
|
+
}
|
|
279
|
+
case 'list':
|
|
280
|
+
collector.nodeCount += 1;
|
|
281
|
+
visitCelV2Children(node.args, scope, depth, collector);
|
|
282
|
+
return null;
|
|
283
|
+
case 'map':
|
|
284
|
+
collector.nodeCount += 1;
|
|
285
|
+
node.args.forEach(([key, value]) => {
|
|
286
|
+
if (key.op === 'value') {
|
|
287
|
+
collector.nodeCount += 1;
|
|
288
|
+
if (typeof key.args === 'string') {
|
|
289
|
+
noteReserved(collector, key.args);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
else {
|
|
293
|
+
collector.computedKeys.push({ kind: 'MAP_KEY', root: null });
|
|
294
|
+
visitCelV2(key, scope, depth, collector);
|
|
295
|
+
}
|
|
296
|
+
visitCelV2(value, scope, depth, collector);
|
|
297
|
+
});
|
|
298
|
+
return null;
|
|
299
|
+
case '||': {
|
|
300
|
+
const start = collector.fieldPaths.length;
|
|
301
|
+
collector.nodeCount += 1;
|
|
302
|
+
visitCelV2Children(node.args, scope, depth, collector);
|
|
303
|
+
collector.orOperandFieldPaths.push(...collector.fieldPaths.slice(start));
|
|
304
|
+
return null;
|
|
305
|
+
}
|
|
306
|
+
case '/': {
|
|
307
|
+
collector.nodeCount += 1;
|
|
308
|
+
visitCelV2(node.args[0], scope, depth, collector);
|
|
309
|
+
const start = collector.fieldPaths.length;
|
|
310
|
+
visitCelV2(node.args[1], scope, depth, collector);
|
|
311
|
+
collector.divisorFieldPaths.push(...collector.fieldPaths.slice(start));
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
case '==':
|
|
315
|
+
case '!=': {
|
|
316
|
+
const [left, right] = node.args;
|
|
317
|
+
collector.nodeCount += 1;
|
|
318
|
+
const leftPath = visitCelV2(left, scope, depth, collector);
|
|
319
|
+
const rightPath = visitCelV2(right, scope, depth, collector);
|
|
320
|
+
if (leftPath !== null && isCelV2EmptyLiteral(right)) {
|
|
321
|
+
collector.guardedFieldPaths.push(leftPath);
|
|
322
|
+
}
|
|
323
|
+
if (rightPath !== null && isCelV2EmptyLiteral(left)) {
|
|
324
|
+
collector.guardedFieldPaths.push(rightPath);
|
|
325
|
+
}
|
|
326
|
+
return null;
|
|
327
|
+
}
|
|
328
|
+
case '!_':
|
|
329
|
+
case '-_':
|
|
330
|
+
collector.nodeCount += 1;
|
|
331
|
+
visitCelV2(node.args, scope, depth, collector);
|
|
332
|
+
return null;
|
|
333
|
+
case '?:':
|
|
334
|
+
collector.nodeCount += 1;
|
|
335
|
+
visitCelV2Children(node.args, scope, depth, collector);
|
|
336
|
+
return null;
|
|
337
|
+
default:
|
|
338
|
+
// The remaining operators are binary: `&&`, `in`, `+ - * %`, `< <= > >=`.
|
|
339
|
+
collector.nodeCount += 1;
|
|
340
|
+
visitCelV2Children(node.args, scope, depth, collector);
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
const isLegacyCstNode = (value) => typeof value === 'object' &&
|
|
345
|
+
value !== null &&
|
|
346
|
+
typeof value.name === 'string' &&
|
|
347
|
+
typeof value.children === 'object' &&
|
|
348
|
+
value.children !== null;
|
|
349
|
+
const isLegacyToken = (value) => typeof value.image === 'string';
|
|
350
|
+
const readLegacyNodes = (node, key) => (node.children[key] ?? []).filter(isLegacyCstNode);
|
|
351
|
+
const readLegacyTokens = (node, key) => (node.children[key] ?? []).filter(isLegacyToken);
|
|
352
|
+
const readLegacyElements = (node) => Object.values(node.children).flat();
|
|
353
|
+
function readCelLegacyReferences(expression) {
|
|
354
|
+
const handle = (0, cel_legacy_adapter_1.parseCelLegacy)(expression);
|
|
355
|
+
if (handle === null) {
|
|
356
|
+
return { limitExceeded: false, status: 'PARSE_FAILED' };
|
|
357
|
+
}
|
|
358
|
+
if (!isLegacyCstNode(handle.native)) {
|
|
359
|
+
return { status: 'UNAVAILABLE' };
|
|
360
|
+
}
|
|
361
|
+
const collector = createCollector();
|
|
362
|
+
visitLegacy(handle.native, new Set(), 0, collector);
|
|
363
|
+
return { references: toReferences(collector, 'cel-legacy'), status: 'OK' };
|
|
364
|
+
}
|
|
365
|
+
/** The one wrapped atom of a trivial chain (`expr` → … → `atomicExpression`). */
|
|
366
|
+
const unwrapLegacyAtom = (node) => {
|
|
367
|
+
const elements = readLegacyElements(node);
|
|
368
|
+
if (node.name === 'atomicExpression') {
|
|
369
|
+
return elements.length === 1 ? elements[0] : null;
|
|
370
|
+
}
|
|
371
|
+
if (node.name === 'parenthesisExpression') {
|
|
372
|
+
const inner = readLegacyNodes(node, 'expr')[0];
|
|
373
|
+
return inner ? unwrapLegacyAtom(inner) : null;
|
|
374
|
+
}
|
|
375
|
+
if (elements.length !== 1) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
const [only] = elements;
|
|
379
|
+
return isLegacyCstNode(only) ? unwrapLegacyAtom(only) : null;
|
|
380
|
+
};
|
|
381
|
+
/** cel-legacy does not decode escapes (P1 G25), so only plain literals count. */
|
|
382
|
+
const readLegacyStringLiteral = (token) => {
|
|
383
|
+
if (token.tokenType.name !== 'StringLiteral') {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
const match = /^"([^"\\]*)"$|^'([^'\\]*)'$/u.exec(token.image);
|
|
387
|
+
return match ? (match[1] ?? match[2] ?? '') : null;
|
|
388
|
+
};
|
|
389
|
+
const readLegacyLiteralKey = (expression) => {
|
|
390
|
+
const atom = unwrapLegacyAtom(expression);
|
|
391
|
+
if (atom === null || !isLegacyToken(atom)) {
|
|
392
|
+
return null;
|
|
393
|
+
}
|
|
394
|
+
const text = readLegacyStringLiteral(atom);
|
|
395
|
+
if (text !== null) {
|
|
396
|
+
return { key: text, string: true };
|
|
397
|
+
}
|
|
398
|
+
return atom.tokenType.name === 'Integer' ||
|
|
399
|
+
atom.tokenType.name === 'UnsignedInteger'
|
|
400
|
+
? { key: atom.image.replace(/[uU]$/u, ''), string: false }
|
|
401
|
+
: null;
|
|
402
|
+
};
|
|
403
|
+
const isLegacyEmptyLiteral = (node) => {
|
|
404
|
+
const atom = unwrapLegacyAtom(node);
|
|
405
|
+
if (atom === null) {
|
|
406
|
+
return false;
|
|
407
|
+
}
|
|
408
|
+
if (isLegacyToken(atom)) {
|
|
409
|
+
return (atom.tokenType.name === 'Null' || readLegacyStringLiteral(atom) === '');
|
|
410
|
+
}
|
|
411
|
+
return (atom.name === 'listExpression' && readLegacyNodes(atom, 'lhs').length === 0);
|
|
412
|
+
};
|
|
413
|
+
/** The bare identifier an `expr` is, if it is nothing else. */
|
|
414
|
+
const readLegacyBareIdentifier = (node) => {
|
|
415
|
+
const atom = unwrapLegacyAtom(node);
|
|
416
|
+
if (atom === null ||
|
|
417
|
+
isLegacyToken(atom) ||
|
|
418
|
+
atom.name !== 'identifierExpression' ||
|
|
419
|
+
readLegacyElements(atom).length !== 1) {
|
|
420
|
+
return null;
|
|
421
|
+
}
|
|
422
|
+
return readLegacyTokens(atom, 'Identifier')[0]?.image ?? null;
|
|
423
|
+
};
|
|
424
|
+
const readLegacyCallArguments = (node) => [
|
|
425
|
+
...readLegacyNodes(node, 'arg'),
|
|
426
|
+
...readLegacyNodes(node, 'args'),
|
|
427
|
+
];
|
|
428
|
+
const visitLegacyAll = (nodes, scope, depth, collector) => {
|
|
429
|
+
nodes.forEach((child) => {
|
|
430
|
+
visitLegacy(child, scope, depth, collector);
|
|
431
|
+
});
|
|
432
|
+
};
|
|
433
|
+
/** `lhs` followed by any number of `rhs`: an n-ary operator chain. */
|
|
434
|
+
const visitLegacyChain = (node, scope, depth, collector) => {
|
|
435
|
+
const lhs = readLegacyNodes(node, 'lhs');
|
|
436
|
+
const rhs = readLegacyNodes(node, 'rhs');
|
|
437
|
+
if (rhs.length === 0) {
|
|
438
|
+
return lhs[0] ? visitLegacy(lhs[0], scope, depth, collector) : null;
|
|
439
|
+
}
|
|
440
|
+
collector.nodeCount += rhs.length;
|
|
441
|
+
visitLegacyAll([...lhs, ...rhs], scope, depth, collector);
|
|
442
|
+
return null;
|
|
443
|
+
};
|
|
444
|
+
/** The member and index steps after an identifier, in source order. */
|
|
445
|
+
const readLegacySteps = (node) => {
|
|
446
|
+
const offsetOf = (step) => {
|
|
447
|
+
const first = readLegacyElements(step).find(isLegacyToken);
|
|
448
|
+
return first?.startOffset ?? 0;
|
|
449
|
+
};
|
|
450
|
+
return [
|
|
451
|
+
...readLegacyNodes(node, 'identifierDotExpression'),
|
|
452
|
+
...readLegacyNodes(node, 'identifierIndexExpression'),
|
|
453
|
+
...readLegacyNodes(node, 'indexExpression'),
|
|
454
|
+
...readLegacyNodes(node, 'Index'),
|
|
455
|
+
].sort((left, right) => offsetOf(left) - offsetOf(right));
|
|
456
|
+
};
|
|
457
|
+
const visitLegacyStep = (step, state, rootName, scope, depth, collector) => {
|
|
458
|
+
collector.nodeCount += 1;
|
|
459
|
+
if (step.name !== 'identifierDotExpression') {
|
|
460
|
+
const index = readLegacyNodes(step, 'expr')[0];
|
|
461
|
+
const literal = index ? readLegacyLiteralKey(index) : null;
|
|
462
|
+
if (literal !== null) {
|
|
463
|
+
if (literal.string) {
|
|
464
|
+
noteReserved(collector, literal.key);
|
|
465
|
+
}
|
|
466
|
+
return state.complete
|
|
467
|
+
? { complete: true, segments: [...state.segments, literal.key] }
|
|
468
|
+
: state;
|
|
469
|
+
}
|
|
470
|
+
collector.computedKeys.push({ kind: 'INDEX', root: rootName });
|
|
471
|
+
if (index) {
|
|
472
|
+
visitLegacy(index, scope, depth, collector);
|
|
473
|
+
}
|
|
474
|
+
return { complete: false, segments: state.segments };
|
|
475
|
+
}
|
|
476
|
+
const name = readLegacyTokens(step, 'Identifier')[0]?.image ?? '';
|
|
477
|
+
if (readLegacyTokens(step, 'OpenParenthesis').length === 0) {
|
|
478
|
+
noteReserved(collector, name);
|
|
479
|
+
return state.complete
|
|
480
|
+
? { complete: true, segments: [...state.segments, name] }
|
|
481
|
+
: state;
|
|
482
|
+
}
|
|
483
|
+
const args = readLegacyCallArguments(step);
|
|
484
|
+
const bound = args[0] ? readLegacyBareIdentifier(args[0]) : null;
|
|
485
|
+
if (rootName !== null && state.complete) {
|
|
486
|
+
// A method called on the root itself uses the root as a value.
|
|
487
|
+
if (state.segments.length === 0) {
|
|
488
|
+
collector.wholeRootReferences.add(rootName);
|
|
489
|
+
}
|
|
490
|
+
noteReceiver(collector, name, {
|
|
491
|
+
complete: true,
|
|
492
|
+
root: rootName,
|
|
493
|
+
segments: state.segments,
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
if (COMPREHENSION_MACROS.has(name) && bound !== null && args.length >= 2) {
|
|
497
|
+
const innerDepth = depth + 1;
|
|
498
|
+
collector.comprehensionDepth = Math.max(collector.comprehensionDepth, innerDepth);
|
|
499
|
+
visitLegacyAll(args.slice(1), new Set([...scope, bound]), innerDepth, collector);
|
|
500
|
+
}
|
|
501
|
+
else {
|
|
502
|
+
collector.functionCalls.push({
|
|
503
|
+
argumentCount: args.length,
|
|
504
|
+
name,
|
|
505
|
+
receiver: true,
|
|
506
|
+
});
|
|
507
|
+
visitLegacyAll(args, scope, depth, collector);
|
|
508
|
+
}
|
|
509
|
+
return { complete: false, segments: state.segments };
|
|
510
|
+
};
|
|
511
|
+
const visitLegacySteps = (steps, rootName, scope, depth, collector) => steps.reduce((state, step) => visitLegacyStep(step, state, rootName, scope, depth, collector), { complete: true, segments: [] });
|
|
512
|
+
function visitLegacy(node, scope, depth, collector) {
|
|
513
|
+
switch (node.name) {
|
|
514
|
+
case 'expr': {
|
|
515
|
+
const condition = readLegacyNodes(node, 'conditionalOr')[0];
|
|
516
|
+
const branches = [
|
|
517
|
+
...readLegacyNodes(node, 'lhs'),
|
|
518
|
+
...readLegacyNodes(node, 'rhs'),
|
|
519
|
+
];
|
|
520
|
+
const path = condition
|
|
521
|
+
? visitLegacy(condition, scope, depth, collector)
|
|
522
|
+
: null;
|
|
523
|
+
if (branches.length === 0) {
|
|
524
|
+
return path;
|
|
525
|
+
}
|
|
526
|
+
collector.nodeCount += 1;
|
|
527
|
+
visitLegacyAll(branches, scope, depth, collector);
|
|
528
|
+
return null;
|
|
529
|
+
}
|
|
530
|
+
case 'conditionalOr': {
|
|
531
|
+
if (readLegacyNodes(node, 'rhs').length === 0) {
|
|
532
|
+
return visitLegacyChain(node, scope, depth, collector);
|
|
533
|
+
}
|
|
534
|
+
const start = collector.fieldPaths.length;
|
|
535
|
+
visitLegacyChain(node, scope, depth, collector);
|
|
536
|
+
collector.orOperandFieldPaths.push(...collector.fieldPaths.slice(start));
|
|
537
|
+
return null;
|
|
538
|
+
}
|
|
539
|
+
case 'conditionalAnd':
|
|
540
|
+
case 'addition':
|
|
541
|
+
return visitLegacyChain(node, scope, depth, collector);
|
|
542
|
+
case 'relation': {
|
|
543
|
+
const [left] = readLegacyNodes(node, 'lhs');
|
|
544
|
+
const [right] = readLegacyNodes(node, 'rhs');
|
|
545
|
+
const [operator] = readLegacyTokens(node, 'ComparisonOperator');
|
|
546
|
+
if (!right || !left) {
|
|
547
|
+
return left ? visitLegacy(left, scope, depth, collector) : null;
|
|
548
|
+
}
|
|
549
|
+
collector.nodeCount += 1;
|
|
550
|
+
const leftPath = visitLegacy(left, scope, depth, collector);
|
|
551
|
+
const rightPath = visitLegacy(right, scope, depth, collector);
|
|
552
|
+
if (operator?.image === '==' || operator?.image === '!=') {
|
|
553
|
+
if (leftPath !== null && isLegacyEmptyLiteral(right)) {
|
|
554
|
+
collector.guardedFieldPaths.push(leftPath);
|
|
555
|
+
}
|
|
556
|
+
if (rightPath !== null && isLegacyEmptyLiteral(left)) {
|
|
557
|
+
collector.guardedFieldPaths.push(rightPath);
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
return null;
|
|
561
|
+
}
|
|
562
|
+
case 'multiplication': {
|
|
563
|
+
const [left] = readLegacyNodes(node, 'lhs');
|
|
564
|
+
const rhs = readLegacyNodes(node, 'rhs');
|
|
565
|
+
const operators = readLegacyTokens(node, 'MultiplicationOperator');
|
|
566
|
+
if (rhs.length === 0) {
|
|
567
|
+
return left ? visitLegacy(left, scope, depth, collector) : null;
|
|
568
|
+
}
|
|
569
|
+
collector.nodeCount += rhs.length;
|
|
570
|
+
if (left) {
|
|
571
|
+
visitLegacy(left, scope, depth, collector);
|
|
572
|
+
}
|
|
573
|
+
rhs.forEach((operand, index) => {
|
|
574
|
+
const start = collector.fieldPaths.length;
|
|
575
|
+
visitLegacy(operand, scope, depth, collector);
|
|
576
|
+
if (operators[index]?.image === '/') {
|
|
577
|
+
collector.divisorFieldPaths.push(...collector.fieldPaths.slice(start));
|
|
578
|
+
}
|
|
579
|
+
});
|
|
580
|
+
return null;
|
|
581
|
+
}
|
|
582
|
+
case 'unaryExpression': {
|
|
583
|
+
const [atom] = readLegacyNodes(node, 'atomicExpression');
|
|
584
|
+
if (!atom) {
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
if (readLegacyTokens(node, 'UnaryOperator').length === 0) {
|
|
588
|
+
return visitLegacy(atom, scope, depth, collector);
|
|
589
|
+
}
|
|
590
|
+
collector.nodeCount += 1;
|
|
591
|
+
visitLegacy(atom, scope, depth, collector);
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
case 'atomicExpression': {
|
|
595
|
+
const [child] = readLegacyElements(node);
|
|
596
|
+
if (!child) {
|
|
597
|
+
return null;
|
|
598
|
+
}
|
|
599
|
+
if (isLegacyToken(child)) {
|
|
600
|
+
collector.nodeCount += 1;
|
|
601
|
+
return null;
|
|
602
|
+
}
|
|
603
|
+
return visitLegacy(child, scope, depth, collector);
|
|
604
|
+
}
|
|
605
|
+
case 'parenthesisExpression': {
|
|
606
|
+
const [inner] = readLegacyNodes(node, 'expr');
|
|
607
|
+
return inner ? visitLegacy(inner, scope, depth, collector) : null;
|
|
608
|
+
}
|
|
609
|
+
case 'listExpression': {
|
|
610
|
+
collector.nodeCount += 1;
|
|
611
|
+
visitLegacyAll([...readLegacyNodes(node, 'lhs'), ...readLegacyNodes(node, 'rhs')], scope, depth, collector);
|
|
612
|
+
visitLegacySteps(readLegacySteps(node), null, scope, depth, collector);
|
|
613
|
+
return null;
|
|
614
|
+
}
|
|
615
|
+
case 'mapExpression': {
|
|
616
|
+
collector.nodeCount += 1;
|
|
617
|
+
readLegacyNodes(node, 'keyValues').forEach((entry) => {
|
|
618
|
+
const [key] = readLegacyNodes(entry, 'key');
|
|
619
|
+
const [value] = readLegacyNodes(entry, 'value');
|
|
620
|
+
const literal = key ? readLegacyLiteralKey(key) : null;
|
|
621
|
+
collector.nodeCount += 1;
|
|
622
|
+
const atom = key ? unwrapLegacyAtom(key) : null;
|
|
623
|
+
if (literal !== null) {
|
|
624
|
+
if (literal.string) {
|
|
625
|
+
noteReserved(collector, literal.key);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
else if (key && (atom === null || !isLegacyToken(atom))) {
|
|
629
|
+
// Any literal token (`true`, `1.5`) is still a literal key.
|
|
630
|
+
collector.computedKeys.push({ kind: 'MAP_KEY', root: null });
|
|
631
|
+
visitLegacy(key, scope, depth, collector);
|
|
632
|
+
}
|
|
633
|
+
if (value) {
|
|
634
|
+
visitLegacy(value, scope, depth, collector);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
visitLegacySteps(readLegacySteps(node), null, scope, depth, collector);
|
|
638
|
+
return null;
|
|
639
|
+
}
|
|
640
|
+
case 'macrosExpression': {
|
|
641
|
+
const name = readLegacyTokens(node, 'Identifier')[0]?.image ?? '';
|
|
642
|
+
const args = readLegacyCallArguments(node);
|
|
643
|
+
collector.nodeCount += 1;
|
|
644
|
+
if (name === 'has' && args.length === 1) {
|
|
645
|
+
const path = visitLegacy(args[0], scope, depth, collector);
|
|
646
|
+
if (path !== null) {
|
|
647
|
+
collector.hasArguments.push(path);
|
|
648
|
+
collector.guardedFieldPaths.push(path);
|
|
649
|
+
}
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
collector.functionCalls.push({
|
|
653
|
+
argumentCount: args.length,
|
|
654
|
+
name,
|
|
655
|
+
receiver: false,
|
|
656
|
+
});
|
|
657
|
+
visitLegacyAll(args, scope, depth, collector);
|
|
658
|
+
return null;
|
|
659
|
+
}
|
|
660
|
+
case 'identifierExpression': {
|
|
661
|
+
const name = readLegacyTokens(node, 'Identifier')[0]?.image ?? '';
|
|
662
|
+
const rootName = scope.has(name) ? null : name;
|
|
663
|
+
const steps = readLegacySteps(node);
|
|
664
|
+
collector.nodeCount += 1;
|
|
665
|
+
if (rootName !== null) {
|
|
666
|
+
collector.roots.add(rootName);
|
|
667
|
+
}
|
|
668
|
+
if (steps.length === 0) {
|
|
669
|
+
if (rootName !== null) {
|
|
670
|
+
collector.wholeRootReferences.add(rootName);
|
|
671
|
+
}
|
|
672
|
+
return null;
|
|
673
|
+
}
|
|
674
|
+
const { complete, segments } = visitLegacySteps(steps, rootName, scope, depth, collector);
|
|
675
|
+
if (rootName === null) {
|
|
676
|
+
return null;
|
|
677
|
+
}
|
|
678
|
+
const path = { complete, root: rootName, segments };
|
|
679
|
+
collector.fieldPaths.push(path);
|
|
680
|
+
return path;
|
|
681
|
+
}
|
|
682
|
+
default:
|
|
683
|
+
visitLegacyAll(readLegacyElements(node).filter(isLegacyCstNode), scope, depth, collector);
|
|
684
|
+
return null;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
/** Whether the expression reads the top-level form field `fieldKey`. */
|
|
688
|
+
const readsFormField = (references, fieldKey) => references.formFieldKeys.includes(fieldKey);
|
|
689
|
+
exports.readsFormField = readsFormField;
|
|
690
|
+
/**
|
|
691
|
+
* Whether the expression steps inside the table field `tableKey`
|
|
692
|
+
* (`form.items.qty`, `form.items[0]`, `form.items.exists(r, ...)`), the AST
|
|
693
|
+
* twin of `referencesTableInternals` (ADR 16 §3.8). The table as a whole
|
|
694
|
+
* (`size(form.items)`, `form.items.size()`, `form.items == null`) is not.
|
|
695
|
+
*/
|
|
696
|
+
const readsTableInternals = (references, tableKey) => references.fieldPaths.some((path) => FORM_ROOTS.has(path.root) &&
|
|
697
|
+
path.segments[0] === tableKey &&
|
|
698
|
+
(path.segments.length > 1 || !path.complete)) ||
|
|
699
|
+
references.receiverPaths.some(({ name, path }) => name !== 'size' &&
|
|
700
|
+
FORM_ROOTS.has(path.root) &&
|
|
701
|
+
path.segments.length === 1 &&
|
|
702
|
+
path.segments[0] === tableKey);
|
|
703
|
+
exports.readsTableInternals = readsTableInternals;
|
|
704
|
+
/**
|
|
705
|
+
* The CEL types a form field is declared with for the lint's type check
|
|
706
|
+
* (docs/24-design/p2-p3 §2.2). Only names the engine accepts are produced;
|
|
707
|
+
* anything else is `dyn`. Never `optional(...)`: the engine cannot convert it.
|
|
708
|
+
*/
|
|
709
|
+
const readExpressionFieldType = (field) => {
|
|
710
|
+
switch (field.type) {
|
|
711
|
+
case 'text':
|
|
712
|
+
case 'textarea':
|
|
713
|
+
case 'date':
|
|
714
|
+
case 'datetime':
|
|
715
|
+
case 'org_unit':
|
|
716
|
+
return 'string';
|
|
717
|
+
case 'number':
|
|
718
|
+
case 'money':
|
|
719
|
+
return 'double';
|
|
720
|
+
case 'boolean':
|
|
721
|
+
return 'bool';
|
|
722
|
+
case 'member':
|
|
723
|
+
return field.multiple === true ? 'list<string>' : 'string';
|
|
724
|
+
case 'select':
|
|
725
|
+
case 'autocomplete':
|
|
726
|
+
case 'radio':
|
|
727
|
+
case 'checkbox':
|
|
728
|
+
return (0, form_1.isFormOptionFieldDefinition)(field) &&
|
|
729
|
+
(0, form_1.readFormFieldSelectionMode)(field) === 'multiple'
|
|
730
|
+
? 'list<string>'
|
|
731
|
+
: 'string';
|
|
732
|
+
case 'table':
|
|
733
|
+
return 'list<dyn>';
|
|
734
|
+
default:
|
|
735
|
+
return 'dyn';
|
|
736
|
+
}
|
|
737
|
+
};
|
|
738
|
+
exports.readExpressionFieldType = readExpressionFieldType;
|
|
739
|
+
const readSchemaFields = (schema) => Array.isArray(schema?.fields)
|
|
740
|
+
? schema.fields.filter((field) => typeof field?.fieldKey === 'string' &&
|
|
741
|
+
!exports.RESERVED_PATH_SEGMENTS.has(field.fieldKey))
|
|
742
|
+
: [];
|
|
743
|
+
const readDefinedFunctionNames = (environment) => {
|
|
744
|
+
const functions = environment.getDefinitions().functions;
|
|
745
|
+
return {
|
|
746
|
+
globals: new Set(functions
|
|
747
|
+
.filter((definition) => !definition.receiverType)
|
|
748
|
+
.map((definition) => definition.name)),
|
|
749
|
+
methods: new Set(functions
|
|
750
|
+
.filter((definition) => Boolean(definition.receiverType))
|
|
751
|
+
.map((definition) => definition.name)),
|
|
752
|
+
};
|
|
753
|
+
};
|
|
754
|
+
let pristineFunctionNames = null;
|
|
755
|
+
/**
|
|
756
|
+
* The functions cel-v2 itself provides (`size`, `int`, `contains`, …), read
|
|
757
|
+
* from a fresh environment of the same class so that host functions a later
|
|
758
|
+
* environment may register are never mistaken for the engine's own.
|
|
759
|
+
*/
|
|
760
|
+
const readEngineFunctionNames = () => {
|
|
761
|
+
if (pristineFunctionNames === null) {
|
|
762
|
+
const EnvironmentClass = (0, cel_v2_adapter_1.loadCelV2Environment)()
|
|
763
|
+
.constructor;
|
|
764
|
+
pristineFunctionNames = readDefinedFunctionNames(new EnvironmentClass());
|
|
765
|
+
}
|
|
766
|
+
return pristineFunctionNames;
|
|
767
|
+
};
|
|
768
|
+
const knownFunctionNamesByEnvironment = new WeakMap();
|
|
769
|
+
/** Global functions the shared environment can type-check today. */
|
|
770
|
+
const readCheckableGlobalNames = (environment) => {
|
|
771
|
+
const cached = knownFunctionNamesByEnvironment.get(environment);
|
|
772
|
+
if (cached) {
|
|
773
|
+
return cached;
|
|
774
|
+
}
|
|
775
|
+
const names = readDefinedFunctionNames(environment).globals;
|
|
776
|
+
knownFunctionNamesByEnvironment.set(environment, names);
|
|
777
|
+
return names;
|
|
778
|
+
};
|
|
779
|
+
const issue = (code, label, subject) => ({
|
|
780
|
+
code,
|
|
781
|
+
label,
|
|
782
|
+
severity: (0, bpm_core_shared_1.readExpressionLintSeverity)(code),
|
|
783
|
+
...(subject === undefined ? {} : { subject }),
|
|
784
|
+
});
|
|
785
|
+
/**
|
|
786
|
+
* A use point that is set but is not one (a client may send any string, a
|
|
787
|
+
* prototype key such as `toString` included). It is never used as an index:
|
|
788
|
+
* such a context allows no root and no host function, so the lint fails
|
|
789
|
+
* closed instead of throwing.
|
|
790
|
+
*/
|
|
791
|
+
const hasUnknownUsagePoint = (context) => context.usagePoint !== undefined &&
|
|
792
|
+
context.usagePoint !== null &&
|
|
793
|
+
!(0, bpm_core_shared_1.isExpressionUsagePoint)(context.usagePoint);
|
|
794
|
+
const readAllowedRoots = (context) => {
|
|
795
|
+
if (context.allowedRootIdentifiers?.length) {
|
|
796
|
+
return context.allowedRootIdentifiers;
|
|
797
|
+
}
|
|
798
|
+
if (hasUnknownUsagePoint(context)) {
|
|
799
|
+
return [];
|
|
800
|
+
}
|
|
801
|
+
return context.usagePoint
|
|
802
|
+
? bpm_core_shared_1.EXPRESSION_ROOT_IDENTIFIERS_BY_USAGE_POINT[context.usagePoint]
|
|
803
|
+
: null;
|
|
804
|
+
};
|
|
805
|
+
const lintStructure = (references, label, context) => {
|
|
806
|
+
const allowedRoots = readAllowedRoots(context);
|
|
807
|
+
const allowed = new Set(allowedRoots ?? []);
|
|
808
|
+
const rootIssues = allowedRoots === null
|
|
809
|
+
? []
|
|
810
|
+
: references.roots
|
|
811
|
+
.filter((root) => !allowed.has(root))
|
|
812
|
+
.map((root) => issue('EXPR_ROOT_NOT_ALLOWED', label, root));
|
|
813
|
+
const wholeIssues = references.wholeRootReferences
|
|
814
|
+
.filter((root) => WHOLE_REFERENCE_ROOTS.has(root) &&
|
|
815
|
+
(allowedRoots === null || allowed.has(root)))
|
|
816
|
+
.map((root) => issue('EXPR_WHOLE_FORM_REFERENCE', label, root));
|
|
817
|
+
return [
|
|
818
|
+
...rootIssues,
|
|
819
|
+
...wholeIssues,
|
|
820
|
+
...(references.computedKeys.length > 0
|
|
821
|
+
? [issue('EXPR_COMPUTED_KEY', label)]
|
|
822
|
+
: []),
|
|
823
|
+
...references.reservedSegments.map((segment) => issue('EXPR_RESERVED_PATH_SEGMENT', label, segment)),
|
|
824
|
+
...(references.comprehensionDepth > exports.EXPRESSION_MAX_COMPREHENSION_DEPTH
|
|
825
|
+
? [issue('EXPR_COMPREHENSION_NESTING', label)]
|
|
826
|
+
: []),
|
|
827
|
+
];
|
|
828
|
+
};
|
|
829
|
+
const lintFields = (references, label, schema) => {
|
|
830
|
+
if (!schema) {
|
|
831
|
+
return [];
|
|
832
|
+
}
|
|
833
|
+
const known = new Set(readSchemaFields(schema).map((field) => field.fieldKey));
|
|
834
|
+
return references.formFieldKeys
|
|
835
|
+
.filter((fieldKey) => !known.has(fieldKey))
|
|
836
|
+
.map((fieldKey) => issue('EXPR_FIELD_UNKNOWN', label, fieldKey));
|
|
837
|
+
};
|
|
838
|
+
const lintLogger = new common_1.Logger('ExpressionLint');
|
|
839
|
+
/**
|
|
840
|
+
* Logs that the host's function registry failed the lint. Only a fixed
|
|
841
|
+
* reason: the registry's own message is host text and is never kept
|
|
842
|
+
* (docs/24 §2.6).
|
|
843
|
+
*/
|
|
844
|
+
const logRegistryFailure = (reason) => {
|
|
845
|
+
lintLogger.warn(`code=EXPR_FUNCTION_NOT_REGISTERED reason=${reason}`);
|
|
846
|
+
};
|
|
847
|
+
const isStringList = (value) => Array.isArray(value) && value.every((entry) => typeof entry === 'string');
|
|
848
|
+
/**
|
|
849
|
+
* The called function names the registry cannot vouch for, in first-seen
|
|
850
|
+
* order and without duplicates (ADR 22 §5.3; publish and rollback share it).
|
|
851
|
+
*
|
|
852
|
+
* Only names {@link isRegistryFunctionName} accepts — `…_vN` and the nine
|
|
853
|
+
* built-ins — are put to `assertPublishable`; any other name can never be
|
|
854
|
+
* registered and is always reported. Without a registry only the built-ins
|
|
855
|
+
* exist.
|
|
856
|
+
*
|
|
857
|
+
* The registry is host code and is isolated: an `assertPublishable` that
|
|
858
|
+
* throws or answers something other than a list of strings makes every name
|
|
859
|
+
* asked about unavailable. The result only ever holds names the expression
|
|
860
|
+
* itself called, never a value the registry returned.
|
|
861
|
+
*/
|
|
862
|
+
function readUnavailableExpressionFunctions(names, registry) {
|
|
863
|
+
const unique = [...new Set(names)];
|
|
864
|
+
const asked = unique.filter(expression_function_invocation_1.isRegistryFunctionName);
|
|
865
|
+
const builtIns = new Set(bpm_core_shared_1.BPM_BUILT_IN_EXPRESSION_FUNCTION_NAMES);
|
|
866
|
+
const answered = (() => {
|
|
867
|
+
if (!registry) {
|
|
868
|
+
return new Set(asked.filter((name) => !builtIns.has(name)));
|
|
869
|
+
}
|
|
870
|
+
if (asked.length === 0) {
|
|
871
|
+
return new Set();
|
|
872
|
+
}
|
|
873
|
+
try {
|
|
874
|
+
const result = registry.assertPublishable(asked);
|
|
875
|
+
if (isStringList(result)) {
|
|
876
|
+
return new Set(result);
|
|
877
|
+
}
|
|
878
|
+
logRegistryFailure('assert-publishable-shape');
|
|
879
|
+
}
|
|
880
|
+
catch {
|
|
881
|
+
logRegistryFailure('assert-publishable-threw');
|
|
882
|
+
}
|
|
883
|
+
return new Set(asked);
|
|
884
|
+
})();
|
|
885
|
+
return unique.filter((name) => !(0, expression_function_invocation_1.isRegistryFunctionName)(name) || answered.has(name));
|
|
886
|
+
}
|
|
887
|
+
/**
|
|
888
|
+
* The parameter count the registry declares for `name`: `ABSENT` when `get`
|
|
889
|
+
* answers `null` (`assertPublishable` already decided availability), `BROKEN`
|
|
890
|
+
* when it throws or returns something without a parameter list. Isolated like
|
|
891
|
+
* {@link readUnavailableExpressionFunctions}.
|
|
892
|
+
*/
|
|
893
|
+
const readRegisteredParameterCount = (registry, name) => {
|
|
894
|
+
try {
|
|
895
|
+
const registered = registry.get(name);
|
|
896
|
+
if (registered === null) {
|
|
897
|
+
return 'ABSENT';
|
|
898
|
+
}
|
|
899
|
+
const parameters = typeof registered === 'object'
|
|
900
|
+
? registered.parameters
|
|
901
|
+
: undefined;
|
|
902
|
+
if (Array.isArray(parameters)) {
|
|
903
|
+
return parameters.length;
|
|
904
|
+
}
|
|
905
|
+
logRegistryFailure('get-shape');
|
|
906
|
+
}
|
|
907
|
+
catch {
|
|
908
|
+
logRegistryFailure('get-threw');
|
|
909
|
+
}
|
|
910
|
+
return 'BROKEN';
|
|
911
|
+
};
|
|
912
|
+
const readLintEnvironment = (registry) => {
|
|
913
|
+
if (!registry || registry === expression_function_registry_1.BUILT_IN_EXPRESSION_FUNCTION_REGISTRY) {
|
|
914
|
+
return { environment: (0, cel_v2_adapter_1.loadCelV2Environment)(), failed: false };
|
|
915
|
+
}
|
|
916
|
+
// Loaded first and outside the `try`: a failure to load the engine is a
|
|
917
|
+
// configuration error, not the registry's.
|
|
918
|
+
const builtInEnvironment = (0, cel_v2_adapter_1.loadCelV2Environment)();
|
|
919
|
+
try {
|
|
920
|
+
return { environment: (0, cel_v2_adapter_1.loadCelV2Environment)(registry), failed: false };
|
|
921
|
+
}
|
|
922
|
+
catch {
|
|
923
|
+
logRegistryFailure('environment');
|
|
924
|
+
return { environment: builtInEnvironment, failed: true };
|
|
925
|
+
}
|
|
926
|
+
};
|
|
927
|
+
const lintFunctions = (references, label, context, lintEnvironment) => {
|
|
928
|
+
const engine = readEngineFunctionNames();
|
|
929
|
+
const unknownMethods = [
|
|
930
|
+
...new Set(references.functionCalls
|
|
931
|
+
.filter((call) => call.receiver && !engine.methods.has(call.name))
|
|
932
|
+
.map((call) => call.name)),
|
|
933
|
+
];
|
|
934
|
+
const hostCalls = references.functionCalls.filter((call) => !call.receiver && !engine.globals.has(call.name));
|
|
935
|
+
const hostNames = [...new Set(hostCalls.map((call) => call.name))];
|
|
936
|
+
const registry = context.functionRegistry ?? null;
|
|
937
|
+
const builtIns = new Set(bpm_core_shared_1.BPM_BUILT_IN_EXPRESSION_FUNCTION_NAMES);
|
|
938
|
+
const missing = new Set(lintEnvironment.failed
|
|
939
|
+
? hostNames
|
|
940
|
+
: readUnavailableExpressionFunctions(hostNames, registry));
|
|
941
|
+
// A call whose argument count matches no registered parameter list names
|
|
942
|
+
// no registered function either (ADR 22 §5.3: "the function and version").
|
|
943
|
+
const wrongArity = registry
|
|
944
|
+
? hostCalls
|
|
945
|
+
.filter((call) => !missing.has(call.name))
|
|
946
|
+
.filter((call) => {
|
|
947
|
+
const count = readRegisteredParameterCount(registry, call.name);
|
|
948
|
+
return count !== 'ABSENT' && count !== call.argumentCount;
|
|
949
|
+
})
|
|
950
|
+
.map((call) => call.name)
|
|
951
|
+
: [];
|
|
952
|
+
const notRegistered = [
|
|
953
|
+
...new Set([...unknownMethods, ...missing, ...wrongArity]),
|
|
954
|
+
];
|
|
955
|
+
const hostFunctionsAllowed = hasUnknownUsagePoint(context)
|
|
956
|
+
? false
|
|
957
|
+
: !context.usagePoint || (0, bpm_core_shared_1.allowsHostExpressionFunctions)(context.usagePoint);
|
|
958
|
+
const notAllowed = hostFunctionsAllowed
|
|
959
|
+
? []
|
|
960
|
+
: hostNames.filter((name) => !missing.has(name) && !builtIns.has(name));
|
|
961
|
+
const checkable = readCheckableGlobalNames(lintEnvironment.environment);
|
|
962
|
+
return {
|
|
963
|
+
issues: [
|
|
964
|
+
...notRegistered.map((name) => issue('EXPR_FUNCTION_NOT_REGISTERED', label, name)),
|
|
965
|
+
// A registry that cannot be read refuses even an expression that
|
|
966
|
+
// calls nothing: evaluation builds its environment from the same list.
|
|
967
|
+
...(lintEnvironment.failed && notRegistered.length === 0
|
|
968
|
+
? [issue('EXPR_FUNCTION_NOT_REGISTERED', label)]
|
|
969
|
+
: []),
|
|
970
|
+
...notAllowed.map((name) => issue('EXPR_FUNCTION_NOT_ALLOWED', label, name)),
|
|
971
|
+
],
|
|
972
|
+
uncheckable: lintEnvironment.failed ||
|
|
973
|
+
unknownMethods.length > 0 ||
|
|
974
|
+
hostNames.some((name) => !checkable.has(name)),
|
|
975
|
+
};
|
|
976
|
+
};
|
|
977
|
+
/** Base CEL types a use point may produce; `null` means anything. */
|
|
978
|
+
const readExpectedResultTypes = (context) => {
|
|
979
|
+
switch (context.usagePoint) {
|
|
980
|
+
case 'INITIATOR_POLICY':
|
|
981
|
+
case 'ENTRY_CONDITION':
|
|
982
|
+
case 'FLOW_CONDITION':
|
|
983
|
+
case 'DELEGATION_CONDITION':
|
|
984
|
+
return ['bool'];
|
|
985
|
+
case 'APPROVER_RESOLVER':
|
|
986
|
+
return ['string', 'list'];
|
|
987
|
+
case 'DATA_SOURCE_BINDING':
|
|
988
|
+
case 'NOTIFY_WEBHOOK_BINDING':
|
|
989
|
+
return ['bool', 'double', 'int', 'null', 'string', 'uint'];
|
|
990
|
+
case 'SERVICE_FORM_FIELD_VALUE': {
|
|
991
|
+
if (!context.targetField) {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
const declared = (0, exports.readExpressionFieldType)(context.targetField);
|
|
995
|
+
const base = declared.split('<')[0];
|
|
996
|
+
if (base === 'dyn') {
|
|
997
|
+
return null;
|
|
998
|
+
}
|
|
999
|
+
return base === 'double'
|
|
1000
|
+
? ['double', 'int', 'uint', 'null']
|
|
1001
|
+
: [base, 'null'];
|
|
1002
|
+
}
|
|
1003
|
+
default:
|
|
1004
|
+
return null;
|
|
1005
|
+
}
|
|
1006
|
+
};
|
|
1007
|
+
const isResultTypeAccepted = (type, expected) => {
|
|
1008
|
+
if (expected === null || type === undefined) {
|
|
1009
|
+
return true;
|
|
1010
|
+
}
|
|
1011
|
+
const base = type.split('<')[0].trim();
|
|
1012
|
+
return base === 'dyn' || base === 'null_type' || expected.includes(base);
|
|
1013
|
+
};
|
|
1014
|
+
const readCheckErrorNode = (error) => {
|
|
1015
|
+
const node = error?.node;
|
|
1016
|
+
return typeof node === 'object' &&
|
|
1017
|
+
node !== null &&
|
|
1018
|
+
typeof node.op === 'string'
|
|
1019
|
+
? node
|
|
1020
|
+
: null;
|
|
1021
|
+
};
|
|
1022
|
+
/**
|
|
1023
|
+
* Comparisons a typed check refuses that the runtime (where every field is
|
|
1024
|
+
* `dyn`) accepts and the designer writes on purpose: `form.x != null`,
|
|
1025
|
+
* `form.amount != ""`, `form.member != []` (`readConditionExpression`) and
|
|
1026
|
+
* `form.qty in [1, 2, 3]` (P1 P03 routes the same in both dialects).
|
|
1027
|
+
*/
|
|
1028
|
+
const isTolerated = (node) => node.op === 'in' ||
|
|
1029
|
+
((node.op === '==' || node.op === '!=') &&
|
|
1030
|
+
node.args.some((operand) => isCelV2EmptyLiteral(operand)));
|
|
1031
|
+
/**
|
|
1032
|
+
* The schema type (its base, `list<string>` is `list`) of an argument that is
|
|
1033
|
+
* exactly `form.<key>`, `formData.<key>` or `form["<key>"]` for a field
|
|
1034
|
+
* `runTypeCheck` demoted to `dyn`; `null` for anything else, which keeps the
|
|
1035
|
+
* type the check gave it.
|
|
1036
|
+
*/
|
|
1037
|
+
const readDemotedFieldType = (argument, schemaTypes, checkedTypes) => {
|
|
1038
|
+
if (schemaTypes === null || checkedTypes === null) {
|
|
1039
|
+
return null;
|
|
1040
|
+
}
|
|
1041
|
+
const { base, steps } = readCelV2Chain(argument);
|
|
1042
|
+
const step = steps.length === 1 ? steps[0] : null;
|
|
1043
|
+
const literal = step?.kind === 'index' ? readCelV2LiteralKey(step.index) : null;
|
|
1044
|
+
const fieldKey = step?.kind === 'member'
|
|
1045
|
+
? step.name
|
|
1046
|
+
: literal?.string === true
|
|
1047
|
+
? literal.key
|
|
1048
|
+
: null;
|
|
1049
|
+
if (fieldKey === null ||
|
|
1050
|
+
base.op !== 'id' ||
|
|
1051
|
+
!FORM_ROOTS.has(base.args) ||
|
|
1052
|
+
argument.op === '.?' ||
|
|
1053
|
+
argument.op === '[?]' ||
|
|
1054
|
+
checkedTypes.get(fieldKey) !== 'dyn') {
|
|
1055
|
+
return null;
|
|
1056
|
+
}
|
|
1057
|
+
const schemaType = schemaTypes.get(fieldKey);
|
|
1058
|
+
return schemaType === undefined || schemaType === 'dyn'
|
|
1059
|
+
? null
|
|
1060
|
+
: schemaType.split('<')[0].trim();
|
|
1061
|
+
};
|
|
1062
|
+
const createLintEnvironment = (fieldTypes, base) => {
|
|
1063
|
+
// `clone` copies the registry environment's registrations (the mixed
|
|
1064
|
+
// int/double operators, the built-ins and the host's functions) without
|
|
1065
|
+
// touching it. That environment is frozen when it is built (cel-v2
|
|
1066
|
+
// adapter), before anyone can clone it, so cloning here never stops a
|
|
1067
|
+
// registration that is still to come, whichever of boot, lint and
|
|
1068
|
+
// evaluation runs first.
|
|
1069
|
+
const environment = base.clone();
|
|
1070
|
+
if (fieldTypes !== null) {
|
|
1071
|
+
const schema = Object.fromEntries(fieldTypes);
|
|
1072
|
+
environment.registerVariable({ name: 'form', schema });
|
|
1073
|
+
environment.registerVariable({ name: 'formData', schema });
|
|
1074
|
+
}
|
|
1075
|
+
return environment;
|
|
1076
|
+
};
|
|
1077
|
+
/**
|
|
1078
|
+
* `env.check()` with each form field declared at its schema type. A refused
|
|
1079
|
+
* comparison the runtime accepts (see `isTolerated`) demotes the fields in it
|
|
1080
|
+
* to `dyn` and checks again; anything else is the finding. Bounded by the
|
|
1081
|
+
* field count, since every round demotes at least one field.
|
|
1082
|
+
*/
|
|
1083
|
+
const runTypeCheck = (expression, fieldTypes, base) => {
|
|
1084
|
+
const check = (types, rounds) => {
|
|
1085
|
+
const parsed = createLintEnvironment(types, base).parse(expression);
|
|
1086
|
+
const result = parsed.check();
|
|
1087
|
+
if (result.valid) {
|
|
1088
|
+
// The engine checks host and built-in parameters through their
|
|
1089
|
+
// signatures, except numbers, which are registered as `dyn` there:
|
|
1090
|
+
// compare each argument's checked type with the declared one here, or
|
|
1091
|
+
// `demo_costBand_v1(form.title)` publishes and fails every case.
|
|
1092
|
+
// A field demoted to `dyn` for a tolerated comparison is demoted in the
|
|
1093
|
+
// whole expression, so an argument that is that field is compared at
|
|
1094
|
+
// its schema type: `form.title != null && f(form.title)` still fails
|
|
1095
|
+
// every case with a text title.
|
|
1096
|
+
const mismatch = (0, cel_v2_adapter_1.findCelV2ArgumentTypeMismatch)(parsed.ast, base, (argument) => readDemotedFieldType(argument, fieldTypes, types));
|
|
1097
|
+
if (mismatch === null) {
|
|
1098
|
+
return { status: 'OK', type: result.type };
|
|
1099
|
+
}
|
|
1100
|
+
const argumentFieldKeys = collectCelV2References(mismatch.argument).formFieldKeys;
|
|
1101
|
+
return {
|
|
1102
|
+
code: 'EXPR_FIELD_TYPE_MISMATCH',
|
|
1103
|
+
fieldKey: argumentFieldKeys.length === 1 ? argumentFieldKeys[0] : null,
|
|
1104
|
+
status: 'FAILED',
|
|
1105
|
+
};
|
|
1106
|
+
}
|
|
1107
|
+
const node = readCheckErrorNode(result.error);
|
|
1108
|
+
const fieldKeys = node ? collectCelV2References(node).formFieldKeys : [];
|
|
1109
|
+
const code = result.error?.code;
|
|
1110
|
+
if (code === 'no_such_key') {
|
|
1111
|
+
return {
|
|
1112
|
+
code: 'EXPR_FIELD_UNKNOWN',
|
|
1113
|
+
fieldKey: fieldKeys.length === 1 ? fieldKeys[0] : null,
|
|
1114
|
+
status: 'FAILED',
|
|
1115
|
+
};
|
|
1116
|
+
}
|
|
1117
|
+
const demotable = types === null || node === null || !isTolerated(node)
|
|
1118
|
+
? []
|
|
1119
|
+
: fieldKeys.filter((fieldKey) => {
|
|
1120
|
+
const type = types.get(fieldKey);
|
|
1121
|
+
return type !== undefined && type !== 'dyn';
|
|
1122
|
+
});
|
|
1123
|
+
if (types === null || demotable.length === 0 || rounds <= 0) {
|
|
1124
|
+
return {
|
|
1125
|
+
code: 'EXPR_FIELD_TYPE_MISMATCH',
|
|
1126
|
+
fieldKey: fieldKeys.length === 1 ? fieldKeys[0] : null,
|
|
1127
|
+
status: 'FAILED',
|
|
1128
|
+
};
|
|
1129
|
+
}
|
|
1130
|
+
return check(new Map([...types].map(([fieldKey, type]) => [
|
|
1131
|
+
fieldKey,
|
|
1132
|
+
demotable.includes(fieldKey) ? 'dyn' : type,
|
|
1133
|
+
])), rounds - 1);
|
|
1134
|
+
};
|
|
1135
|
+
return check(fieldTypes, fieldTypes?.size ?? 0);
|
|
1136
|
+
};
|
|
1137
|
+
const lintTypes = (expression, label, context, base) => {
|
|
1138
|
+
const fields = readSchemaFields(context.formSchema);
|
|
1139
|
+
const fieldTypes = context.formSchema
|
|
1140
|
+
? new Map(fields.map((field) => [
|
|
1141
|
+
field.fieldKey,
|
|
1142
|
+
(0, exports.readExpressionFieldType)(field),
|
|
1143
|
+
]))
|
|
1144
|
+
: null;
|
|
1145
|
+
const outcome = runTypeCheck(expression, fieldTypes, base);
|
|
1146
|
+
if (outcome.status === 'FAILED') {
|
|
1147
|
+
return [issue(outcome.code, label, outcome.fieldKey ?? undefined)];
|
|
1148
|
+
}
|
|
1149
|
+
return isResultTypeAccepted(outcome.type, readExpectedResultTypes(context))
|
|
1150
|
+
? []
|
|
1151
|
+
: [issue('EXPR_RESULT_TYPE_MISMATCH', label)];
|
|
1152
|
+
};
|
|
1153
|
+
const readPathKey = (path) => [path.root, ...path.segments].join('.');
|
|
1154
|
+
const readGuardedFormFieldKeys = (references) => new Set((0, exports.readFormFieldKeys)(references.guardedFieldPaths));
|
|
1155
|
+
const lintWarnings = (references, label, context) => {
|
|
1156
|
+
const fields = readSchemaFields(context.formSchema);
|
|
1157
|
+
const fieldsByKey = new Map(fields.map((field) => [field.fieldKey, field]));
|
|
1158
|
+
const guarded = readGuardedFormFieldKeys(references);
|
|
1159
|
+
const optionalIssues = context.formSchema
|
|
1160
|
+
? references.formFieldKeys
|
|
1161
|
+
.filter((fieldKey) => fieldsByKey.get(fieldKey)?.required === false)
|
|
1162
|
+
.filter((fieldKey) => !guarded.has(fieldKey))
|
|
1163
|
+
.map((fieldKey) => issue('EXPR_OPTIONAL_FIELD_WITHOUT_HAS', label, fieldKey))
|
|
1164
|
+
: [];
|
|
1165
|
+
const orAbsorbs = (0, exports.readFormFieldKeys)(references.orOperandFieldPaths).some((fieldKey) => !guarded.has(fieldKey));
|
|
1166
|
+
const hasTested = new Set(references.hasArguments.map(readPathKey));
|
|
1167
|
+
const hasNestedParent = references.hasArguments.some((path) => path.segments.length >= 2 &&
|
|
1168
|
+
!hasTested.has(readPathKey({ ...path, segments: path.segments.slice(0, -1) })));
|
|
1169
|
+
const readsForm = references.roots.some((root) => FORM_ROOTS.has(root));
|
|
1170
|
+
const target = context.targetField;
|
|
1171
|
+
const hiddenCopies = context.usagePoint === 'SERVICE_FORM_FIELD_VALUE' &&
|
|
1172
|
+
target &&
|
|
1173
|
+
target.hidden !== true
|
|
1174
|
+
? references.formFieldKeys.filter((fieldKey) => fieldKey !== target.fieldKey &&
|
|
1175
|
+
fieldsByKey.get(fieldKey)?.hidden === true)
|
|
1176
|
+
: [];
|
|
1177
|
+
return [
|
|
1178
|
+
...optionalIssues,
|
|
1179
|
+
...(orAbsorbs ? [issue('EXPR_OR_ABSORBS_ERROR', label)] : []),
|
|
1180
|
+
...(hasNestedParent ? [issue('EXPR_HAS_NESTED_PARENT', label)] : []),
|
|
1181
|
+
...(context.usagePoint === 'APPROVER_RESOLVER' && readsForm
|
|
1182
|
+
? [issue('EXPR_APPROVER_READS_FORM', label)]
|
|
1183
|
+
: []),
|
|
1184
|
+
...hiddenCopies.map((fieldKey) => issue('EXPR_HIDDEN_FIELD_COPIED', label, fieldKey)),
|
|
1185
|
+
...(references.divisorFieldPaths.length > 0
|
|
1186
|
+
? [issue('EXPR_DIVISOR_IS_FIELD', label)]
|
|
1187
|
+
: []),
|
|
1188
|
+
];
|
|
1189
|
+
};
|
|
1190
|
+
/**
|
|
1191
|
+
* The cel-v2 lint of one expression (ADR 22 §3.4, docs/24 §2.2): errors that
|
|
1192
|
+
* refuse a publish and warnings that do not, in that order. The expression is
|
|
1193
|
+
* expected trimmed and within the length limit (`ConditionService` checks
|
|
1194
|
+
* both first).
|
|
1195
|
+
*
|
|
1196
|
+
* `environmentRegistry` is the registry evaluation will run with (the one
|
|
1197
|
+
* `ConditionService` was given): the expression is parsed and type-checked in
|
|
1198
|
+
* its cel-v2 environment, so the built-ins and the host's functions are
|
|
1199
|
+
* type-checked like the engine's own. Absent means the built-ins only. It is
|
|
1200
|
+
* separate from `context.functionRegistry`, which only answers availability
|
|
1201
|
+
* (`assertPublishable`, `get`) and need not be able to build an environment.
|
|
1202
|
+
*
|
|
1203
|
+
* The type check (`env.check()`) runs only when nothing structural is wrong
|
|
1204
|
+
* and every function called is one that environment knows; a call to one it
|
|
1205
|
+
* does not know is covered by `EXPR_FUNCTION_NOT_REGISTERED` instead.
|
|
1206
|
+
*/
|
|
1207
|
+
function lintCelV2Expression(expression, label, context = {}, environmentRegistry) {
|
|
1208
|
+
const lintEnvironment = readLintEnvironment(environmentRegistry);
|
|
1209
|
+
const parsed = readCelV2References(expression, lintEnvironment.environment);
|
|
1210
|
+
if (parsed.status !== 'OK') {
|
|
1211
|
+
return [
|
|
1212
|
+
issue(parsed.status === 'PARSE_FAILED' && parsed.limitExceeded
|
|
1213
|
+
? 'EXPR_COMPLEXITY_EXCEEDED'
|
|
1214
|
+
: 'EXPR_PARSE_FAILED', label),
|
|
1215
|
+
];
|
|
1216
|
+
}
|
|
1217
|
+
const { references } = parsed;
|
|
1218
|
+
const structural = lintStructure(references, label, context);
|
|
1219
|
+
const fieldIssues = lintFields(references, label, context.formSchema);
|
|
1220
|
+
const functions = lintFunctions(references, label, context, lintEnvironment);
|
|
1221
|
+
const errors = [...structural, ...fieldIssues, ...functions.issues];
|
|
1222
|
+
const typeIssues = errors.length === 0 && !functions.uncheckable
|
|
1223
|
+
? lintTypes(expression, label, context, lintEnvironment.environment)
|
|
1224
|
+
: [];
|
|
1225
|
+
return [
|
|
1226
|
+
...errors,
|
|
1227
|
+
...typeIssues,
|
|
1228
|
+
...lintWarnings(references, label, context),
|
|
1229
|
+
];
|
|
1230
|
+
}
|
|
1231
|
+
/**
|
|
1232
|
+
* The global function names an expression calls that cel-v2 does not provide
|
|
1233
|
+
* itself: the built-ins and host functions a rollback must find registered
|
|
1234
|
+
* (docs/24 §1). `null` when the expression cannot be read. Hand the result to
|
|
1235
|
+
* {@link readUnavailableExpressionFunctions}, which applies the same name
|
|
1236
|
+
* rule and registry isolation as the publish lint.
|
|
1237
|
+
*/
|
|
1238
|
+
function readExpressionHostFunctionNames(expression, dialect = expression_dialect_1.DEFAULT_EXPRESSION_DIALECT) {
|
|
1239
|
+
// cel-legacy has no function registration at all: nothing to look up.
|
|
1240
|
+
if (dialect !== 'cel-v2') {
|
|
1241
|
+
return [];
|
|
1242
|
+
}
|
|
1243
|
+
const parsed = readCelV2References(expression);
|
|
1244
|
+
if (parsed.status !== 'OK') {
|
|
1245
|
+
return null;
|
|
1246
|
+
}
|
|
1247
|
+
const engine = readEngineFunctionNames().globals;
|
|
1248
|
+
return [
|
|
1249
|
+
...new Set(parsed.references.functionCalls
|
|
1250
|
+
.filter((call) => !call.receiver && !engine.has(call.name))
|
|
1251
|
+
.map((call) => call.name)),
|
|
1252
|
+
];
|
|
1253
|
+
}
|
|
1254
|
+
//# sourceMappingURL=expression-ast.js.map
|