diff --git a/src/detectors/typeChecker/FileLinter.ts b/src/detectors/typeChecker/FileLinter.ts index ae99e406..3be04f34 100644 --- a/src/detectors/typeChecker/FileLinter.ts +++ b/src/detectors/typeChecker/FileLinter.ts @@ -55,6 +55,7 @@ export default class FileLinter { // const nodeType = this.#checker.getTypeAtLocation(node); this.analyzePropertyAccessExpression(node as ts.CallExpression); // Check for global this.analyzeCallExpression(node as ts.CallExpression); // Check for deprecation + this.analyzeLibInitCall(node as ts.CallExpression); // Check for sap/ui/core/Lib.init usages } else if (node.kind === ts.SyntaxKind.PropertyAccessExpression || node.kind === ts.SyntaxKind.ElementAccessExpression) { this.analyzePropertyAccessExpression( @@ -211,6 +212,74 @@ export default class FileLinter { }); } + getSymbolModuleDeclaration(symbol: ts.Symbol) { + let parent = symbol.valueDeclaration?.parent; + while (parent && !ts.isModuleDeclaration(parent)) { + parent = parent.parent; + } + return parent; + } + + analyzeLibInitCall(node: ts.CallExpression) { + if (!ts.isIdentifier(node.expression) && // Assignment `const LibInit = Library.init` and destructuring + !ts.isPropertyAccessExpression(node.expression) && /* Lib.init() */ + !ts.isElementAccessExpression(node.expression) /* Lib["init"]() */) { + return; + } + + const nodeExp = node.expression; + const nodeType = this.#checker.getTypeAtLocation(nodeExp); + if (!nodeType.symbol || nodeType.symbol.getName() !== "init") { + return; + } + + const moduleDeclaration = this.getSymbolModuleDeclaration(nodeType.symbol); + if (!moduleDeclaration || moduleDeclaration.name.text !== "sap/ui/core/Lib") { + return; + } + + const initArg = node?.arguments[0] && + ts.isObjectLiteralExpression(node.arguments[0]) && + node.arguments[0]; + + let nodeToHighlight; + + if (!initArg) { + nodeToHighlight = node; + } else { + const apiVersionNode = initArg.properties.find((prop) => { + return ts.isPropertyAssignment(prop) && + ts.isIdentifier(prop.name) && + prop.name.text === "apiVersion"; + }); + + if (!apiVersionNode) { // No arguments or no 'apiVersion' property + nodeToHighlight = node; + } else if (ts.isPropertyAssignment(apiVersionNode) && + apiVersionNode.initializer.getText() !== "2") { // String value would be "\"2\"" + nodeToHighlight = apiVersionNode; + } + } + + if (nodeToHighlight) { + let importedVarName: string; + if (ts.isIdentifier(nodeExp)) { + importedVarName = nodeExp.getText(); + } else { + importedVarName = nodeExp.expression.getText() + ".init"; + } + + this.#reporter.addMessage({ + node: nodeToHighlight, + severity: LintMessageSeverity.Error, + ruleId: "ui5-linter-no-partially-deprecated-api", + message: + `Call to ${importedVarName}() must be declared with property {apiVersion: 2}`, + messageDetails: this.#messageDetails ? `{@link sap.ui.core.Lib.init Lib.init}` : undefined, + }); + } + } + getDeprecationInfoForAccess(node: ts.AccessExpression): DeprecationInfo | null { let symbol; if (ts.isPropertyAccessExpression(node)) { diff --git a/test/fixtures/linter/rules/NoDeprecatedApi/library.js b/test/fixtures/linter/rules/NoDeprecatedApi/library.js new file mode 100644 index 00000000..019befec --- /dev/null +++ b/test/fixtures/linter/rules/NoDeprecatedApi/library.js @@ -0,0 +1,42 @@ +/*! + * ${copyright} + */ +sap.ui.define([ + "sap/ui/core/Lib", +], function (Library) { + "use strict"; + + Library.init(); + Library.init("a"); + Library.init({}); + Library.init({ + test: 12 + }); + Library.init({ + apiVersion: "23" + }); + Library.init({ + apiVersion: 11 + }); + Library.init({ + apiVersion: "2" + }); + Library["init"]({ + apiVersion: 1 + }); + + const LibInit = Library.init; + LibInit({ + apiVersion: 1 + }); + + const {init} = Library; + init({ + apiVersion: 1 + }); + + const {init: intRenames} = Library; + intRenames({ + apiVersion: 1 + }); +}); diff --git a/test/fixtures/linter/rules/NoDeprecatedApi/library_negative.js b/test/fixtures/linter/rules/NoDeprecatedApi/library_negative.js new file mode 100644 index 00000000..055bb82d --- /dev/null +++ b/test/fixtures/linter/rules/NoDeprecatedApi/library_negative.js @@ -0,0 +1,36 @@ +/*! + * ${copyright} + */ +sap.ui.define([ + "sap/ui/core/Lib", +], function (Library) { + "use strict"; + + Library.init({ + apiVersion: 2 + }); + + Library["init"]({ + apiVersion: 2 + }); + + // Should be ignored + Library.load({ + apiVersion: 23 + }); + + const LibInit = Library.init; + LibInit({ + apiVersion: 2 + }); + + const {init} = Library; + init({ + apiVersion: 2 + }); + + const {init: intRenames} = Library; + intRenames({ + apiVersion: 2 + }); +}); diff --git a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md index 12c97140..eee0a0b2 100644 --- a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md +++ b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.md @@ -788,6 +788,136 @@ Generated by [AVA](https://avajs.dev). }, ] +## General: library.js + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 11, + fatalErrorCount: 0, + filePath: 'library.js', + messages: [ + { + column: 2, + fatal: undefined, + line: 9, + message: 'Call to Library.init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 2, + fatal: undefined, + line: 10, + message: 'Call to Library.init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 2, + fatal: undefined, + line: 11, + message: 'Call to Library.init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 2, + fatal: undefined, + line: 12, + message: 'Call to Library.init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 3, + fatal: undefined, + line: 16, + message: 'Call to Library.init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 3, + fatal: undefined, + line: 19, + message: 'Call to Library.init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 3, + fatal: undefined, + line: 22, + message: 'Call to Library.init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 3, + fatal: undefined, + line: 25, + message: 'Call to Library.init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 3, + fatal: undefined, + line: 30, + message: 'Call to LibInit() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 3, + fatal: undefined, + line: 35, + message: 'Call to init() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + { + column: 3, + fatal: undefined, + line: 40, + message: 'Call to intRenames() must be declared with property {apiVersion: 2}', + messageDetails: 'Lib.init (https://ui5.sap.com/1.120/#/api/sap.ui.core.Lib)', + ruleId: 'ui5-linter-no-partially-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + }, + ] + +## General: library_negative.js + +> Snapshot 1 + + [ + { + coverageInfo: [], + errorCount: 0, + fatalErrorCount: 0, + filePath: 'library_negative.js', + messages: [], + warningCount: 0, + }, + ] + ## General: manifest.json > Snapshot 1 diff --git a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap index 21210806..c5cc4143 100644 Binary files a/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap and b/test/lib/linter/rules/snapshots/NoDeprecatedApi.ts.snap differ