From e7efd04b423c643e0dfa6ac194e6c91c799b3a33 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 18 Jul 2025 18:07:28 +0000
Subject: [PATCH 1/4] Initial plan
From 445946e717c78f1bc6ee31201132eaa08e438830 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 18 Jul 2025 18:20:08 +0000
Subject: [PATCH 2/4] Implement better error messaging for incorrect XML tag
casing
Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
---
src/DiagnosticMessages.ts | 5 ++++
src/parser/SGParser.spec.ts | 52 +++++++++++++++++++++++++++++++++++++
src/parser/SGParser.ts | 22 ++++++++++++----
3 files changed, 74 insertions(+), 5 deletions(-)
diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts
index 7f9f3f975..5e3ff19bc 100644
--- a/src/DiagnosticMessages.ts
+++ b/src/DiagnosticMessages.ts
@@ -354,6 +354,11 @@ export let DiagnosticMessages = {
code: 1065,
severity: DiagnosticSeverity.Error
}),
+ xmlTagCaseMismatch: (tagName: string, expectedTagName: string) => ({
+ message: `Tag '${tagName}' must be all lower case. Use '${expectedTagName}' instead.`,
+ code: 1143,
+ severity: DiagnosticSeverity.Error
+ }),
expectedStatementOrFunctionCallButReceivedExpression: () => ({
message: `Expected statement or function call but instead found expression`,
code: 1066,
diff --git a/src/parser/SGParser.spec.ts b/src/parser/SGParser.spec.ts
index 22f7f451e..d4963ce0a 100644
--- a/src/parser/SGParser.spec.ts
+++ b/src/parser/SGParser.spec.ts
@@ -113,6 +113,58 @@ describe('SGParser', () => {
});
});
+ it('Adds error when incorrect casing is used for children tag', () => {
+ const parser = new SGParser();
+ parser.parse(
+ 'pkg:/components/ParentScene.xml', trim`
+
+
+
+
+
+
+ `);
+ expect(parser.diagnostics).to.be.lengthOf(1);
+ expect(parser.diagnostics[0]).to.deep.include({
+ ...DiagnosticMessages.xmlTagCaseMismatch('Children', 'children'),
+ range: Range.create(2, 5, 2, 13)
+ });
+ });
+
+ it('Adds error when incorrect casing is used for interface tag', () => {
+ const parser = new SGParser();
+ parser.parse(
+ 'pkg:/components/ParentScene.xml', trim`
+
+
+
+
+
+
+ `);
+ expect(parser.diagnostics).to.be.lengthOf(1);
+ expect(parser.diagnostics[0]).to.deep.include({
+ ...DiagnosticMessages.xmlTagCaseMismatch('Interface', 'interface'),
+ range: Range.create(2, 5, 2, 14)
+ });
+ });
+
+ it('Adds error when incorrect casing is used for script tag', () => {
+ const parser = new SGParser();
+ parser.parse(
+ 'pkg:/components/ParentScene.xml', trim`
+
+
+
+
+ `);
+ expect(parser.diagnostics).to.be.lengthOf(1);
+ expect(parser.diagnostics[0]).to.deep.include({
+ ...DiagnosticMessages.xmlTagCaseMismatch('Script', 'script'),
+ range: Range.create(2, 5, 2, 11)
+ });
+ });
+
it('Adds error when a leaf tag is found to have children', () => {
const parser = new SGParser();
parser.parse(
diff --git a/src/parser/SGParser.ts b/src/parser/SGParser.ts
index bf2b5a6ce..005d75ccc 100644
--- a/src/parser/SGParser.ts
+++ b/src/parser/SGParser.ts
@@ -257,11 +257,23 @@ function mapElements(content: ContentCstNode, allow: string[], diagnostics: Diag
if (allow.includes(name.image)) {
tags.push(mapElement(entry, diagnostics));
} else {
- //unexpected tag
- diagnostics.push({
- ...DiagnosticMessages.xmlUnexpectedTag(name.image),
- range: rangeFromTokens(name)
- });
+ // Check if this is a case mismatch for a known tag
+ const lowerCaseTag = name.image.toLowerCase();
+ const matchingAllowedTag = allow.find(allowedTag => allowedTag.toLowerCase() === lowerCaseTag);
+
+ if (matchingAllowedTag && matchingAllowedTag !== name.image) {
+ // Case mismatch for a known tag
+ diagnostics.push({
+ ...DiagnosticMessages.xmlTagCaseMismatch(name.image, matchingAllowedTag),
+ range: rangeFromTokens(name)
+ });
+ } else {
+ // Truly unexpected tag
+ diagnostics.push({
+ ...DiagnosticMessages.xmlUnexpectedTag(name.image),
+ range: rangeFromTokens(name)
+ });
+ }
}
} else {
//bad xml syntax...
From 016d2db325b66e185235366e20f78c90b735bd52 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 18 Jul 2025 20:04:21 +0000
Subject: [PATCH 3/4] Move XML tag casing validation from SGParser to
XmlFileValidator
- Move case validation logic from SGParser.ts to XmlFileValidator.ts for better plugin support
- Update SGParser to create AST objects for case-mismatched tags (case-insensitive parsing)
- Add comprehensive tests for XML tag casing validation in XmlFileValidator
- Update existing SGParser tests to reflect new behavior
- Add plugin test demonstrating validation catches AST modifications
Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
---
src/bscPlugin/validation/XmlFileValidator.ts | 67 +++++++++-
src/files/XmlFile.spec.ts | 124 +++++++++++++++++++
src/parser/SGParser.spec.ts | 24 +---
src/parser/SGParser.ts | 17 +--
4 files changed, 205 insertions(+), 27 deletions(-)
diff --git a/src/bscPlugin/validation/XmlFileValidator.ts b/src/bscPlugin/validation/XmlFileValidator.ts
index 44912dced..a9f6d0fdd 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, SGComponent, SGInterface } from '../../parser/SGTypes';
import util from '../../util';
export class XmlFileValidator {
@@ -14,6 +14,7 @@ export class XmlFileValidator {
util.validateTooDeepFile(this.event.file);
if (this.event.file.parser.ast.root) {
this.validateComponent(this.event.file.parser.ast);
+ this.validateTagCasing(this.event.file.parser.ast);
} else {
//skip empty XML
}
@@ -62,4 +63,68 @@ export class XmlFileValidator {
}
}
+ private validateTagCasing(ast: SGAst) {
+ const { component } = ast;
+ if (!component) {
+ return;
+ }
+
+ this.validateComponentTagCasing(component);
+ }
+
+ private validateComponentTagCasing(component: SGComponent) {
+ // Validate component-level tags
+ const componentLevelTags = ['children', 'interface', 'script', 'customization'];
+
+ // Check interface tag
+ if (component.api) {
+ this.validateTagNameCasing(component.api.tag, componentLevelTags);
+ this.validateInterfaceTagCasing(component.api);
+ }
+
+ // Check script tags
+ for (const script of component.scripts) {
+ this.validateTagNameCasing(script.tag, componentLevelTags);
+ }
+
+ // Check children tag
+ if (component.children) {
+ this.validateTagNameCasing(component.children.tag, componentLevelTags);
+ }
+
+ // Check customization tags
+ for (const customization of component.customizations) {
+ this.validateTagNameCasing(customization.tag, componentLevelTags);
+ }
+ }
+
+ private validateInterfaceTagCasing(interfaceTag: SGInterface) {
+ const interfaceLevelTags = ['field', 'function'];
+
+ // Check field tags
+ for (const field of interfaceTag.fields) {
+ this.validateTagNameCasing(field.tag, interfaceLevelTags);
+ }
+
+ // Check function tags
+ for (const func of interfaceTag.functions) {
+ this.validateTagNameCasing(func.tag, interfaceLevelTags);
+ }
+ }
+
+ private validateTagNameCasing(tag: { text: string; range?: any }, allowedTags: string[]) {
+ const tagName = tag.text;
+ const lowerCaseTag = tagName.toLowerCase();
+ const matchingAllowedTag = allowedTags.find(allowedTag => allowedTag.toLowerCase() === lowerCaseTag);
+
+ if (matchingAllowedTag && matchingAllowedTag !== tagName) {
+ // Case mismatch for a known tag
+ this.event.file.diagnostics.push({
+ ...DiagnosticMessages.xmlTagCaseMismatch(tagName, matchingAllowedTag),
+ range: tag.range,
+ file: this.event.file
+ });
+ }
+ }
+
}
diff --git a/src/files/XmlFile.spec.ts b/src/files/XmlFile.spec.ts
index e8726a7e8..1d1f91bc4 100644
--- a/src/files/XmlFile.spec.ts
+++ b/src/files/XmlFile.spec.ts
@@ -1324,4 +1324,128 @@ describe('XmlFile', () => {
expect(program.getComponent('comp1')!.file.pkgPath).to.equal(comp2.pkgPath);
});
});
+
+ describe('XML tag casing validation', () => {
+ it('Adds error when incorrect casing is used for children tag', () => {
+ file = program.setFile('components/ChildScene.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectDiagnostics(program, [
+ {
+ ...DiagnosticMessages.xmlTagCaseMismatch('Children', 'children'),
+ range: Range.create(2, 5, 2, 13)
+ }
+ ]);
+ });
+
+ it('Adds error when incorrect casing is used for interface tag', () => {
+ file = program.setFile('components/ChildScene.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectDiagnostics(program, [
+ {
+ ...DiagnosticMessages.xmlTagCaseMismatch('Interface', 'interface'),
+ range: Range.create(2, 5, 2, 14)
+ }
+ ]);
+ });
+
+ it('Adds error when incorrect casing is used for script tag', () => {
+ file = program.setFile('components/ChildScene.xml', trim`
+
+
+
+
+ `);
+ program.validate();
+ expectDiagnostics(program, [
+ {
+ ...DiagnosticMessages.xmlTagCaseMismatch('Script', 'script'),
+ range: Range.create(2, 5, 2, 11)
+ }
+ ]);
+ });
+
+ it('Adds error when incorrect casing is used for field tag in interface', () => {
+ file = program.setFile('components/ChildScene.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectDiagnostics(program, [
+ {
+ ...DiagnosticMessages.xmlTagCaseMismatch('Field', 'field'),
+ range: Range.create(3, 9, 3, 14)
+ }
+ ]);
+ });
+
+ it('Does not add error for correctly cased tags', () => {
+ file = program.setFile('components/ChildScene.xml', trim`
+
+
+
+
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectZeroDiagnostics(program);
+ });
+
+ it('Catches casing issues when plugins modify AST after parsing', () => {
+ // This is the test requested in the comment - plugins modify tag casing and validation catches it
+ program.plugins.add({
+ name: 'test-plugin-modify-casing',
+ afterFileParse: (file) => {
+ if (isXmlFile(file) && file.parser.ast.component?.children) {
+ // Plugin modifies the children tag to incorrect casing
+ file.parser.ast.component.children.tag.text = 'Children';
+ }
+ }
+ });
+
+ file = program.setFile('components/ChildScene.xml', trim`
+
+
+
+
+
+
+ `);
+ program.validate();
+ expectDiagnostics(program, [
+ {
+ ...DiagnosticMessages.xmlTagCaseMismatch('Children', 'children'),
+ range: Range.create(2, 5, 2, 13)
+ }
+ ]);
+ });
+ });
});
diff --git a/src/parser/SGParser.spec.ts b/src/parser/SGParser.spec.ts
index d4963ce0a..2ef9060c8 100644
--- a/src/parser/SGParser.spec.ts
+++ b/src/parser/SGParser.spec.ts
@@ -113,7 +113,7 @@ describe('SGParser', () => {
});
});
- it('Adds error when incorrect casing is used for children tag', () => {
+ it('Does not add case mismatch error during parsing (now handled in validation)', () => {
const parser = new SGParser();
parser.parse(
'pkg:/components/ParentScene.xml', trim`
@@ -124,14 +124,10 @@ describe('SGParser', () => {
`);
- expect(parser.diagnostics).to.be.lengthOf(1);
- expect(parser.diagnostics[0]).to.deep.include({
- ...DiagnosticMessages.xmlTagCaseMismatch('Children', 'children'),
- range: Range.create(2, 5, 2, 13)
- });
+ expect(parser.diagnostics).to.be.lengthOf(0);
});
- it('Adds error when incorrect casing is used for interface tag', () => {
+ it('Does not add case mismatch error during parsing for interface tag (now handled in validation)', () => {
const parser = new SGParser();
parser.parse(
'pkg:/components/ParentScene.xml', trim`
@@ -142,14 +138,10 @@ describe('SGParser', () => {
`);
- expect(parser.diagnostics).to.be.lengthOf(1);
- expect(parser.diagnostics[0]).to.deep.include({
- ...DiagnosticMessages.xmlTagCaseMismatch('Interface', 'interface'),
- range: Range.create(2, 5, 2, 14)
- });
+ expect(parser.diagnostics).to.be.lengthOf(0);
});
- it('Adds error when incorrect casing is used for script tag', () => {
+ it('Does not add case mismatch error during parsing for script tag (now handled in validation)', () => {
const parser = new SGParser();
parser.parse(
'pkg:/components/ParentScene.xml', trim`
@@ -158,11 +150,7 @@ describe('SGParser', () => {
`);
- expect(parser.diagnostics).to.be.lengthOf(1);
- expect(parser.diagnostics[0]).to.deep.include({
- ...DiagnosticMessages.xmlTagCaseMismatch('Script', 'script'),
- range: Range.create(2, 5, 2, 11)
- });
+ expect(parser.diagnostics).to.be.lengthOf(0);
});
it('Adds error when a leaf tag is found to have children', () => {
diff --git a/src/parser/SGParser.ts b/src/parser/SGParser.ts
index 005d75ccc..3b9051797 100644
--- a/src/parser/SGParser.ts
+++ b/src/parser/SGParser.ts
@@ -187,7 +187,10 @@ function mapElement({ children }: ElementCstNode, diagnostics: Diagnostic[]): SG
const name = mapToken(nameToken);
const attributes = mapAttributes(children.attribute);
const content = children.content?.[0];
- switch (name.text) {
+
+ // Use case-insensitive matching to handle incorrect casing
+ const lowerCaseName = name.text.toLowerCase();
+ switch (lowerCaseName) {
case 'component':
const componentContent = mapElements(content, ['interface', 'script', 'children', 'customization'], diagnostics);
return new SGComponent(name, attributes, componentContent, range);
@@ -254,19 +257,17 @@ function mapElements(content: ContentCstNode, allow: string[], diagnostics: Diag
for (const entry of element) {
const name = entry.children.Name?.[0];
if (name?.image) {
+ // First check if it's exactly allowed
if (allow.includes(name.image)) {
tags.push(mapElement(entry, diagnostics));
} else {
// Check if this is a case mismatch for a known tag
const lowerCaseTag = name.image.toLowerCase();
const matchingAllowedTag = allow.find(allowedTag => allowedTag.toLowerCase() === lowerCaseTag);
-
- if (matchingAllowedTag && matchingAllowedTag !== name.image) {
- // Case mismatch for a known tag
- diagnostics.push({
- ...DiagnosticMessages.xmlTagCaseMismatch(name.image, matchingAllowedTag),
- range: rangeFromTokens(name)
- });
+
+ if (matchingAllowedTag) {
+ // Case mismatch for a known tag - create the AST object but validation will catch the casing issue
+ tags.push(mapElement(entry, diagnostics));
} else {
// Truly unexpected tag
diagnostics.push({
From e55039874c386accee782958542490497e95cd65 Mon Sep 17 00:00:00 2001
From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com>
Date: Fri, 18 Jul 2025 20:10:24 +0000
Subject: [PATCH 4/4] Move xmlTagCaseMismatch diagnostic to correct order in
DiagnosticMessages.ts
Co-authored-by: TwitchBronBron <2544493+TwitchBronBron@users.noreply.github.com>
---
src/DiagnosticMessages.ts | 10 +++++-----
src/bscPlugin/validation/XmlFileValidator.ts | 4 ++--
src/parser/SGParser.ts | 4 ++--
3 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/src/DiagnosticMessages.ts b/src/DiagnosticMessages.ts
index 5e3ff19bc..918519c85 100644
--- a/src/DiagnosticMessages.ts
+++ b/src/DiagnosticMessages.ts
@@ -354,11 +354,6 @@ export let DiagnosticMessages = {
code: 1065,
severity: DiagnosticSeverity.Error
}),
- xmlTagCaseMismatch: (tagName: string, expectedTagName: string) => ({
- message: `Tag '${tagName}' must be all lower case. Use '${expectedTagName}' instead.`,
- code: 1143,
- severity: DiagnosticSeverity.Error
- }),
expectedStatementOrFunctionCallButReceivedExpression: () => ({
message: `Expected statement or function call but instead found expression`,
code: 1066,
@@ -752,6 +747,11 @@ export let DiagnosticMessages = {
message: `Non-void ${functionType} must return a value`,
code: 1142,
severity: DiagnosticSeverity.Error
+ }),
+ xmlTagCaseMismatch: (tagName: string, expectedTagName: string) => ({
+ message: `Tag '${tagName}' must be all lower case. Use '${expectedTagName}' instead.`,
+ code: 1143,
+ severity: DiagnosticSeverity.Error
})
};
diff --git a/src/bscPlugin/validation/XmlFileValidator.ts b/src/bscPlugin/validation/XmlFileValidator.ts
index a9f6d0fdd..5cf5f7247 100644
--- a/src/bscPlugin/validation/XmlFileValidator.ts
+++ b/src/bscPlugin/validation/XmlFileValidator.ts
@@ -75,7 +75,7 @@ export class XmlFileValidator {
private validateComponentTagCasing(component: SGComponent) {
// Validate component-level tags
const componentLevelTags = ['children', 'interface', 'script', 'customization'];
-
+
// Check interface tag
if (component.api) {
this.validateTagNameCasing(component.api.tag, componentLevelTags);
@@ -100,7 +100,7 @@ export class XmlFileValidator {
private validateInterfaceTagCasing(interfaceTag: SGInterface) {
const interfaceLevelTags = ['field', 'function'];
-
+
// Check field tags
for (const field of interfaceTag.fields) {
this.validateTagNameCasing(field.tag, interfaceLevelTags);
diff --git a/src/parser/SGParser.ts b/src/parser/SGParser.ts
index 3b9051797..f2b170eae 100644
--- a/src/parser/SGParser.ts
+++ b/src/parser/SGParser.ts
@@ -187,7 +187,7 @@ function mapElement({ children }: ElementCstNode, diagnostics: Diagnostic[]): SG
const name = mapToken(nameToken);
const attributes = mapAttributes(children.attribute);
const content = children.content?.[0];
-
+
// Use case-insensitive matching to handle incorrect casing
const lowerCaseName = name.text.toLowerCase();
switch (lowerCaseName) {
@@ -264,7 +264,7 @@ function mapElements(content: ContentCstNode, allow: string[], diagnostics: Diag
// Check if this is a case mismatch for a known tag
const lowerCaseTag = name.image.toLowerCase();
const matchingAllowedTag = allow.find(allowedTag => allowedTag.toLowerCase() === lowerCaseTag);
-
+
if (matchingAllowedTag) {
// Case mismatch for a known tag - create the AST object but validation will catch the casing issue
tags.push(mapElement(entry, diagnostics));