form0-core 0.1.3 → 0.1.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "form0-core",
3
- "version": "0.1.3",
3
+ "version": "0.1.5",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org/"
6
6
  },
@@ -66,25 +66,54 @@ export class ContextResolver {
66
66
  }
67
67
 
68
68
  /**
69
- * Determine if a field is accessible from the current execution context
69
+ * Resolve field access with structured metadata for editor integrations.
70
70
  * @param {Object} executionContext - Current execution context
71
71
  * @param {string} fieldName - Field data_name being accessed
72
- * @returns {string} 'accessible', 'restricted', or 'not_found'
72
+ * @param {Object | null} [providedFieldInfo]
73
+ * @returns {{
74
+ * level: 'accessible' | 'restricted' | 'not_found',
75
+ * code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section' | null,
76
+ * message: string | null,
77
+ * suggestion: string | null,
78
+ * fieldInfo: Object | null,
79
+ * }}
73
80
  */
74
- resolveFieldAccess(executionContext, fieldName) {
75
- const fieldInfo = this.fieldOwnership.get(fieldName);
81
+ resolveFieldAccessInfo(executionContext, fieldName, providedFieldInfo = null) {
82
+ const fieldInfo = providedFieldInfo || this.fieldOwnership.get(fieldName) || null;
76
83
 
77
84
  if (!fieldInfo) {
78
- return 'not_found';
85
+ return {
86
+ level: 'not_found',
87
+ code: null,
88
+ message: `Field '${fieldName}' does not exist in the form schema`,
89
+ suggestion: `Check that field '${fieldName}' exists in the form schema`,
90
+ fieldInfo: null,
91
+ };
79
92
  }
80
93
 
81
94
  // Main form fields (no parent RepeatableSection) are always accessible
82
95
  if (fieldInfo.parentPath.length === 0) {
83
- return 'accessible';
96
+ return this.buildAccessInfo(
97
+ 'accessible',
98
+ 'main_form',
99
+ fieldName,
100
+ fieldInfo,
101
+ executionContext
102
+ );
84
103
  }
85
104
 
86
105
  // RepeatableSection field access depends on execution context
87
- return this.checkRepeatableSectionAccess(executionContext, fieldInfo);
106
+ return this.checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo);
107
+ }
108
+
109
+ /**
110
+ * Determine if a field is accessible from the current execution context
111
+ * @param {Object} executionContext - Current execution context
112
+ * @param {string} fieldName - Field data_name being accessed
113
+ * @returns {string} 'accessible', 'restricted', or 'not_found'
114
+ */
115
+ resolveFieldAccess(executionContext, fieldName) {
116
+ return this.resolveFieldAccessInfo(executionContext, fieldName).level;
88
117
  }
89
118
 
90
119
  /**
@@ -94,22 +123,49 @@ export class ContextResolver {
94
123
  * @returns {string} 'accessible' or 'restricted'
95
124
  */
96
125
  checkRepeatableSectionAccess(executionContext, fieldInfo) {
126
+ return this.checkRepeatableSectionAccessInfo(
127
+ executionContext,
128
+ fieldInfo.field?.data_name || fieldInfo.preferredKey,
129
+ fieldInfo
130
+ ).level;
131
+ }
132
+
133
+ /**
134
+ * Check RepeatableSection field access and return structured access metadata.
135
+ * @param {Object} executionContext - Current execution context
136
+ * @param {string} fieldName - Field data_name being accessed
137
+ * @param {Object} fieldInfo - Field ownership information
138
+ * @returns {{
139
+ * level: 'accessible' | 'restricted' | 'not_found',
140
+ * code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section' | null,
141
+ * message: string | null,
142
+ * suggestion: string | null,
143
+ * fieldInfo: Object | null,
144
+ * }}
145
+ */
146
+ checkRepeatableSectionAccessInfo(executionContext, fieldName, fieldInfo) {
97
147
  const { type: contextType, eventType, fieldName: contextFieldName } = executionContext;
98
148
 
99
149
  if (contextType === 'calculation') {
100
150
  // CalculatedField can only access fields in same RepeatableSection context
101
151
  const calculationFieldInfo = this.fieldOwnership.get(contextFieldName);
102
152
  if (!calculationFieldInfo) {
103
- return 'restricted'; // Unknown calculation field
153
+ return this.buildAccessInfo(
154
+ 'restricted',
155
+ 'different_repeatable_section',
156
+ fieldName,
157
+ fieldInfo,
158
+ executionContext
159
+ );
104
160
  }
105
161
 
106
- // Check if both fields have the same RepeatableSection ancestry
107
- return this.haveSameRepeatableSectionContext(
162
+ return this.resolveRepeatableSectionRelationship(
108
163
  calculationFieldInfo.parentPath,
109
- fieldInfo.parentPath
110
- )
111
- ? 'accessible'
112
- : 'restricted';
164
+ fieldInfo.parentPath,
165
+ fieldName,
166
+ fieldInfo,
167
+ executionContext
168
+ );
113
169
  }
114
170
 
115
171
  if (contextType === 'event') {
@@ -118,38 +174,75 @@ export class ContextResolver {
118
174
  switch (eventInfo.scope) {
119
175
  case 'main':
120
176
  // Record events can't access RepeatableSection fields
121
- return 'restricted';
177
+ return this.buildAccessInfo(
178
+ 'restricted',
179
+ 'different_repeatable_section',
180
+ fieldName,
181
+ fieldInfo,
182
+ executionContext
183
+ );
122
184
 
123
185
  case 'contextual':
124
186
  // Field events can access fields in same RepeatableSection context
125
187
  if (!contextFieldName) {
126
- return 'restricted'; // No field context
188
+ return this.buildAccessInfo(
189
+ 'restricted',
190
+ 'different_repeatable_section',
191
+ fieldName,
192
+ fieldInfo,
193
+ executionContext
194
+ );
127
195
  }
128
196
 
129
197
  const triggerFieldInfo = this.fieldOwnership.get(contextFieldName);
130
198
  if (!triggerFieldInfo) {
131
- return 'restricted'; // Unknown trigger field
199
+ return this.buildAccessInfo(
200
+ 'restricted',
201
+ 'different_repeatable_section',
202
+ fieldName,
203
+ fieldInfo,
204
+ executionContext
205
+ );
132
206
  }
133
207
 
134
- return this.haveSameRepeatableSectionContext(
208
+ return this.resolveRepeatableSectionRelationship(
135
209
  triggerFieldInfo.parentPath,
136
- fieldInfo.parentPath
137
- )
138
- ? 'accessible'
139
- : 'restricted';
210
+ fieldInfo.parentPath,
211
+ fieldName,
212
+ fieldInfo,
213
+ executionContext
214
+ );
140
215
 
141
216
  case 'repeatable':
142
217
  // RepeatableSection events - future implementation
143
218
  // For now, assume accessible (will be refined when implemented)
144
- return 'accessible';
219
+ return this.buildAccessInfo(
220
+ 'accessible',
221
+ 'same_repeatable_section',
222
+ fieldName,
223
+ fieldInfo,
224
+ executionContext
225
+ );
145
226
 
146
227
  default:
147
- return 'restricted';
228
+ return this.buildAccessInfo(
229
+ 'restricted',
230
+ 'different_repeatable_section',
231
+ fieldName,
232
+ fieldInfo,
233
+ executionContext
234
+ );
148
235
  }
149
236
  }
150
237
 
151
238
  // Unknown context type
152
- return 'restricted';
239
+ return this.buildAccessInfo(
240
+ 'restricted',
241
+ 'different_repeatable_section',
242
+ fieldName,
243
+ fieldInfo,
244
+ executionContext
245
+ );
153
246
  }
154
247
 
155
248
  /**
@@ -159,25 +252,118 @@ export class ContextResolver {
159
252
  * @returns {boolean} True if they share the same RepeatableSection context
160
253
  */
161
254
  haveSameRepeatableSectionContext(parentPath1, parentPath2) {
162
- const contextPath = parentPath1;
163
- const targetPath = parentPath2;
255
+ return (
256
+ this.resolveRepeatableSectionAccessCode(parentPath1, parentPath2) !==
257
+ 'different_repeatable_section'
258
+ );
259
+ }
164
260
 
165
- // Main form calculations can only access other main form fields
166
- if (contextPath.length === 0) {
167
- return targetPath.length === 0;
261
+ resolveRepeatableSectionRelationship(
262
+ contextPath,
263
+ targetPath,
264
+ fieldName,
265
+ fieldInfo,
266
+ executionContext
267
+ ) {
268
+ const code = this.resolveRepeatableSectionAccessCode(contextPath, targetPath);
269
+ const level = code === 'different_repeatable_section' ? 'restricted' : 'accessible';
270
+
271
+ return this.buildAccessInfo(level, code, fieldName, fieldInfo, executionContext);
272
+ }
273
+
274
+ resolveRepeatableSectionAccessCode(contextPath, targetPath) {
275
+ if (targetPath.length === 0) {
276
+ return 'main_form';
168
277
  }
169
278
 
170
- // Child context can access its own fields
171
279
  if (ContextResolver.pathsEqual(contextPath, targetPath)) {
172
- return true;
280
+ return contextPath.length === 0 ? 'main_form' : 'same_repeatable_section';
173
281
  }
174
282
 
175
- // Child context can access ancestors (including main form)
176
- if (targetPath.length < contextPath.length && ContextResolver.isPrefix(targetPath, contextPath)) {
177
- return true;
283
+ if (
284
+ targetPath.length < contextPath.length &&
285
+ ContextResolver.isPrefix(targetPath, contextPath)
286
+ ) {
287
+ return targetPath.length === 0 ? 'main_form' : 'ancestor_repeatable_context';
288
+ }
289
+
290
+ return 'different_repeatable_section';
291
+ }
292
+
293
+ buildAccessInfo(level, code, fieldName, fieldInfo, executionContext) {
294
+ if (level === 'accessible') {
295
+ return {
296
+ level,
297
+ code,
298
+ message: null,
299
+ suggestion: null,
300
+ fieldInfo,
301
+ };
302
+ }
303
+
304
+ return {
305
+ level,
306
+ code,
307
+ message: `Field '${fieldName}' is not accessible from current context`,
308
+ suggestion: this.buildRestrictedAccessSuggestion(
309
+ executionContext,
310
+ fieldName,
311
+ fieldInfo,
312
+ code
313
+ ),
314
+ fieldInfo,
315
+ };
316
+ }
317
+
318
+ buildRestrictedAccessSuggestion(executionContext, fieldName, fieldInfo, code) {
319
+ const { type: contextType, eventType } = executionContext;
320
+
321
+ if (contextType === 'calculation') {
322
+ if (code === 'different_repeatable_section') {
323
+ return `CalculatedField can only access fields in the same RepeatableSection or ancestor contexts. Field '${fieldName}' is in RepeatableSection: ${this.formatRepeatableSectionPath(fieldInfo.parentPath)}`;
324
+ }
178
325
  }
179
326
 
180
- return false;
327
+ if (contextType === 'event') {
328
+ const eventInfo = getEventInfo(eventType);
329
+
330
+ if (eventInfo.scope === 'main') {
331
+ return `Record-level events (like '${eventType}') can only access main form fields, not RepeatableSection fields`;
332
+ }
333
+
334
+ if (eventInfo.scope === 'contextual') {
335
+ return `Field events can only access fields in the same RepeatableSection or ancestor contexts as the triggering field`;
336
+ }
337
+ }
338
+
339
+ return 'Check the field access rules for your current context';
340
+ }
341
+
342
+ formatRepeatableSectionPath(parentPath) {
343
+ if (!Array.isArray(parentPath) || parentPath.length === 0) {
344
+ return 'main form';
345
+ }
346
+
347
+ return parentPath
348
+ .map((_, index) => this.formatRepeatableSectionSegment(parentPath.slice(0, index + 1)))
349
+ .join(' -> ');
350
+ }
351
+
352
+ formatRepeatableSectionSegment(currentPath) {
353
+ for (const treeInfo of this.repeatableSectionTree.values()) {
354
+ if (!ContextResolver.pathsEqual(treeInfo.currentPath, currentPath)) {
355
+ continue;
356
+ }
357
+
358
+ const dataName = treeInfo.field?.data_name || treeInfo.preferredKey;
359
+ if (dataName === treeInfo.preferredKey) {
360
+ return dataName;
361
+ }
362
+
363
+ return `${dataName} (${treeInfo.preferredKey})`;
364
+ }
365
+
366
+ return currentPath[currentPath.length - 1];
181
367
  }
182
368
 
183
369
  static isPrefix(prefix, fullPath) {
@@ -221,19 +407,26 @@ export class ContextResolver {
221
407
  * @returns {Object} Warning object with structured information
222
408
  */
223
409
  generateAccessWarning(executionContext, fieldName, reason) {
224
- const fieldInfo = this.fieldOwnership.get(fieldName);
225
- const suggestion = this.generateAccessSuggestion(executionContext, fieldName, fieldInfo);
410
+ const accessInfo =
411
+ reason === 'not_found'
412
+ ? this.resolveFieldAccessInfo(executionContext, fieldName)
413
+ : this.resolveFieldAccessInfo(executionContext, fieldName);
414
+ const fieldInfo = accessInfo.fieldInfo;
226
415
 
227
416
  return {
228
417
  type: 'FIELD_ACCESS_WARNING',
229
418
  fieldName,
230
419
  executionContext,
231
420
  reason,
421
+ accessCode: accessInfo.code,
232
422
  message:
233
- reason === 'not_found'
423
+ accessInfo.message ||
424
+ (reason === 'not_found'
234
425
  ? `Field '${fieldName}' does not exist in the form schema`
235
- : `Field '${fieldName}' is not accessible from current context`,
236
- suggestion,
426
+ : `Field '${fieldName}' is not accessible from current context`),
427
+ suggestion:
428
+ accessInfo.suggestion ||
429
+ this.generateAccessSuggestion(executionContext, fieldName, fieldInfo),
237
430
  fieldContext: fieldInfo
238
431
  ? {
239
432
  parentPath: fieldInfo.parentPath,
@@ -252,28 +445,26 @@ export class ContextResolver {
252
445
  * @returns {string} Helpful suggestion message
253
446
  */
254
447
  generateAccessSuggestion(executionContext, fieldName, fieldInfo) {
255
- if (!fieldInfo) {
256
- return `Check that field '${fieldName}' exists in the form schema`;
448
+ const accessInfo = this.resolveFieldAccessInfo(executionContext, fieldName, fieldInfo);
449
+
450
+ if (accessInfo.suggestion) {
451
+ return accessInfo.suggestion;
257
452
  }
258
453
 
259
- const { type: contextType, eventType, fieldName: contextFieldName } = executionContext;
454
+ if (accessInfo.level === 'not_found') {
455
+ return `Check that field '${fieldName}' exists in the form schema`;
456
+ }
260
457
 
261
- if (contextType === 'calculation') {
262
- if (fieldInfo.parentPath.length === 0) {
263
- return 'Main form fields are always accessible from calculations';
264
- } else {
265
- return `CalculatedField can only access fields in the same RepeatableSection. Field '${fieldName}' is in RepeatableSection: ${fieldInfo.parentPath.join(' -> ')}`;
266
- }
458
+ if (accessInfo.code === 'main_form') {
459
+ return 'Main form fields are always accessible from calculations';
267
460
  }
268
461
 
269
- if (contextType === 'event') {
270
- const eventInfo = getEventInfo(eventType);
462
+ if (accessInfo.code === 'same_repeatable_section') {
463
+ return 'Fields in the same RepeatableSection are accessible from this context';
464
+ }
271
465
 
272
- if (eventInfo.scope === 'main') {
273
- return `Record-level events (like '${eventType}') can only access main form fields, not RepeatableSection fields`;
274
- } else if (eventInfo.scope === 'contextual') {
275
- return `Field events can only access fields in the same RepeatableSection context as the triggering field`;
276
- }
466
+ if (accessInfo.code === 'ancestor_repeatable_context') {
467
+ return 'Fields in ancestor RepeatableSection contexts are accessible from this context';
277
468
  }
278
469
 
279
470
  return 'Check the field access rules for your current context';
@@ -3,6 +3,10 @@ import { __setEvalContext, __clearEvalContext } from '../builtins/control/eval.j
3
3
  import { __setDataNamesContext, __clearDataNamesContext } from '../builtins/schema/datanames.js';
4
4
  import { validateExpression, createSecureContext, withTimeout } from '../security/validation.js';
5
5
  import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
6
+ import {
7
+ isMultilineCalculationExpression,
8
+ normalizeInlineCalculationExpression,
9
+ } from '../utilities/calculation-expression-utils.js';
6
10
 
7
11
  export function runExpression(
8
12
  expr,
@@ -34,8 +38,7 @@ export function runExpression(
34
38
 
35
39
  try {
36
40
  // Handle both expressions and multi-line code (Windows-safe)
37
- const isMultiLine =
38
- expr.includes('\r\n') || expr.includes('\n') || expr.includes('function');
41
+ const isMultiLine = isMultilineCalculationExpression(expr);
39
42
 
40
43
  if (isMultiLine) {
41
44
  // Execute as code block (recompile each time for now)
@@ -46,7 +49,8 @@ export function runExpression(
46
49
  return consumed.called ? consumed.value : result;
47
50
  } else {
48
51
  // Execute as expression (existing behavior)
49
- const fn = new Function(...keys, `return (${expr});`);
52
+ const inlineExpression = normalizeInlineCalculationExpression(expr);
53
+ const fn = new Function(...keys, `return (${inlineExpression});`);
50
54
  const result = fn(...values);
51
55
  const consumed = __consumeResult();
52
56
  return consumed.called ? consumed.value : result;
@@ -6,6 +6,10 @@ import {
6
6
  import { ContextResolver } from '../engine/context-resolver.js';
7
7
  import { DEFAULT_SECURITY_CONFIG } from '../security/config.js';
8
8
  import { validateExpression } from '../security/validation.js';
9
+ import {
10
+ isMultilineCalculationExpression,
11
+ normalizeInlineCalculationExpression,
12
+ } from './calculation-expression-utils.js';
9
13
  import { flattenFields } from './field-helpers.js';
10
14
 
11
15
  const COMMON_GLOBAL_FUNCTIONS = new Set([
@@ -157,6 +161,40 @@ function cloneCatalogDefinition(definition) {
157
161
  };
158
162
  }
159
163
 
164
+ function toCalculationReferenceAccess(accessInfo) {
165
+ return {
166
+ level: accessInfo.level,
167
+ code: accessInfo.code,
168
+ message: accessInfo.message,
169
+ suggestion: accessInfo.suggestion,
170
+ };
171
+ }
172
+
173
+ function toCalculationReferenceDefinition(field, resolver, executionContext) {
174
+ const accessInfo = resolver.resolveFieldAccessInfo(executionContext, field.data_name);
175
+
176
+ if (accessInfo.level === 'not_found') {
177
+ return null;
178
+ }
179
+
180
+ return {
181
+ key: typeof field.key === 'string' ? field.key : field.data_name,
182
+ reference: `$${field.data_name}`,
183
+ dataName: field.data_name,
184
+ label: typeof field.label === 'string' ? field.label : field.data_name,
185
+ type: typeof field.type === 'string' ? field.type : 'UnknownField',
186
+ description: typeof field.description === 'string' ? field.description : null,
187
+ displayStyle:
188
+ field.display && typeof field.display === 'object' && typeof field.display.style === 'string'
189
+ ? field.display.style
190
+ : typeof field.display === 'string'
191
+ ? field.display
192
+ : null,
193
+ parentPath: resolver.getFieldInfo(field.data_name)?.parentPath || [],
194
+ access: toCalculationReferenceAccess(accessInfo),
195
+ };
196
+ }
197
+
160
198
  function createIssue({ code, severity, message, symbol = null, index = null, length = null }) {
161
199
  return {
162
200
  code,
@@ -178,15 +216,15 @@ function addIssue(issues, issueKeys, issue) {
178
216
  }
179
217
 
180
218
  function validateSyntax(expression) {
181
- const isMultiLine =
182
- expression.includes('\r\n') || expression.includes('\n') || expression.includes('function');
219
+ const isMultiLine = isMultilineCalculationExpression(expression);
183
220
 
184
221
  if (isMultiLine) {
185
222
  new Function(expression);
186
223
  return;
187
224
  }
188
225
 
189
- new Function(`return (${expression});`);
226
+ const inlineExpression = normalizeInlineCalculationExpression(expression);
227
+ new Function(`return (${inlineExpression});`);
190
228
  }
191
229
 
192
230
  function createSchemaFilteredSecurityConfig(securityConfig) {
@@ -220,6 +258,12 @@ function createSchemaFilteredSecurityConfig(securityConfig) {
220
258
  * @property {string | null} description
221
259
  * @property {string | null} displayStyle
222
260
  * @property {string[]} parentPath
261
+ * @property {{
262
+ * level: 'accessible' | 'restricted',
263
+ * code: 'main_form' | 'same_repeatable_section' | 'ancestor_repeatable_context' | 'different_repeatable_section',
264
+ * message: string | null,
265
+ * suggestion: string | null,
266
+ * }} access
223
267
  *
224
268
  * @typedef {Object} CalculationExpressionIssue
225
269
  * @property {string} code
@@ -246,9 +290,14 @@ export function getCalculationBuiltinCatalog() {
246
290
  * @param {Object} options
247
291
  * @param {Object} options.schema
248
292
  * @param {string} options.fieldDataName
293
+ * @param {boolean} [options.includeRestricted=false]
249
294
  * @returns {CalculationReferenceDefinition[]}
250
295
  */
251
- export function getCalculationReferenceCatalog({ schema, fieldDataName }) {
296
+ export function getCalculationReferenceCatalog({
297
+ schema,
298
+ fieldDataName,
299
+ includeRestricted = false,
300
+ }) {
252
301
  const form = normalizeFormSchema(schema);
253
302
  const resolver = new ContextResolver(form);
254
303
  const executionContext = buildExecutionContext(form, fieldDataName, resolver);
@@ -262,26 +311,9 @@ export function getCalculationReferenceCatalog({ schema, fieldDataName }) {
262
311
  field.type !== 'RepeatableSection' &&
263
312
  field.type !== 'BuildingPlanSection'
264
313
  )
265
- .filter(
266
- (field) => resolver.resolveFieldAccess(executionContext, field.data_name) === 'accessible'
267
- )
268
- .map((field) => ({
269
- key: typeof field.key === 'string' ? field.key : field.data_name,
270
- reference: `$${field.data_name}`,
271
- dataName: field.data_name,
272
- label: typeof field.label === 'string' ? field.label : field.data_name,
273
- type: typeof field.type === 'string' ? field.type : 'UnknownField',
274
- description: typeof field.description === 'string' ? field.description : null,
275
- displayStyle:
276
- field.display &&
277
- typeof field.display === 'object' &&
278
- typeof field.display.style === 'string'
279
- ? field.display.style
280
- : typeof field.display === 'string'
281
- ? field.display
282
- : null,
283
- parentPath: resolver.getFieldInfo(field.data_name)?.parentPath || [],
284
- }));
314
+ .map((field) => toCalculationReferenceDefinition(field, resolver, executionContext))
315
+ .filter((reference) => reference !== null)
316
+ .filter((reference) => includeRestricted || reference.access.level === 'accessible');
285
317
  }
286
318
 
287
319
  /**
@@ -311,6 +343,7 @@ export function analyzeCalculationExpression({
311
343
  const usedBuiltinNames = new Set();
312
344
  const referencedFields = [];
313
345
  const referencedFieldNames = new Set();
346
+ const setResultCalls = [];
314
347
 
315
348
  const normalizedExpression = typeof expression === 'string' ? expression : '';
316
349
  const form = normalizeFormSchema(schema);
@@ -407,6 +440,10 @@ export function analyzeCalculationExpression({
407
440
  usedBuiltins.push(call.name);
408
441
  }
409
442
 
443
+ if (call.name === 'SETRESULT') {
444
+ setResultCalls.push(call);
445
+ }
446
+
410
447
  if (builtinMetadata.status === 'unavailable') {
411
448
  addIssue(
412
449
  issues,
@@ -452,31 +489,50 @@ export function analyzeCalculationExpression({
452
489
  );
453
490
  }
454
491
 
492
+ if (setResultCalls.length > 1) {
493
+ const duplicateSetResultCall = setResultCalls[1];
494
+ addIssue(
495
+ issues,
496
+ issueKeys,
497
+ createIssue({
498
+ code: 'multiple_setresult_calls',
499
+ severity: 'error',
500
+ message: 'SETRESULT() can only be used once in a CalculatedField expression.',
501
+ symbol: duplicateSetResultCall.name,
502
+ index: duplicateSetResultCall.index,
503
+ length: duplicateSetResultCall.length,
504
+ })
505
+ );
506
+ }
507
+
455
508
  for (const reference of extractFieldReferenceMatches(normalizedExpression)) {
456
509
  if (!referencedFieldNames.has(reference.fieldName)) {
457
510
  referencedFieldNames.add(reference.fieldName);
458
511
  referencedFields.push(reference.fieldName);
459
512
  }
460
513
 
461
- const accessLevel = resolver.resolveFieldAccess(executionContext, reference.fieldName);
462
- if (accessLevel === 'restricted') {
514
+ const accessInfo = resolver.resolveFieldAccessInfo(executionContext, reference.fieldName);
515
+ if (accessInfo.level === 'restricted') {
463
516
  addIssue(
464
517
  issues,
465
518
  issueKeys,
466
519
  createIssue({
467
520
  code: 'restricted_field_reference',
468
521
  severity: 'error',
469
- message: resolver.generateAccessSuggestion(
470
- executionContext,
471
- reference.fieldName,
472
- resolver.getFieldInfo(reference.fieldName)
473
- ),
522
+ message:
523
+ accessInfo.suggestion ||
524
+ accessInfo.message ||
525
+ resolver.generateAccessSuggestion(
526
+ executionContext,
527
+ reference.fieldName,
528
+ resolver.getFieldInfo(reference.fieldName)
529
+ ),
474
530
  symbol: reference.reference,
475
531
  index: reference.index,
476
532
  length: reference.length,
477
533
  })
478
534
  );
479
- } else if (accessLevel === 'not_found') {
535
+ } else if (accessInfo.level === 'not_found') {
480
536
  addIssue(
481
537
  issues,
482
538
  issueKeys,
@@ -0,0 +1,15 @@
1
+ export function isMultilineCalculationExpression(expression) {
2
+ const normalizedExpression = typeof expression === 'string' ? expression : '';
3
+
4
+ return (
5
+ normalizedExpression.includes('\r\n') ||
6
+ normalizedExpression.includes('\n') ||
7
+ normalizedExpression.includes('function')
8
+ );
9
+ }
10
+
11
+ export function normalizeInlineCalculationExpression(expression) {
12
+ const normalizedExpression = typeof expression === 'string' ? expression : '';
13
+
14
+ return normalizedExpression.replace(/;+\s*$/u, '');
15
+ }