Skip to content

Commit b085aa0

Browse files
committed
feat: Autofix deprecated class properties in XML
1 parent 1dda012 commit b085aa0

File tree

10 files changed

+276
-17
lines changed

10 files changed

+276
-17
lines changed

src/autofix/autofix.ts

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {Resource} from "@ui5/fs";
1111
import {collectIdentifiers} from "./utils.js";
1212
import {ExportCodeToBeUsed} from "../linter/ui5Types/fixHints/FixHints.js";
1313
import generateSolutionCodeReplacer from "./solutions/codeReplacer.js";
14+
import generateXmlSolutionDeprecatedApi from "./solutions/generateXmlSolutionDeprecatedApi.js";
1415

1516
const log = getLogger("linter:autofix");
1617

@@ -215,22 +216,27 @@ export default async function ({
215216
}: AutofixOptions): Promise<AutofixResult> {
216217
// Group messages by ID and only process files for which fixes are available
217218
const messages = new Map<string, Map<MESSAGE, RawLintMessage[]>>();
218-
const resources: Resource[] = [];
219+
const jsResources: Resource[] = [];
220+
const xmlResources: Resource[] = [];
219221
for (const [_, autofixResource] of autofixResources) {
220222
const messagesById = getAutofixMessages(autofixResource);
221-
// Currently only global access autofixes are supported
222-
// This needs to stay aligned with the applyFixes function
223+
// Set of supported message IDs needs to stay aligned with the applyFixes function
223224
if (messagesById.has(MESSAGE.NO_GLOBALS) ||
224225
messagesById.has(MESSAGE.DEPRECATED_API_ACCESS) ||
226+
messagesById.has(MESSAGE.DEPRECATED_PROPERTY_OF_CLASS) ||
225227
messagesById.has(MESSAGE.DEPRECATED_FUNCTION_CALL)) {
226228
messages.set(autofixResource.resource.getPath(), messagesById);
227-
resources.push(autofixResource.resource);
229+
if (autofixResource.resource.getPath().endsWith(".xml")) {
230+
xmlResources.push(autofixResource.resource);
231+
} else {
232+
jsResources.push(autofixResource.resource);
233+
}
228234
}
229235
}
230236

231237
const sourceFiles: SourceFiles = new Map();
232238
const resourcePaths = [];
233-
for (const resource of resources) {
239+
for (const resource of jsResources) {
234240
const resourcePath = resource.getPath();
235241
const sourceFile = ts.createSourceFile(
236242
resource.getPath(),
@@ -262,7 +268,7 @@ export default async function ({
262268
log.verbose(`Applying autofixes to ${resourcePath}`);
263269
let newContent;
264270
try {
265-
newContent = applyFixes(checker, sourceFile, resourcePath, messages.get(resourcePath)!);
271+
newContent = applyFixesJs(checker, sourceFile, resourcePath, messages.get(resourcePath)!);
266272
} catch (err) {
267273
if (err instanceof Error) {
268274
log.verbose(`Error while applying autofix to ${resourcePath}: ${err}`);
@@ -286,10 +292,18 @@ export default async function ({
286292
}
287293
}
288294

295+
for (const resource of xmlResources) {
296+
const resourcePath = resource.getPath();
297+
const newContent = await applyFixesXml(resource, messages.get(resourcePath)!);
298+
if (newContent) {
299+
res.set(resourcePath, newContent);
300+
}
301+
}
302+
289303
return res;
290304
}
291305

292-
function applyFixes(
306+
function applyFixesJs(
293307
checker: ts.TypeChecker, sourceFile: ts.SourceFile, resourcePath: ResourcePath,
294308
messagesById: Map<MESSAGE, RawLintMessage[]>
295309
): string | undefined {
@@ -353,6 +367,28 @@ function applyFixes(
353367
return applyChanges(content, changeSet);
354368
}
355369

370+
async function applyFixesXml(
371+
resource: Resource,
372+
messagesById: Map<MESSAGE, RawLintMessage[]>
373+
): Promise<string | undefined> {
374+
const changeSet: ChangeSet[] = [];
375+
const messages: RawLintMessage<MESSAGE.DEPRECATED_PROPERTY_OF_CLASS>[] = [];
376+
377+
if (messagesById.has(MESSAGE.DEPRECATED_PROPERTY_OF_CLASS)) {
378+
messages.push(
379+
...messagesById.get(
380+
MESSAGE.DEPRECATED_PROPERTY_OF_CLASS) as RawLintMessage<MESSAGE.DEPRECATED_PROPERTY_OF_CLASS>[]
381+
);
382+
}
383+
const content = await resource.getString();
384+
await generateXmlSolutionDeprecatedApi(messages, changeSet, content, resource);
385+
386+
if (changeSet.length === 0) {
387+
return undefined;
388+
}
389+
return applyChanges(content, changeSet);
390+
}
391+
356392
function applyChanges(content: string, changeSet: ChangeSet[]): string {
357393
changeSet.sort((a, b) => b.start - a.start);
358394
const s = new MagicString(content);
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import {Resource} from "@ui5/fs";
2+
import {Attribute, Position, SaxEventType} from "sax-wasm";
3+
import {RawLintMessage} from "../../linter/LinterContext.js";
4+
import {MESSAGE} from "../../linter/messages.js";
5+
import {parseXML} from "../../utils/xmlParser.js";
6+
import {ChangeAction, ChangeSet} from "../autofix.js";
7+
import {getLogger} from "@ui5/logger";
8+
9+
const log = getLogger("linter:autofix:generateXmlSolutionDeprecatedApi");
10+
11+
export default async function generateXmlSolutionDeprecatedApi(
12+
messages: RawLintMessage<MESSAGE.DEPRECATED_PROPERTY_OF_CLASS>[],
13+
changeSet: ChangeSet[], content: string, resource: Resource) {
14+
function toPosition(position: Position) {
15+
let pos: number;
16+
if (position.line === 0) {
17+
pos = position.character;
18+
} else {
19+
pos = 0;
20+
const lines = content.split("\n");
21+
for (let i = 0; i < position.line; i++) {
22+
pos += lines[i].length + 1; // +1 for the newline character we used to split the lines with
23+
}
24+
pos += position.character;
25+
}
26+
return pos;
27+
}
28+
29+
function handleAttribute(attr: Attribute) {
30+
// Check whether line and column match with any of the messages
31+
const line = attr.name.start.line + 1;
32+
const column = attr.name.start.character + 1;
33+
const message = messages.find((message) => {
34+
return message.position?.line === line && message.position?.column === column;
35+
});
36+
if (!message?.fixHints) {
37+
return;
38+
}
39+
40+
const {classProperty, classPropertyToBeUsed} = message.fixHints;
41+
42+
if (classProperty === undefined || classPropertyToBeUsed === undefined) {
43+
return;
44+
}
45+
46+
if (classPropertyToBeUsed) {
47+
changeSet.push({
48+
action: ChangeAction.REPLACE,
49+
start: toPosition(attr.name.start),
50+
end: toPosition(attr.name.end),
51+
value: classPropertyToBeUsed,
52+
});
53+
} else {
54+
changeSet.push({
55+
action: ChangeAction.DELETE,
56+
start: toPosition(attr.name.start),
57+
end: toPosition(attr.value.end) + 1,
58+
});
59+
}
60+
}
61+
62+
await parseXML(resource.getStream(), (event, tag) => {
63+
if (event === SaxEventType.Attribute) {
64+
handleAttribute(tag as Attribute);
65+
}
66+
}, SaxEventType.Attribute);
67+
}

src/linter/ui5Types/SourceFileLinter.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -701,12 +701,12 @@ export default class SourceFileLinter {
701701
if (!ts.isPropertyAssignment(prop)) {
702702
return;
703703
}
704-
const propertyName = getPropertyNameText(prop.name);
705-
if (!propertyName) {
704+
const propertyNameText = getPropertyNameText(prop.name);
705+
if (!propertyNameText) {
706706
return;
707707
}
708708
const propertySymbol = getSymbolForPropertyInConstructSignatures(
709-
possibleConstructSignatures, argIdx, propertyName
709+
possibleConstructSignatures, argIdx, propertyNameText
710710
);
711711
if (!propertySymbol) {
712712
return;
@@ -715,13 +715,22 @@ export default class SourceFileLinter {
715715
if (!deprecationInfo) {
716716
return;
717717
}
718+
719+
const propertyName = propertySymbol.escapedName as string;
720+
const className = this.checker.typeToString(nodeType);
721+
let fixHints;
722+
if (moduleDeclaration?.name.text) {
723+
fixHints = this.getDeprecatedClassPropertyFixHints(
724+
prop, propertyName, moduleDeclaration.name.text);
725+
}
718726
this.#reporter.addMessage(MESSAGE.DEPRECATED_PROPERTY_OF_CLASS,
719727
{
720-
propertyName: propertySymbol.escapedName as string,
721-
className: this.checker.typeToString(nodeType),
728+
propertyName,
729+
className,
722730
details: deprecationInfo.messageDetails,
723731
},
724-
prop
732+
prop,
733+
fixHints
725734
);
726735
});
727736
});
@@ -1812,4 +1821,8 @@ export default class SourceFileLinter {
18121821
getJquerySapFixHints(node: ts.CallExpression | ts.AccessExpression) {
18131822
return this.#fixHintsGenerator?.getJquerySapFixHints(node);
18141823
}
1824+
1825+
getDeprecatedClassPropertyFixHints(node: ts.PropertyAssignment, propertyName: string, className: string) {
1826+
return this.#fixHintsGenerator?.getDeprecatedClassPropertyFixHints(node, propertyName, className);
1827+
}
18151828
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import ts from "typescript";
2+
import type {FixHints} from "./FixHints.js";
3+
4+
const CLASS_PROPERTY_REPLACEMENTS = new Map<string, Map<string, FixHints>>([
5+
["sap/ui/comp/smarttable/SmartTable", new Map([
6+
["useExportToExcel", {
7+
classProperty: "useExportToExcel",
8+
classPropertyToBeUsed: "enableExport",
9+
}],
10+
])],
11+
["sap/ui/layout/form/SimpleForm", new Map([
12+
["minWidth", {
13+
classProperty: "minWidth",
14+
classPropertyToBeUsed: "",
15+
}],
16+
])],
17+
["sap/m/Button", new Map([
18+
["tap", {
19+
classProperty: "tap",
20+
classPropertyToBeUsed: "press",
21+
}],
22+
])],
23+
]);
24+
25+
export default class DeprprecatedClassPropertyGenerator {
26+
getFixHints(node: ts.PropertyAssignment, propertyName: string, className: string): FixHints | undefined {
27+
const fixHint = CLASS_PROPERTY_REPLACEMENTS.get(className)?.get(propertyName);
28+
return fixHint;
29+
}
30+
}

src/linter/ui5Types/fixHints/FixHints.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@ export interface FixHints {
3939
* e.g. `if (window.sap.ui.layout) { ... }`
4040
*/
4141
conditional?: boolean;
42+
43+
classProperty?: string;
44+
classPropertyToBeUsed?: string;
4245
}

src/linter/ui5Types/fixHints/FixHintsGenerator.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,20 @@ import {AmbientModuleCache} from "../AmbientModuleCache.js";
33
import GlobalsFixHintsGenerator from "./GlobalsFixHintsGenerator.js";
44
import JquerySapFixHintsGenerator from "./JquerySapFixHintsGenerator.js";
55
import {FixHints} from "./FixHints.js";
6+
import DeprecatedClassPropertyGenerator from "./DeprecatedClassPropertyGenerator.js";
67

78
export default class FixHintsGenerator {
89
private globalsGenerator: GlobalsFixHintsGenerator;
910
private jquerySapGenerator: JquerySapFixHintsGenerator;
11+
private deprecatedClassPropertyGenerator: DeprecatedClassPropertyGenerator;
1012

1113
constructor(
1214
resourcePath: string,
1315
ambientModuleCache: AmbientModuleCache
1416
) {
1517
this.globalsGenerator = new GlobalsFixHintsGenerator(resourcePath, ambientModuleCache);
1618
this.jquerySapGenerator = new JquerySapFixHintsGenerator();
19+
this.deprecatedClassPropertyGenerator = new DeprecatedClassPropertyGenerator();
1720
}
1821

1922
public getGlobalsFixHints(node: ts.CallExpression | ts.AccessExpression): FixHints | undefined {
@@ -25,4 +28,10 @@ export default class FixHintsGenerator {
2528
): FixHints | undefined {
2629
return this.jquerySapGenerator.getFixHints(node);
2730
}
31+
32+
public getDeprecatedClassPropertyFixHints(
33+
node: ts.PropertyAssignment, propertyName: string, className: string
34+
): FixHints | undefined {
35+
return this.deprecatedClassPropertyGenerator.getFixHints(node, propertyName, className);
36+
}
2837
}

src/utils/xmlParser.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import type {ReadStream} from "node:fs";
2-
import {Detail, Reader, SaxEventType, SAXParser, Tag, Text} from "sax-wasm";
2+
import {Detail, Reader, SaxEvent, SaxEventType, SAXParser, Tag, Text} from "sax-wasm";
33
import {finished} from "node:stream/promises";
44
import fs from "node:fs/promises";
55
import {createRequire} from "node:module";
@@ -61,9 +61,10 @@ async function initSaxWasm() {
6161
}
6262

6363
export async function parseXML(
64-
contentStream: ReadStream, parseHandler: (type: SaxEventType, tag: Reader<Detail>) => void) {
64+
contentStream: ReadStream, parseHandler: typeof SAXParser.prototype.eventHandler,
65+
events: number = SaxEventType.CloseTag | SaxEventType.OpenTag | SaxEventType.Comment) {
6566
const saxWasmBuffer = await initSaxWasm();
66-
const saxParser = new SAXParser(SaxEventType.CloseTag | SaxEventType.OpenTag | SaxEventType.Comment);
67+
const saxParser = new SAXParser(events);
6768

6869
saxParser.eventHandler = parseHandler;
6970

@@ -72,7 +73,7 @@ export async function parseXML(
7273
throw new Error("Unknown error during WASM Initialization");
7374
}
7475

75-
// stream from a file in the current directory
76+
// Start the stream
7677
contentStream.on("data", (chunk: Uint8Array) => {
7778
try {
7879
saxParser.write(chunk);
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<mvc:View xmlns:mvc="sap.ui.core.mvc"
2+
xmlns="sap.m"
3+
xmlns:table="sap.ui.table"
4+
xmlns:tablePlugins="sap.ui.table.plugins"
5+
xmlns:st="sap.ui.comp.smarttable"
6+
xmlns:form="sap.ui.layout.form"
7+
controllerName="com.myapp.controller.Main"
8+
>
9+
10+
<st:SmartTable
11+
entitySet="LineItemsSet"
12+
useExportToExcel="true"
13+
></st:SmartTable><!-- REPLACE: Property "useExportToExcel" is deprecated -->
14+
15+
<form:SimpleForm minWidth="100px"> <!-- REMOVE: Property "minWidth" is deprecated -->
16+
</form:SimpleForm>
17+
18+
<table:Table groupBy="some-column"> <!-- Association "groupBy" is deprecated -->
19+
<table:plugins> <!-- Aggregation "plugins" is deprecated -->
20+
<tablePlugins:MultiSelectionPlugin id="multi-selection-plugin" />
21+
</table:plugins>
22+
</table:Table>
23+
24+
<SegmentedButton id="segmented-button"> <!-- Default aggregation "buttons" is deprecated -->
25+
<Button id="segmented-button-inner" tap=".onButtonTap"/> <!-- REPLACE: Event "tap" is deprecated -->
26+
</SegmentedButton>
27+
28+
</mvc:View>

0 commit comments

Comments
 (0)