form0-core 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/builtins/builtin-metadata.js +44 -0
- package/src/builtins/choice/choicelabel.js +11 -0
- package/src/builtins/choice/choicelabels.js +11 -0
- package/src/builtins/choice/choicevalue.js +11 -0
- package/src/builtins/choice/choicevalues.js +11 -0
- package/src/builtins/choice/hasother.js +11 -0
- package/src/builtins/choice/other.js +11 -0
- package/src/builtins/control/eval.js +10 -0
- package/src/builtins/control/setresult.js +11 -0
- package/src/builtins/event/control/off.js +10 -0
- package/src/builtins/event/control/on.js +10 -0
- package/src/builtins/event/field/setvalue.js +10 -0
- package/src/builtins/event/ui/alert.js +10 -0
- package/src/builtins/logical/and.js +11 -0
- package/src/builtins/logical/array.js +11 -0
- package/src/builtins/logical/count.js +11 -0
- package/src/builtins/logical/counta.js +11 -0
- package/src/builtins/logical/countblank.js +12 -3
- package/src/builtins/logical/if.js +11 -0
- package/src/builtins/logical/or.js +11 -0
- package/src/builtins/math/abs.js +11 -0
- package/src/builtins/math/ceiling.js +11 -0
- package/src/builtins/math/cos.js +12 -1
- package/src/builtins/math/round.js +13 -2
- package/src/builtins/math/sin.js +12 -1
- package/src/builtins/registry.js +130 -92
- package/src/builtins/schema/datanames.js +10 -0
- package/src/builtins/schema/form.js +11 -0
- package/src/builtins/string/upper.js +12 -1
- package/src/engine/context-resolver.js +202 -56
- package/src/index.js +6 -0
- package/src/utilities/calculation-editor.js +534 -0
package/src/index.js
CHANGED
|
@@ -30,6 +30,12 @@ export {
|
|
|
30
30
|
} from './utilities/form-link-helpers.js';
|
|
31
31
|
export { expandBuildingPlanSchema } from './schema/building-plan-expander.js';
|
|
32
32
|
export { FIELD_SPECS } from './schema/field-specs.js';
|
|
33
|
+
export { getBuiltinDefinitions } from './builtins/registry.js';
|
|
34
|
+
export {
|
|
35
|
+
getCalculationBuiltinCatalog,
|
|
36
|
+
getCalculationReferenceCatalog,
|
|
37
|
+
analyzeCalculationExpression,
|
|
38
|
+
} from './utilities/calculation-editor.js';
|
|
33
39
|
export {
|
|
34
40
|
getFormAIMetadata,
|
|
35
41
|
getFormAIContext,
|
|
@@ -0,0 +1,534 @@
|
|
|
1
|
+
import { BUILTIN_CONTEXTS } from '../builtins/builtin-metadata.js';
|
|
2
|
+
import {
|
|
3
|
+
BUILTIN_DEFINITION_BY_NAME,
|
|
4
|
+
CALCULATION_BUILTIN_DEFINITIONS,
|
|
5
|
+
} from '../builtins/registry.js';
|
|
6
|
+
import { ContextResolver } from '../engine/context-resolver.js';
|
|
7
|
+
import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
|
|
8
|
+
import { validateExpression } from '../security/validation.js';
|
|
9
|
+
import { flattenFields } from './field-helpers.js';
|
|
10
|
+
|
|
11
|
+
const COMMON_GLOBAL_FUNCTIONS = new Set([
|
|
12
|
+
'Math',
|
|
13
|
+
'Date',
|
|
14
|
+
'JSON',
|
|
15
|
+
'Number',
|
|
16
|
+
'String',
|
|
17
|
+
'Array',
|
|
18
|
+
'Object',
|
|
19
|
+
'Boolean',
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const CALCULATION_BUILTIN_STATUS_BY_NAME = Object.freeze({
|
|
23
|
+
EVAL: 'advanced',
|
|
24
|
+
FORM: 'unavailable',
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
function getCalculationBuiltinStatus(name) {
|
|
28
|
+
return CALCULATION_BUILTIN_STATUS_BY_NAME[name] ?? 'stable';
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function toCalculationBuiltinDefinition(definition) {
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
name: definition.name,
|
|
34
|
+
category: definition.category,
|
|
35
|
+
signature: definition.signature,
|
|
36
|
+
description: definition.description,
|
|
37
|
+
examples: Object.freeze([...definition.examples]),
|
|
38
|
+
status: getCalculationBuiltinStatus(definition.name),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const CALCULATION_BUILTIN_CATALOG = Object.freeze(
|
|
43
|
+
CALCULATION_BUILTIN_DEFINITIONS.map(toCalculationBuiltinDefinition)
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const CALCULATION_BUILTIN_METADATA_BY_NAME = new Map(
|
|
47
|
+
CALCULATION_BUILTIN_CATALOG.map((definition) => [definition.name, definition])
|
|
48
|
+
);
|
|
49
|
+
|
|
50
|
+
function normalizeFormSchema(schema) {
|
|
51
|
+
if (schema && Array.isArray(schema.elements)) {
|
|
52
|
+
return schema;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (schema && schema.form && Array.isArray(schema.form.elements)) {
|
|
56
|
+
return schema.form;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error('Expected a form schema or a root schema containing form.elements');
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function isBuiltinCallMatchAllowed(functionName) {
|
|
63
|
+
return COMMON_GLOBAL_FUNCTIONS.has(functionName);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function removeEvalContents(code) {
|
|
67
|
+
let result = '';
|
|
68
|
+
let i = 0;
|
|
69
|
+
|
|
70
|
+
while (i < code.length) {
|
|
71
|
+
const evalMatch = code.substring(i).match(/^EVAL\s*\(/);
|
|
72
|
+
if (evalMatch) {
|
|
73
|
+
result += 'EVAL()';
|
|
74
|
+
i += evalMatch[0].length;
|
|
75
|
+
|
|
76
|
+
let parenCount = 1;
|
|
77
|
+
let inSingleQuote = false;
|
|
78
|
+
let inDoubleQuote = false;
|
|
79
|
+
let inBacktick = false;
|
|
80
|
+
|
|
81
|
+
while (i < code.length && parenCount > 0) {
|
|
82
|
+
const char = code[i];
|
|
83
|
+
|
|
84
|
+
if (char === "'" && !inDoubleQuote && !inBacktick) {
|
|
85
|
+
inSingleQuote = !inSingleQuote;
|
|
86
|
+
} else if (char === '"' && !inSingleQuote && !inBacktick) {
|
|
87
|
+
inDoubleQuote = !inDoubleQuote;
|
|
88
|
+
} else if (char === '`' && !inSingleQuote && !inDoubleQuote) {
|
|
89
|
+
inBacktick = !inBacktick;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (!inSingleQuote && !inDoubleQuote && !inBacktick) {
|
|
93
|
+
if (char === '(') {
|
|
94
|
+
parenCount++;
|
|
95
|
+
} else if (char === ')') {
|
|
96
|
+
parenCount--;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
i++;
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
result += code[i];
|
|
104
|
+
i++;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return result;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function extractFunctionCalls(code) {
|
|
112
|
+
const functionCallPattern = /\b([A-Z_][A-Z0-9_]*)\s*\(/g;
|
|
113
|
+
const matches = [];
|
|
114
|
+
let match;
|
|
115
|
+
|
|
116
|
+
while ((match = functionCallPattern.exec(code)) !== null) {
|
|
117
|
+
matches.push({
|
|
118
|
+
name: match[1],
|
|
119
|
+
index: match.index,
|
|
120
|
+
length: match[1].length,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return matches;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function extractFieldReferenceMatches(code) {
|
|
128
|
+
const cleanedCode = removeEvalContents(code);
|
|
129
|
+
const fieldRegex = /\$([a-zA-Z_][a-zA-Z0-9_]*)/g;
|
|
130
|
+
const matches = [];
|
|
131
|
+
let match;
|
|
132
|
+
|
|
133
|
+
while ((match = fieldRegex.exec(cleanedCode)) !== null) {
|
|
134
|
+
matches.push({
|
|
135
|
+
fieldName: match[1],
|
|
136
|
+
reference: `$${match[1]}`,
|
|
137
|
+
index: match.index,
|
|
138
|
+
length: match[0].length,
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return matches;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function buildExecutionContext(form, fieldDataName, resolver) {
|
|
146
|
+
return {
|
|
147
|
+
type: 'calculation',
|
|
148
|
+
fieldName: fieldDataName,
|
|
149
|
+
parentPath: resolver.getFieldInfo(fieldDataName)?.parentPath || [],
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function cloneCatalogDefinition(definition) {
|
|
154
|
+
return {
|
|
155
|
+
...definition,
|
|
156
|
+
examples: [...definition.examples],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function toCalculationReferenceAccess(accessInfo) {
|
|
161
|
+
return {
|
|
162
|
+
level: accessInfo.level,
|
|
163
|
+
code: accessInfo.code,
|
|
164
|
+
message: accessInfo.message,
|
|
165
|
+
suggestion: accessInfo.suggestion,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function toCalculationReferenceDefinition(field, resolver, executionContext) {
|
|
170
|
+
const accessInfo = resolver.resolveFieldAccessInfo(executionContext, field.data_name);
|
|
171
|
+
|
|
172
|
+
if (accessInfo.level === 'not_found') {
|
|
173
|
+
return null;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
key: typeof field.key === 'string' ? field.key : field.data_name,
|
|
178
|
+
reference: `$${field.data_name}`,
|
|
179
|
+
dataName: field.data_name,
|
|
180
|
+
label: typeof field.label === 'string' ? field.label : field.data_name,
|
|
181
|
+
type: typeof field.type === 'string' ? field.type : 'UnknownField',
|
|
182
|
+
description: typeof field.description === 'string' ? field.description : null,
|
|
183
|
+
displayStyle:
|
|
184
|
+
field.display &&
|
|
185
|
+
typeof field.display === 'object' &&
|
|
186
|
+
typeof field.display.style === 'string'
|
|
187
|
+
? field.display.style
|
|
188
|
+
: typeof field.display === 'string'
|
|
189
|
+
? field.display
|
|
190
|
+
: null,
|
|
191
|
+
parentPath: resolver.getFieldInfo(field.data_name)?.parentPath || [],
|
|
192
|
+
access: toCalculationReferenceAccess(accessInfo),
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function createIssue({ code, severity, message, symbol = null, index = null, length = null }) {
|
|
197
|
+
return {
|
|
198
|
+
code,
|
|
199
|
+
severity,
|
|
200
|
+
message,
|
|
201
|
+
symbol,
|
|
202
|
+
startIndex: typeof index === 'number' ? index : null,
|
|
203
|
+
endIndex: typeof index === 'number' && typeof length === 'number' ? index + length : null,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function addIssue(issues, issueKeys, issue) {
|
|
208
|
+
const key = `${issue.code}:${issue.symbol || issue.message}`;
|
|
209
|
+
if (issueKeys.has(key)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
issueKeys.add(key);
|
|
213
|
+
issues.push(issue);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function validateSyntax(expression) {
|
|
217
|
+
const isMultiLine =
|
|
218
|
+
expression.includes('\r\n') || expression.includes('\n') || expression.includes('function');
|
|
219
|
+
|
|
220
|
+
if (isMultiLine) {
|
|
221
|
+
new Function(expression);
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
new Function(`return (${expression});`);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function createSchemaFilteredSecurityConfig(securityConfig) {
|
|
229
|
+
if (!securityConfig || typeof securityConfig !== 'object') {
|
|
230
|
+
return DEFAULT_SECURITY_CONFIG;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
...securityConfig,
|
|
235
|
+
validateBuiltins: false,
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* @typedef {'stable' | 'advanced' | 'unavailable'} CalculationBuiltinStatus
|
|
241
|
+
*
|
|
242
|
+
* @typedef {Object} CalculationBuiltinDefinition
|
|
243
|
+
* @property {string} name
|
|
244
|
+
* @property {string} category
|
|
245
|
+
* @property {string} signature
|
|
246
|
+
* @property {string} description
|
|
247
|
+
* @property {string[]} examples
|
|
248
|
+
* @property {CalculationBuiltinStatus} status
|
|
249
|
+
*
|
|
250
|
+
* @typedef {Object} CalculationReferenceDefinition
|
|
251
|
+
* @property {string} key
|
|
252
|
+
* @property {string} reference
|
|
253
|
+
* @property {string} dataName
|
|
254
|
+
* @property {string} label
|
|
255
|
+
* @property {string} type
|
|
256
|
+
* @property {string | null} description
|
|
257
|
+
* @property {string | null} displayStyle
|
|
258
|
+
* @property {string[]} parentPath
|
|
259
|
+
* @property {{
|
|
260
|
+
* level: 'accessible' | 'restricted',
|
|
261
|
+
* code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section',
|
|
262
|
+
* message: string | null,
|
|
263
|
+
* suggestion: string | null,
|
|
264
|
+
* }} access
|
|
265
|
+
*
|
|
266
|
+
* @typedef {Object} CalculationExpressionIssue
|
|
267
|
+
* @property {string} code
|
|
268
|
+
* @property {'error' | 'warning'} severity
|
|
269
|
+
* @property {string} message
|
|
270
|
+
* @property {string | null} symbol
|
|
271
|
+
* @property {number | null} startIndex
|
|
272
|
+
* @property {number | null} endIndex
|
|
273
|
+
*/
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Return the canonical builtin catalog for CalculatedField authoring.
|
|
277
|
+
* Event-only builtins are intentionally excluded.
|
|
278
|
+
*
|
|
279
|
+
* @returns {CalculationBuiltinDefinition[]}
|
|
280
|
+
*/
|
|
281
|
+
export function getCalculationBuiltinCatalog() {
|
|
282
|
+
return CALCULATION_BUILTIN_CATALOG.map(cloneCatalogDefinition);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Return the list of field references accessible from the given CalculatedField.
|
|
287
|
+
*
|
|
288
|
+
* @param {Object} options
|
|
289
|
+
* @param {Object} options.schema
|
|
290
|
+
* @param {string} options.fieldDataName
|
|
291
|
+
* @param {boolean} [options.includeRestricted=false]
|
|
292
|
+
* @returns {CalculationReferenceDefinition[]}
|
|
293
|
+
*/
|
|
294
|
+
export function getCalculationReferenceCatalog({
|
|
295
|
+
schema,
|
|
296
|
+
fieldDataName,
|
|
297
|
+
includeRestricted = false,
|
|
298
|
+
}) {
|
|
299
|
+
const form = normalizeFormSchema(schema);
|
|
300
|
+
const resolver = new ContextResolver(form);
|
|
301
|
+
const executionContext = buildExecutionContext(form, fieldDataName, resolver);
|
|
302
|
+
|
|
303
|
+
return flattenFields(form.elements)
|
|
304
|
+
.filter((field) => field && typeof field === 'object')
|
|
305
|
+
.filter((field) => field.data_name && field.data_name !== fieldDataName)
|
|
306
|
+
.filter(
|
|
307
|
+
(field) =>
|
|
308
|
+
field.type !== 'Section' &&
|
|
309
|
+
field.type !== 'RepeatableSection' &&
|
|
310
|
+
field.type !== 'BuildingPlanSection'
|
|
311
|
+
)
|
|
312
|
+
.map((field) => toCalculationReferenceDefinition(field, resolver, executionContext))
|
|
313
|
+
.filter((reference) => reference !== null)
|
|
314
|
+
.filter((reference) => includeRestricted || reference.access.level === 'accessible');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Statistically analyze a CalculatedField expression without executing it.
|
|
319
|
+
*
|
|
320
|
+
* @param {Object} options
|
|
321
|
+
* @param {string} options.expression
|
|
322
|
+
* @param {Object} options.schema
|
|
323
|
+
* @param {string} options.fieldDataName
|
|
324
|
+
* @param {Object} [options.securityConfig]
|
|
325
|
+
* @returns {{
|
|
326
|
+
* valid: boolean,
|
|
327
|
+
* issues: CalculationExpressionIssue[],
|
|
328
|
+
* usedBuiltins: string[],
|
|
329
|
+
* referencedFields: string[],
|
|
330
|
+
* }}
|
|
331
|
+
*/
|
|
332
|
+
export function analyzeCalculationExpression({
|
|
333
|
+
expression,
|
|
334
|
+
schema,
|
|
335
|
+
fieldDataName,
|
|
336
|
+
securityConfig = DEFAULT_SECURITY_CONFIG,
|
|
337
|
+
}) {
|
|
338
|
+
const issues = [];
|
|
339
|
+
const issueKeys = new Set();
|
|
340
|
+
const usedBuiltins = [];
|
|
341
|
+
const usedBuiltinNames = new Set();
|
|
342
|
+
const referencedFields = [];
|
|
343
|
+
const referencedFieldNames = new Set();
|
|
344
|
+
|
|
345
|
+
const normalizedExpression = typeof expression === 'string' ? expression : '';
|
|
346
|
+
const form = normalizeFormSchema(schema);
|
|
347
|
+
const resolver = new ContextResolver(form);
|
|
348
|
+
const executionContext = buildExecutionContext(form, fieldDataName, resolver);
|
|
349
|
+
|
|
350
|
+
if (normalizedExpression.trim().length === 0) {
|
|
351
|
+
addIssue(
|
|
352
|
+
issues,
|
|
353
|
+
issueKeys,
|
|
354
|
+
createIssue({
|
|
355
|
+
code: 'empty_expression',
|
|
356
|
+
severity: 'error',
|
|
357
|
+
message: 'Calculation expression cannot be empty.',
|
|
358
|
+
index: 0,
|
|
359
|
+
length: 1,
|
|
360
|
+
})
|
|
361
|
+
);
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
try {
|
|
365
|
+
validateSyntax(normalizedExpression);
|
|
366
|
+
} catch (error) {
|
|
367
|
+
addIssue(
|
|
368
|
+
issues,
|
|
369
|
+
issueKeys,
|
|
370
|
+
createIssue({
|
|
371
|
+
code: 'invalid_syntax',
|
|
372
|
+
severity: 'error',
|
|
373
|
+
message:
|
|
374
|
+
error instanceof Error && error.message
|
|
375
|
+
? `Invalid calculation syntax: ${error.message}`
|
|
376
|
+
: 'Invalid calculation syntax.',
|
|
377
|
+
index: 0,
|
|
378
|
+
length: 1,
|
|
379
|
+
})
|
|
380
|
+
);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const securityValidation = validateExpression(
|
|
384
|
+
normalizedExpression,
|
|
385
|
+
createSchemaFilteredSecurityConfig(securityConfig),
|
|
386
|
+
false
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
if (!securityValidation.valid) {
|
|
390
|
+
addIssue(
|
|
391
|
+
issues,
|
|
392
|
+
issueKeys,
|
|
393
|
+
createIssue({
|
|
394
|
+
code: 'security_validation_failed',
|
|
395
|
+
severity: 'error',
|
|
396
|
+
message: securityValidation.reason,
|
|
397
|
+
index: 0,
|
|
398
|
+
length: 1,
|
|
399
|
+
})
|
|
400
|
+
);
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
for (const call of extractFunctionCalls(normalizedExpression)) {
|
|
404
|
+
if (isBuiltinCallMatchAllowed(call.name)) {
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
const builtinDefinition = BUILTIN_DEFINITION_BY_NAME.get(call.name);
|
|
409
|
+
if (builtinDefinition) {
|
|
410
|
+
const isCalculationBuiltin = builtinDefinition.contexts.includes(
|
|
411
|
+
BUILTIN_CONTEXTS.CALCULATION
|
|
412
|
+
);
|
|
413
|
+
|
|
414
|
+
if (!isCalculationBuiltin && builtinDefinition.contexts.includes(BUILTIN_CONTEXTS.EVENT)) {
|
|
415
|
+
addIssue(
|
|
416
|
+
issues,
|
|
417
|
+
issueKeys,
|
|
418
|
+
createIssue({
|
|
419
|
+
code: 'event_builtin_not_allowed',
|
|
420
|
+
severity: 'error',
|
|
421
|
+
message: `${call.name}() is only available in form event handlers, not in CalculatedField expressions.`,
|
|
422
|
+
symbol: call.name,
|
|
423
|
+
index: call.index,
|
|
424
|
+
length: call.length,
|
|
425
|
+
})
|
|
426
|
+
);
|
|
427
|
+
continue;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const builtinMetadata = CALCULATION_BUILTIN_METADATA_BY_NAME.get(call.name);
|
|
431
|
+
if (!builtinMetadata) {
|
|
432
|
+
continue;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
if (!usedBuiltinNames.has(call.name)) {
|
|
436
|
+
usedBuiltinNames.add(call.name);
|
|
437
|
+
usedBuiltins.push(call.name);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
if (builtinMetadata.status === 'unavailable') {
|
|
441
|
+
addIssue(
|
|
442
|
+
issues,
|
|
443
|
+
issueKeys,
|
|
444
|
+
createIssue({
|
|
445
|
+
code: 'unavailable_builtin',
|
|
446
|
+
severity: 'error',
|
|
447
|
+
message: `${call.name}() is not available in CalculatedField expressions.`,
|
|
448
|
+
symbol: call.name,
|
|
449
|
+
index: call.index,
|
|
450
|
+
length: call.length,
|
|
451
|
+
})
|
|
452
|
+
);
|
|
453
|
+
} else if (builtinMetadata.status === 'advanced') {
|
|
454
|
+
addIssue(
|
|
455
|
+
issues,
|
|
456
|
+
issueKeys,
|
|
457
|
+
createIssue({
|
|
458
|
+
code: 'advanced_builtin',
|
|
459
|
+
severity: 'warning',
|
|
460
|
+
message: `${call.name}() is an advanced builtin. Prefer direct $field references when possible.`,
|
|
461
|
+
symbol: call.name,
|
|
462
|
+
index: call.index,
|
|
463
|
+
length: call.length,
|
|
464
|
+
})
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
addIssue(
|
|
472
|
+
issues,
|
|
473
|
+
issueKeys,
|
|
474
|
+
createIssue({
|
|
475
|
+
code: 'unknown_builtin',
|
|
476
|
+
severity: 'error',
|
|
477
|
+
message: `Unknown builtin function: ${call.name}.`,
|
|
478
|
+
symbol: call.name,
|
|
479
|
+
index: call.index,
|
|
480
|
+
length: call.length,
|
|
481
|
+
})
|
|
482
|
+
);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
for (const reference of extractFieldReferenceMatches(normalizedExpression)) {
|
|
486
|
+
if (!referencedFieldNames.has(reference.fieldName)) {
|
|
487
|
+
referencedFieldNames.add(reference.fieldName);
|
|
488
|
+
referencedFields.push(reference.fieldName);
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
const accessInfo = resolver.resolveFieldAccessInfo(executionContext, reference.fieldName);
|
|
492
|
+
if (accessInfo.level === 'restricted') {
|
|
493
|
+
addIssue(
|
|
494
|
+
issues,
|
|
495
|
+
issueKeys,
|
|
496
|
+
createIssue({
|
|
497
|
+
code: 'restricted_field_reference',
|
|
498
|
+
severity: 'error',
|
|
499
|
+
message:
|
|
500
|
+
accessInfo.suggestion ||
|
|
501
|
+
accessInfo.message ||
|
|
502
|
+
resolver.generateAccessSuggestion(
|
|
503
|
+
executionContext,
|
|
504
|
+
reference.fieldName,
|
|
505
|
+
resolver.getFieldInfo(reference.fieldName)
|
|
506
|
+
),
|
|
507
|
+
symbol: reference.reference,
|
|
508
|
+
index: reference.index,
|
|
509
|
+
length: reference.length,
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
} else if (accessInfo.level === 'not_found') {
|
|
513
|
+
addIssue(
|
|
514
|
+
issues,
|
|
515
|
+
issueKeys,
|
|
516
|
+
createIssue({
|
|
517
|
+
code: 'unknown_field_reference',
|
|
518
|
+
severity: 'error',
|
|
519
|
+
message: `Field '${reference.fieldName}' does not exist in the form schema.`,
|
|
520
|
+
symbol: reference.reference,
|
|
521
|
+
index: reference.index,
|
|
522
|
+
length: reference.length,
|
|
523
|
+
})
|
|
524
|
+
);
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
valid: !issues.some((issue) => issue.severity === 'error'),
|
|
530
|
+
issues,
|
|
531
|
+
usedBuiltins,
|
|
532
|
+
referencedFields,
|
|
533
|
+
};
|
|
534
|
+
}
|