Skip to content

Commit 1dda012

Browse files
d3xter666maxreichmannmatz3RandomByteflovogt
authored
feat: Autofix deprecated jQuery.sap APIs (#614)
Migrate deprecated legacy jQuery.sap APIs to new recommended APIs. Fixes: #520 Fixes: #521 Fixes: #522 Fixes: #524 Fixes: #525 Fixes: #527 Fixes: #528 Fixes: #529 Fixes: #531 Fixes: #532 Fixes: #542 Fixes: #543 Fixes: #555 Fixes: #561 Fixes: #562 Fixes: #563 Fixes: #589 Fixes: #590 JIRA: CPOUI5FOUNDATION-990 --------- Co-authored-by: Max Reichmann <[email protected]> Co-authored-by: Matthias Osswald <[email protected]> Co-authored-by: Merlin Beutlberger <[email protected]> Co-authored-by: Florian Vogt <[email protected]>
1 parent a81f61f commit 1dda012

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

56 files changed

+5881
-384
lines changed

docs/Rules.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
- [no-implicit-globals](#no-implicit-globals)
1414
- [no-pseudo-modules](#no-pseudo-modules)
1515
- [parsing-error](#parsing-error)
16+
- [autofix-error](#autofix-error)
1617
- [prefer-test-starter](#prefer-test-starter)
1718
- [ui5-class-declaration](#ui5-class-declaration)
1819
- [unsupported-api-usage](#unsupported-api-usage)
@@ -99,6 +100,10 @@ Checks for dependencies to pseudo modules in the code.
99100

100101
Syntax/parsing errors that appear during the linting process are reported with this rule.
101102

103+
## autofix-error
104+
105+
An expected autofix could not be applied. This is likely a UI5 linter internal issue. Please report this using the bug report template: [github.com/SAP/ui5-linter/issues/](https://github.com/SAP/ui5-linter/issues/new?template=bug-report.md)
106+
102107
## prefer-test-starter
103108

104109
Checks whether test-related files are using the Test Starter concept.

src/autofix/autofix.ts

Lines changed: 108 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,12 @@ import {MESSAGE} from "../linter/messages.js";
55
import {ModuleDeclaration} from "../linter/ui5Types/amdTranspiler/parseModuleDeclaration.js";
66
import generateSolutionNoGlobals from "./solutions/noGlobals.js";
77
import {getLogger} from "@ui5/logger";
8-
import {addDependencies} from "./solutions/amdImports.js";
8+
import {addDependencies, removeDependencies} from "./solutions/amdImports.js";
99
import {RequireExpression} from "../linter/ui5Types/amdTranspiler/parseRequire.js";
1010
import {Resource} from "@ui5/fs";
1111
import {collectIdentifiers} from "./utils.js";
12+
import {ExportCodeToBeUsed} from "../linter/ui5Types/fixHints/FixHints.js";
13+
import generateSolutionCodeReplacer from "./solutions/codeReplacer.js";
1214

1315
const log = getLogger("linter:autofix");
1416

@@ -42,7 +44,7 @@ interface InsertChange extends AbstractChangeSet {
4244
value: string;
4345
}
4446

45-
interface ReplaceChange extends AbstractChangeSet {
47+
export interface ReplaceChange extends AbstractChangeSet {
4648
action: ChangeAction.REPLACE;
4749
end: number;
4850
value: string;
@@ -62,10 +64,9 @@ export interface Position {
6264
pos: number;
6365
}
6466
export interface GlobalPropertyAccessNodeInfo {
65-
globalVariableName: string;
66-
namespace: string;
6767
moduleName: string;
68-
exportName?: string;
68+
exportNameToBeUsed?: string;
69+
exportCodeToBeUsed?: ExportCodeToBeUsed;
6970
propertyAccess?: string;
7071
position: Position;
7172
node?: ts.Identifier | ts.PropertyAccessExpression | ts.ElementAccessExpression;
@@ -87,12 +88,14 @@ export type ModuleDeclarationInfo = ExistingModuleDeclarationInfo | NewModuleDec
8788
export interface ExistingModuleDeclarationInfo {
8889
moduleDeclaration: ModuleDeclaration | RequireExpression;
8990
importRequests: ImportRequests;
91+
additionalNodeInfos: (DeprecatedApiAccessNode | GlobalPropertyAccessNodeInfo)[];
9092
}
9193

9294
export interface NewModuleDeclarationInfo {
9395
declareCall: ts.CallExpression;
9496
requireCalls: Map<string, ts.CallExpression[]>;
9597
importRequests: ImportRequests;
98+
additionalNodeInfos: (DeprecatedApiAccessNode | GlobalPropertyAccessNodeInfo)[];
9699
endPos?: number;
97100
}
98101

@@ -131,24 +134,31 @@ function createProgram(inputFileNames: string[], host: ts.CompilerHost): ts.Prog
131134
return ts.createProgram(inputFileNames, compilerOptions, host);
132135
}
133136

134-
function getJsErrors(code: string, resourcePath: string) {
135-
const sourceFile = ts.createSourceFile(
137+
function createSourceFile(resourcePath: string, code: string) {
138+
return ts.createSourceFile(
136139
resourcePath,
137140
code,
138-
ts.ScriptTarget.ES2022,
139-
true,
140-
ts.ScriptKind.JS
141+
{
142+
languageVersion: ts.ScriptTarget.ES2022,
143+
jsDocParsingMode: ts.JSDocParsingMode.ParseNone,
144+
}
141145
);
146+
}
142147

143-
const host = createCompilerHost(new Map([[resourcePath, sourceFile]]));
144-
const program = createProgram([resourcePath], host);
148+
function getJsErrorsForSourceFile(sourceFile: ts.SourceFile, program: ts.Program) {
145149
const diagnostics = ts.getPreEmitDiagnostics(program, sourceFile);
146-
147150
return diagnostics.filter(function (d) {
148151
return d.file === sourceFile && d.category === ts.DiagnosticCategory.Error;
149152
});
150153
}
151154

155+
function getJsErrors(code: string, resourcePath: string) {
156+
const sourceFile = createSourceFile(resourcePath, code);
157+
const host = createCompilerHost(new Map([[resourcePath, sourceFile]]));
158+
const program = createProgram([resourcePath], host);
159+
return getJsErrorsForSourceFile(sourceFile, program);
160+
}
161+
152162
function getAutofixMessages(resource: AutofixResource) {
153163
// Collect modules of which at least one message has the conditional fixHint flag set
154164
const conditionalModuleAccess = new Set<string>();
@@ -176,6 +186,27 @@ function getAutofixMessages(resource: AutofixResource) {
176186
return messagesById;
177187
}
178188

189+
export function getModuleDeclarationForPosition(
190+
position: number, moduleDeclarations: Map<ts.CallExpression, ExistingModuleDeclarationInfo>
191+
): ModuleDeclarationInfo | undefined {
192+
const potentialDeclarations: {declaration: ModuleDeclarationInfo; start: number}[] = [];
193+
for (const [_, moduleDeclarationInfo] of moduleDeclarations) {
194+
const {moduleDeclaration} = moduleDeclarationInfo;
195+
const factory = "factory" in moduleDeclaration ? moduleDeclaration.factory : moduleDeclaration.callback;
196+
if (!factory || factory.getStart() > position || factory.getEnd() < position) {
197+
continue;
198+
}
199+
potentialDeclarations.push({
200+
declaration: moduleDeclarationInfo,
201+
start: factory.getStart(),
202+
});
203+
}
204+
// Sort by start position so that the declaration closest to the position is returned
205+
// This is relevant in case of nested sap.ui.require calls
206+
potentialDeclarations.sort((a, b) => a.start - b.start);
207+
return potentialDeclarations.pop()?.declaration;
208+
}
209+
179210
export default async function ({
180211
rootDir: _unused1,
181212
namespace: _unused2,
@@ -189,7 +220,9 @@ export default async function ({
189220
const messagesById = getAutofixMessages(autofixResource);
190221
// Currently only global access autofixes are supported
191222
// This needs to stay aligned with the applyFixes function
192-
if (messagesById.has(MESSAGE.NO_GLOBALS)) {
223+
if (messagesById.has(MESSAGE.NO_GLOBALS) ||
224+
messagesById.has(MESSAGE.DEPRECATED_API_ACCESS) ||
225+
messagesById.has(MESSAGE.DEPRECATED_FUNCTION_CALL)) {
193226
messages.set(autofixResource.resource.getPath(), messagesById);
194227
resources.push(autofixResource.resource);
195228
}
@@ -217,16 +250,35 @@ export default async function ({
217250
const checker = program.getTypeChecker();
218251
const res: AutofixResult = new Map();
219252
for (const [resourcePath, sourceFile] of sourceFiles) {
253+
// Checking for syntax errors in the original source file.
254+
// We should not apply autofixes to files with syntax errors
255+
const existingJsErrors = getJsErrorsForSourceFile(sourceFile, program);
256+
if (existingJsErrors.length) {
257+
log.verbose(`Skipping autofix for '${resourcePath}'. Syntax error in original source file : ` +
258+
`${existingJsErrors.map((d) => d.messageText as string).join(", ")}`);
259+
continue;
260+
}
261+
220262
log.verbose(`Applying autofixes to ${resourcePath}`);
221-
const newContent = applyFixes(checker, sourceFile, resourcePath, messages.get(resourcePath)!);
263+
let newContent;
264+
try {
265+
newContent = applyFixes(checker, sourceFile, resourcePath, messages.get(resourcePath)!);
266+
} catch (err) {
267+
if (err instanceof Error) {
268+
log.verbose(`Error while applying autofix to ${resourcePath}: ${err}`);
269+
context.addLintingMessage(resourcePath, MESSAGE.AUTOFIX_ERROR, {message: err.message});
270+
continue;
271+
}
272+
throw err;
273+
}
222274
if (newContent) {
223275
const jsErrors = getJsErrors(newContent, resourcePath);
224276
if (jsErrors.length) {
225277
const message = `Syntax error after applying autofix for '${resourcePath}': ` +
226278
jsErrors.map((d) => d.messageText as string).join(", ");
227279
log.verbose(message);
228280
log.verbose(resourcePath + ":\n" + newContent);
229-
context.addLintingMessage(resourcePath, MESSAGE.PARSING_ERROR, {message});
281+
context.addLintingMessage(resourcePath, MESSAGE.AUTOFIX_ERROR, {message});
230282
} else {
231283
log.verbose(`Autofix applied to ${resourcePath}`);
232284
res.set(resourcePath, newContent);
@@ -245,20 +297,55 @@ function applyFixes(
245297

246298
const changeSet: ChangeSet[] = [];
247299
let existingModuleDeclarations = new Map<ts.CallExpression, ExistingModuleDeclarationInfo>();
300+
const messages: RawLintMessage<
301+
MESSAGE.NO_GLOBALS | MESSAGE.DEPRECATED_API_ACCESS | MESSAGE.DEPRECATED_FUNCTION_CALL>[] = [];
302+
248303
if (messagesById.has(MESSAGE.NO_GLOBALS)) {
249-
existingModuleDeclarations = generateSolutionNoGlobals(
250-
checker, sourceFile, content,
251-
messagesById.get(MESSAGE.NO_GLOBALS) as RawLintMessage<MESSAGE.NO_GLOBALS>[],
252-
changeSet, []);
304+
messages.push(
305+
...messagesById.get(MESSAGE.NO_GLOBALS) as RawLintMessage<MESSAGE.NO_GLOBALS>[]
306+
);
307+
}
308+
309+
if (messagesById.has(MESSAGE.DEPRECATED_API_ACCESS)) {
310+
messages.push(
311+
...messagesById.get(MESSAGE.DEPRECATED_API_ACCESS) as RawLintMessage<MESSAGE.DEPRECATED_API_ACCESS>[]
312+
);
313+
}
314+
315+
if (messagesById.has(MESSAGE.DEPRECATED_FUNCTION_CALL)) {
316+
messages.push(
317+
...messagesById.get(MESSAGE.DEPRECATED_FUNCTION_CALL) as RawLintMessage<MESSAGE.DEPRECATED_FUNCTION_CALL>[]
318+
);
319+
}
320+
321+
if (messages.length === 0) {
322+
return undefined;
253323
}
254324

325+
existingModuleDeclarations = generateSolutionNoGlobals(
326+
checker, sourceFile, content,
327+
messages,
328+
changeSet, []);
329+
255330
// Collect all identifiers in the source file to ensure unique names when adding imports
256331
const identifiers = collectIdentifiers(sourceFile);
257332

258333
for (const [defineCall, moduleDeclarationInfo] of existingModuleDeclarations) {
259-
addDependencies(defineCall, moduleDeclarationInfo, changeSet, resourcePath, identifiers);
334+
// TODO: Find a better way to define modules for removal
335+
const moduleRemovals = new Set(["sap/base/strings/NormalizePolyfill", "jquery.sap.unicode"]);
336+
337+
// Remove dependencies from the existing module declaration
338+
removeDependencies(moduleRemovals,
339+
moduleDeclarationInfo, changeSet, resourcePath, identifiers);
340+
341+
// Resolve dependencies for the module declaration
342+
addDependencies(defineCall, moduleDeclarationInfo, changeSet, resourcePath, identifiers, moduleRemovals);
260343
}
261344

345+
// More complex code replacers. Mainly arguments shifting and repositioning, replacements,
346+
// based on arguments' context
347+
generateSolutionCodeReplacer(existingModuleDeclarations, messages, changeSet, sourceFile, identifiers);
348+
262349
if (changeSet.length === 0) {
263350
// No modifications needed
264351
return undefined;

0 commit comments

Comments
 (0)