diff --git a/src/TemplateMarkInterpreter.ts b/src/TemplateMarkInterpreter.ts index 4654be5..49a89b5 100644 --- a/src/TemplateMarkInterpreter.ts +++ b/src/TemplateMarkInterpreter.ts @@ -575,13 +575,61 @@ export class TemplateMarkInterpreter { const serializer = new Serializer(factory, modelManager); try { serializer.fromJSON(templateMark); - return templateMark; } catch (err) { throw new Error(`Generated invalid agreement: ${err}: ${JSON.stringify(templateMark, null, 2)}`); } + + const errors: Array<{ propertyName: string, message: string }> = []; + const templateClass = this.templateClass; + const guardBlockPaths: Map = new Map(); + + traverse(templateMark).forEach(function (node: any) { + if (!node || typeof node !== 'object' || !node.$class) return; + + const currentPath = this.path.join('/'); + if (OPTIONAL_DEFINITION_RE.test(node.$class) || + CONDITIONAL_DEFINITION_RE.test(node.$class) || + WITH_DEFINITION_RE.test(node.$class)) { + guardBlockPaths.set(node.name, currentPath); + } + if (VARIABLE_DEFINITION_RE.test(node.$class) || + ENUM_VARIABLE_DEFINITION_RE.test(node.$class) || + FORMATTED_VARIABLE_DEFINITION_RE.test(node.$class)) { + const propName = node.name; + if (propName && propName !== 'this') { + try { + const property = templateClass.getProperty(propName); + if (property && property.isOptional()) { + const guardPath = guardBlockPaths.get(propName); + const isGuarded = guardPath !== undefined && + (currentPath === guardPath || currentPath.startsWith(guardPath + '/')); + if (!isGuarded) { + errors.push({ + propertyName: propName, + message: `Optional property '${propName}' is used without a guard. Wrap it in {{#optional ${propName}}}...{{/optional}} or {{#if ${propName}}}...{{/if}}.` + }); + } + } + } catch { + // Property not found at root level, might be nested - skip + } + } + } + }); + + if (errors.length > 0) { + const errorMessage = `Optional properties used without guards: ${errors.map(e => e.propertyName).join(', ')}`; + const error = new Error(errorMessage); + (error as any).errors = errors; + throw error; + } + + return templateMark; } + + /** * Compiles the code nodes containing TS to code nodes containing JS. * @param {*} templateMark the TemplateMark JSON object diff --git a/test/__snapshots__/TemplateMarkInterpreter.test.ts.snap b/test/__snapshots__/TemplateMarkInterpreter.test.ts.snap index c439263..676df9b 100644 --- a/test/__snapshots__/TemplateMarkInterpreter.test.ts.snap +++ b/test/__snapshots__/TemplateMarkInterpreter.test.ts.snap @@ -82,26 +82,9 @@ exports[`templatemark interpreter should fail to generate formula-no-method 1`] ] `; -exports[`templatemark interpreter should fail to generate formula-optional-noguard 1`] = ` -[ - { - "code": " return message.length ", - "errors": [ - { - "category": 1, - "character": 10, - "code": 18048, - "id": "err-18048-475-7", - "length": 7, - "line": 82, - "renderedMessage": "'message' is possibly 'undefined'.", - "start": 1793, - }, - ], - "nodeId": "formula_a3e0e90a2ad3601e1b208a0581e1ce6dfc5aab302d20cb2a0488d78e236096f7", - }, -] -`; +exports[`templatemark interpreter should fail to generate formula-optional-noguard 1`] = `[Error: Optional properties used without guards: message]`; + +exports[`templatemark interpreter should fail to generate optional-noguard 1`] = `[Error: Optional properties used without guards: nickname]`; exports[`templatemark interpreter should fail to generate template-missing-field 1`] = `[TemplateException: Unknown property: missing File text/grammar.tem.md line -1 column -1]`; diff --git a/test/templates/bad/optional-noguard/data.json b/test/templates/bad/optional-noguard/data.json new file mode 100644 index 0000000..f54f4c4 --- /dev/null +++ b/test/templates/bad/optional-noguard/data.json @@ -0,0 +1,4 @@ +{ + "$class": "test@1.0.0.TemplateData", + "name": "World" +} diff --git a/test/templates/bad/optional-noguard/model.cto b/test/templates/bad/optional-noguard/model.cto new file mode 100644 index 0000000..a737509 --- /dev/null +++ b/test/templates/bad/optional-noguard/model.cto @@ -0,0 +1,7 @@ +namespace test@1.0.0 + +@template +concept TemplateData { + o String name + o String nickname optional +} diff --git a/test/templates/bad/optional-noguard/template.md b/test/templates/bad/optional-noguard/template.md new file mode 100644 index 0000000..1ee156f --- /dev/null +++ b/test/templates/bad/optional-noguard/template.md @@ -0,0 +1 @@ +Hello {{name}}! Your nickname is {{nickname}}. diff --git a/test/templates/good/playground/model.cto b/test/templates/good/playground/model.cto index 13acfb7..33c2059 100644 --- a/test/templates/good/playground/model.cto +++ b/test/templates/good/playground/model.cto @@ -23,7 +23,7 @@ concept Order { concept TemplateData { o String name o Address address - o Integer age optional + o Integer age o MonetaryAmount salary o String[] favoriteColors o Order order