Skip to content

Commit 9f4c240

Browse files
committed
refactor(autofix): Validate XML before and after autofix
Similar to the JS-autofix, we should validate XML for syntax errors before and after applying fixes. Add new dependency 'fast-xml-parser' which seems to have good reputation and existing usage across SAP. It has only one transitive dependency and overall reasonable footprint with ~600 KB install size. License is MIT. JIRA: CPOUI5FOUNDATION-994
1 parent fa7642d commit 9f4c240

File tree

7 files changed

+244
-37
lines changed

7 files changed

+244
-37
lines changed

npm-shrinkwrap.json

Lines changed: 31 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@
7878
"@ui5/project": "^4.0.4",
7979
"chalk": "^5.4.1",
8080
"data-with-position": "^0.5.0",
81+
"fast-xml-parser": "^5.2.5",
8182
"figures": "^6.1.0",
8283
"globals": "^16.2.0",
8384
"he": "^1.2.0",

src/autofix/autofix.ts

Lines changed: 99 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,15 @@ function getJsErrors(code: string, resourcePath: string) {
155155
return getJsErrorsForSourceFile(sourceFile, program, host);
156156
}
157157

158+
async function getXmlErrorForFile(content: string) {
159+
const {XMLValidator} = await import("fast-xml-parser");
160+
const validOrError = XMLValidator.validate(content);
161+
if (validOrError === true) {
162+
return; // No errors
163+
}
164+
return validOrError.err;
165+
}
166+
158167
export function getFactoryPosition(moduleDeclaration: ExistingModuleDeclarationInfo): {start: number; end: number} {
159168
const {moduleDeclaration: declaration} = moduleDeclaration;
160169
const factory = "factory" in declaration ? declaration.factory : declaration.callback;
@@ -190,6 +199,23 @@ export default async function ({
190199
}
191200
}
192201

202+
const res: AutofixResult = new Map();
203+
if (jsResources.length) {
204+
log.verbose(`Applying autofixes for ${jsResources.length} JS resources`);
205+
await autofixJs(jsResources, messages, context, res);
206+
}
207+
if (xmlResources.length) {
208+
log.verbose(`Applying autofixes for ${xmlResources.length} XML resources`);
209+
await autofixXml(xmlResources, messages, context, res);
210+
}
211+
212+
return res;
213+
}
214+
215+
async function autofixJs(
216+
jsResources: Resource[], messages: Map<ResourcePath, RawLintMessage[]>, context: LinterContext,
217+
res: AutofixResult
218+
): Promise<void> {
193219
const sourceFiles: SourceFiles = new Map();
194220
const jsResourcePaths = [];
195221
for (const resource of jsResources) {
@@ -210,7 +236,6 @@ export default async function ({
210236
const program = createProgram(jsResourcePaths, compilerHost);
211237

212238
const checker = program.getTypeChecker();
213-
const res: AutofixResult = new Map();
214239
for (const [resourcePath, sourceFile] of sourceFiles) {
215240
// Checking for syntax errors in the original source file.
216241
// We should not apply autofixes to files with syntax errors
@@ -246,26 +271,41 @@ export default async function ({
246271
const jsErrors = getJsErrors(newContent, resourcePath);
247272
if (jsErrors.length) {
248273
const contentWithMarkers = newContent.split("\n");
249-
const message = `Syntax error after applying autofix for '${resourcePath}':\n - ` +
250-
jsErrors
251-
.sort((a, b) => {
252-
if (a.start === undefined || b.start === undefined) {
253-
return 0;
254-
}
255-
return a.start - b.start;
256-
}).map((d) => {
257-
if (d.line !== undefined && d.character !== undefined) {
258-
// Insert line below the finding and mark the character
259-
const line = d.line + 1;
260-
contentWithMarkers.splice(line, 0, " ".repeat(d.character) + "^");
261-
}
262-
let res = d.messageText;
263-
if (d.codeSnippet) {
264-
res += ` (\`${d.codeSnippet}\`)`;
265-
}
266-
return res;
267-
}).join("\n - ");
274+
let lineOffset = 0;
275+
const message = `Syntax error after applying autofix for '${resourcePath}'. ` +
276+
`This is likely a UI5 linter internal issue. Please check the verbose log. ` +
277+
`Please report this using the bug report template: ` +
278+
`https://github.com/UI5/linter/issues/new?template=bug-report.md`;
279+
const errors = jsErrors
280+
.sort((a, b) => {
281+
if (a.start === undefined || b.start === undefined) {
282+
return 0;
283+
}
284+
return a.start - b.start;
285+
}).map((d) => {
286+
let res = d.messageText;
287+
if (d.line !== undefined && d.character !== undefined) {
288+
// Prepend line and character
289+
res = `${d.line + 1}:${d.character + 1} ${res}`;
290+
291+
// Fill contentWithMarkers array for debug logging
292+
// Insert line below the finding and mark the character
293+
const lineContent = contentWithMarkers[d.line + lineOffset];
294+
// Count number of tabs until the character
295+
const tabCount = lineContent.slice(0, d.character).split("\t").length - 1;
296+
const leadingTabs = "\t".repeat(tabCount);
297+
const markerLine = d.line + lineOffset + 1;
298+
contentWithMarkers.splice(markerLine, 0,
299+
leadingTabs + " ".repeat(d.character - tabCount) + "^");
300+
lineOffset++;
301+
}
302+
if (d.codeSnippet) {
303+
res += ` (\`${d.codeSnippet}\`)`;
304+
}
305+
return res;
306+
}).join("\n - ");
268307
log.verbose(message);
308+
log.verbose(errors);
269309
log.verbose(resourcePath + ":\n" + contentWithMarkers.join("\n"));
270310
context.addLintingMessage(resourcePath, MESSAGE.AUTOFIX_ERROR, {message});
271311
} else {
@@ -274,16 +314,51 @@ export default async function ({
274314
}
275315
}
276316
}
317+
}
277318

319+
async function autofixXml(
320+
xmlResources: Resource[], messages: Map<ResourcePath, RawLintMessage[]>, context: LinterContext,
321+
res: AutofixResult
322+
): Promise<void> {
278323
for (const resource of xmlResources) {
279324
const resourcePath = resource.getPath();
325+
const existingXmlError = await getXmlErrorForFile(await resource.getString());
326+
if (existingXmlError) {
327+
log.verbose(`Skipping autofix for '${resourcePath}'. Syntax error reported in original source file:\n` +
328+
`[${existingXmlError.line}:${existingXmlError.col}] ${existingXmlError.msg}`);
329+
continue;
330+
}
280331
const newContent = await applyFixesXml(resource, messages.get(resourcePath)!);
281-
if (newContent) {
282-
res.set(resourcePath, newContent);
332+
if (!newContent) {
333+
continue;
283334
}
284-
}
285335

286-
return res;
336+
const newXmlError = await getXmlErrorForFile(newContent);
337+
if (newXmlError) {
338+
const message = `Syntax error after applying autofix for '${resourcePath}'. ` +
339+
`This is likely a UI5 linter internal issue. Please check the verbose log. ` +
340+
`Please report this using the bug report template: ` +
341+
`https://github.com/UI5/linter/issues/new?template=bug-report.md`;
342+
const error = `Reported error (${newXmlError.line}:${newXmlError.col}): ${newXmlError.msg}`;
343+
log.verbose(message);
344+
log.verbose(error);
345+
const contentWithMarkers = newContent.split("\n");
346+
if (newXmlError.line !== undefined && newXmlError.col !== undefined) {
347+
const line = newXmlError.line - 1;
348+
// Insert line below the finding and mark the character
349+
const lineContent = contentWithMarkers[line];
350+
// Count number of tabs until the character
351+
const tabCount = lineContent.slice(0, newXmlError.col).split("\t").length - 1;
352+
const leadingTabs = "\t".repeat(tabCount);
353+
const markerLine = line + 1;
354+
contentWithMarkers.splice(markerLine, 0, leadingTabs + " ".repeat(newXmlError.col - tabCount) + "^");
355+
}
356+
log.verbose(resourcePath + ":\n" + contentWithMarkers.join("\n"));
357+
context.addLintingMessage(resourcePath, MESSAGE.AUTOFIX_ERROR, {message});
358+
continue;
359+
}
360+
res.set(resourcePath, newContent);
361+
}
287362
}
288363

289364
function applyFixesJs(
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<mvc:View xmlns:mvc="sap.ui.core.mvc"
2+
xmlns="sap.m"
3+
controllerName="com.myapp.controller.Main"
4+
>
5+
<Button id="segmented-button-inner" tap=".onButtonTap"/>
6+
<Button id="segmented-button-inner" tap=".onButtonTap />
7+
8+
</mvc:View>

test/lib/autofix/autofix.ts

Lines changed: 81 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,12 @@ import anyTest, {TestFn} from "ava";
33
import sinonGlobal from "sinon";
44
import esmock from "esmock";
55
import autofix, {AutofixResource, ChangeAction, InsertChange} from "../../../src/autofix/autofix.js";
6-
import LinterContext from "../../../src/linter/LinterContext.js";
6+
import LinterContext, {PositionInfo} from "../../../src/linter/LinterContext.js";
77
import {MESSAGE} from "../../../src/linter/messages.js";
88
import type {addDependencies} from "../../../src/autofix/amdImports.js";
99
import {createResource} from "@ui5/fs/resourceFactory";
10-
import Fix from "../../../src/linter/ui5Types/fix/Fix.js";
10+
import XmlEnabledFix from "../../../src/linter/ui5Types/fix/XmlEnabledFix.js";
11+
import {SaxEventType} from "sax-wasm";
1112

1213
const test = anyTest as TestFn<{
1314
sinon: sinonGlobal.SinonSandbox;
@@ -37,25 +38,31 @@ test.afterEach.always((t) => {
3738
t.context.sinon.restore();
3839
});
3940

40-
class TestFix extends Fix {
41+
class TestFix extends XmlEnabledFix {
42+
constructor(private position: PositionInfo) {
43+
super();
44+
}
45+
4146
visitLinterNode() {
4247
return true;
4348
}
4449

4550
getNodeSearchParameters() {
4651
return {
4752
nodeTypes: [ts.SyntaxKind.PropertyAccessExpression, ts.SyntaxKind.ElementAccessExpression],
48-
position: {
49-
line: 1,
50-
column: 1,
51-
},
53+
xmlEventTypes: [SaxEventType.Attribute],
54+
position: this.position,
5255
};
5356
}
5457

5558
visitAutofixNode() {
5659
return true;
5760
}
5861

62+
visitAutofixXmlNode() {
63+
return true;
64+
}
65+
5966
getAffectedSourceCodeRange() {
6067
return [];
6168
}
@@ -89,7 +96,7 @@ class TestFix extends Fix {
8996
}
9097
}
9198

92-
test("autofix: Parsing error after applying fixes", async (t) => {
99+
test("autofix (JS): Parsing error after applying fixes", async (t) => {
93100
const {autofix, linterContext} = t.context;
94101

95102
const resources = new Map<string, AutofixResource>();
@@ -108,7 +115,10 @@ test("autofix: Parsing error after applying fixes", async (t) => {
108115
column: 1,
109116
},
110117
args: {},
111-
fix: new TestFix(),
118+
fix: new TestFix({
119+
line: 1,
120+
column: 1,
121+
}),
112122
},
113123
],
114124
});
@@ -133,10 +143,68 @@ test("autofix: Parsing error after applying fixes", async (t) => {
133143
{
134144
column: undefined,
135145
line: undefined,
136-
message: `Syntax error after applying autofix for '/resources/file.js':
137-
- Unexpected keyword or identifier. (\`s\`)
138-
- Declaration or statement expected. (\`)\`)
139-
- ')' expected.`,
146+
message: `Syntax error after applying autofix for '/resources/file.js'. ` +
147+
`This is likely a UI5 linter internal issue. Please check the verbose log. ` +
148+
`Please report this using the bug report template: ` +
149+
`https://github.com/UI5/linter/issues/new?template=bug-report.md`,
150+
ruleId: "autofix-error",
151+
severity: 1,
152+
},
153+
],
154+
},
155+
]);
156+
});
157+
158+
test("autofix (XML): Parsing error after applying fixes", async (t) => {
159+
const {autofix, linterContext} = t.context;
160+
161+
const resources = new Map<string, AutofixResource>();
162+
resources.set("/resources/file.view.xml", {
163+
resource: createResource({
164+
path: "/resources/file.view.xml",
165+
string: "<mvc:View xmlns:mvc='sap.ui.core.mvc' xmlns='sap.m'><Button text='Test'/></mvc:View>",
166+
}),
167+
messages: [
168+
// Note: Message details don't need to be correct in this test case
169+
// as we stub the addDependencies function
170+
{
171+
id: MESSAGE.DEPRECATED_PROPERTY,
172+
position: {
173+
line: 1,
174+
column: 1,
175+
},
176+
args: {},
177+
fix: new TestFix({
178+
line: 1,
179+
column: 11,
180+
}),
181+
},
182+
],
183+
});
184+
const result = await autofix({
185+
rootDir: "/",
186+
context: linterContext,
187+
resources,
188+
});
189+
190+
t.truthy(result);
191+
t.deepEqual(Array.from(result.keys()), []);
192+
193+
t.deepEqual(linterContext.generateLintResults(), [
194+
{
195+
coverageInfo: [],
196+
errorCount: 0,
197+
fatalErrorCount: 0,
198+
warningCount: 1,
199+
filePath: "/resources/file.view.xml",
200+
messages: [
201+
{
202+
column: undefined,
203+
line: undefined,
204+
message: `Syntax error after applying autofix for '/resources/file.view.xml'. ` +
205+
`This is likely a UI5 linter internal issue. Please check the verbose log. ` +
206+
`Please report this using the bug report template: ` +
207+
`https://github.com/UI5/linter/issues/new?template=bug-report.md`,
140208
ruleId: "autofix-error",
141209
severity: 1,
142210
},

0 commit comments

Comments
 (0)