diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts index cd81e60bc..a9012a97b 100644 --- a/src/DiagnosticMessages.ts +++ b/src/DiagnosticMessages.ts @@ -833,6 +833,11 @@ export let DiagnosticMessages = { message: `rsg_version=${rsgVersion} was removed in firmware ${removedAt}; use rsg_version=${replacement}`, code: 1154, severity: DiagnosticSeverity.Error + }), + xmlTagMismatch: (openingTag: string, closingTag: string) => ({ + message: `Mismatched closing tag: expected '' but found ''`, + code: 1155, + severity: DiagnosticSeverity.Error }) }; diff --git a/src/bscPlugin/validation/XmlFileValidator.ts b/src/bscPlugin/validation/XmlFileValidator.ts index 44912dced..d23a4eb31 100644 --- a/src/bscPlugin/validation/XmlFileValidator.ts +++ b/src/bscPlugin/validation/XmlFileValidator.ts @@ -1,7 +1,7 @@ import { DiagnosticMessages } from '../../DiagnosticMessages'; import type { XmlFile } from '../../files/XmlFile'; import type { OnFileValidateEvent } from '../../interfaces'; -import type { SGAst } from '../../parser/SGTypes'; +import type { SGAst, SGTag } from '../../parser/SGTypes'; import util from '../../util'; export class XmlFileValidator { @@ -14,11 +14,34 @@ export class XmlFileValidator { util.validateTooDeepFile(this.event.file); if (this.event.file.parser.ast.root) { this.validateComponent(this.event.file.parser.ast); + this.validateTagClosings(this.event.file.parser.ast.root); } else { //skip empty XML } } + /** + * Walk the SG tag tree and report any tag whose closing tag name does not + * match its opening tag name (e.g. ``). This runs at + * validation time (rather than parse time) so it also catches mismatches in + * AST injected by plugins, not just AST produced by the parser. + */ + private validateTagClosings(tag: SGTag) { + const closingTagText = tag.closingTag?.text; + //only validate when a closing tag was actually present (self-closing and + //programmatically-built tags omit it, and must remain valid) + if (closingTagText !== undefined && closingTagText !== tag.tag.text) { + this.event.file.diagnostics.push({ + ...DiagnosticMessages.xmlTagMismatch(tag.tag.text, closingTagText), + range: tag.closingTag.range, + file: this.event.file + }); + } + for (const child of tag.getChildren()) { + this.validateTagClosings(child); + } + } + private validateComponent(ast: SGAst) { const { root, component } = ast; if (!component) { diff --git a/src/files/XmlFile.spec.ts b/src/files/XmlFile.spec.ts index 44d1700ee..134185acb 100644 --- a/src/files/XmlFile.spec.ts +++ b/src/files/XmlFile.spec.ts @@ -669,6 +669,130 @@ describe('XmlFile', () => { `, 'none', 'components/Comp.xml'); }); + it('transpiles mismatched tags correctly using opening tag', () => { + const file = program.setFile('components/Comp.xml', trim` + + + + + + + + `); + file.needsTranspiled = true; + program.validate(); + + // Should have a diagnostic for the mismatch + expect(file.diagnostics).to.have.lengthOf(1); + expect(file.diagnostics[0]).to.deep.include({ + ...DiagnosticMessages.xmlTagMismatch('Group', 'LayoutGroup') + }); + + // But transpile should still work correctly (self-closing since no children) + const transpiled = file.transpile(); + expect(trimMap(transpiled.code)).to.equal(trim` + + +