Skip to content

feat(autofix): Fix basic deprecated Configuration APIs #683

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 15 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion src/autofix/autofix.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ export default async function ({
// This needs to stay aligned with the applyFixes function
if (messagesById.has(MESSAGE.NO_GLOBALS) ||
messagesById.has(MESSAGE.DEPRECATED_API_ACCESS) ||
messagesById.has(MESSAGE.DEPRECATED_PROPERTY) ||
messagesById.has(MESSAGE.DEPRECATED_FUNCTION_CALL)) {
messages.set(autofixResource.resource.getPath(), messagesById);
resources.push(autofixResource.resource);
Expand Down Expand Up @@ -298,7 +299,11 @@ function applyFixes(
const changeSet: ChangeSet[] = [];
let existingModuleDeclarations = new Map<ts.CallExpression, ExistingModuleDeclarationInfo>();
const messages: RawLintMessage<
MESSAGE.NO_GLOBALS | MESSAGE.DEPRECATED_API_ACCESS | MESSAGE.DEPRECATED_FUNCTION_CALL>[] = [];
MESSAGE.NO_GLOBALS |
MESSAGE.DEPRECATED_API_ACCESS |
MESSAGE.DEPRECATED_FUNCTION_CALL |
MESSAGE.DEPRECATED_PROPERTY
>[] = [];

if (messagesById.has(MESSAGE.NO_GLOBALS)) {
messages.push(
Expand All @@ -318,6 +323,12 @@ function applyFixes(
);
}

if (messagesById.has(MESSAGE.DEPRECATED_PROPERTY)) {
messages.push(
...messagesById.get(MESSAGE.DEPRECATED_PROPERTY) as RawLintMessage<MESSAGE.DEPRECATED_PROPERTY>[]
);
}

if (messages.length === 0) {
return undefined;
}
Expand Down
7 changes: 6 additions & 1 deletion src/autofix/solutions/noGlobals.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,12 @@ const log = getLogger("linter:autofix:NoGlobals");

export default function generateSolutionNoGlobals(
checker: ts.TypeChecker, sourceFile: ts.SourceFile, content: string,
messages: RawLintMessage<MESSAGE.NO_GLOBALS | MESSAGE.DEPRECATED_API_ACCESS | MESSAGE.DEPRECATED_FUNCTION_CALL>[],
messages: RawLintMessage<
MESSAGE.NO_GLOBALS |
MESSAGE.DEPRECATED_API_ACCESS |
MESSAGE.DEPRECATED_FUNCTION_CALL |
MESSAGE.DEPRECATED_PROPERTY
>[],
changeSet: ChangeSet[], newModuleDeclarations: NewModuleDeclarationInfo[]
) {
// Collect all global property access nodes
Expand Down
34 changes: 29 additions & 5 deletions src/autofix/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import {getPropertyNameText} from "../linter/ui5Types/utils/utils.js";

export function findGreatestAccessExpression(node: ts.Identifier, matchPropertyAccess?: string):
ts.Identifier | ts.PropertyAccessExpression | ts.ElementAccessExpression {
type Candidate = ts.Identifier | ts.PropertyAccessExpression | ts.ElementAccessExpression;
type Candidate = ts.Identifier | ts.PropertyAccessExpression | ts.ElementAccessExpression | ts.CallExpression;
let scanNode: Candidate = node;
let propertyAccessChain: string[] = [];
if (matchPropertyAccess) {
Expand All @@ -19,15 +19,38 @@ export function findGreatestAccessExpression(node: ts.Identifier, matchPropertyA
}
}

while (ts.isPropertyAccessExpression(scanNode.parent) || ts.isElementAccessExpression(scanNode.parent)) {
const returnType = (node: Candidate) => {
if (!ts.isCallExpression(node)) {
return node;
} else if (
ts.isPropertyAccessExpression(node.expression) ||
ts.isIdentifier(node.expression) ||
ts.isElementAccessExpression(node.expression)) {
return node.expression;
} else {
throw new Error("Expected node to be a property access expression or identifier");
}
};

while (ts.isPropertyAccessExpression(scanNode.parent) ||
ts.isElementAccessExpression(scanNode.parent) ||
(ts.isCallExpression(scanNode.parent) &&
// Do not go above the actual call if wrapped in another call
ts.isPropertyAccessExpression(scanNode.parent.expression) &&
ts.isPropertyAccessExpression(scanNode) &&
scanNode.parent.expression.name === scanNode.name)) {
scanNode = scanNode.parent;
if (matchPropertyAccess) {
const nextPropertyAccess = propertyAccessChain.shift();

if (ts.isCallExpression(scanNode) && ts.isPropertyAccessExpression(scanNode.parent)) {
scanNode = scanNode.parent;
}

let propName;
if (ts.isPropertyAccessExpression(scanNode)) {
propName = getPropertyNameText(scanNode.name);
} else {
} else if (!ts.isCallExpression(scanNode)) {
if (
ts.isStringLiteralLike(scanNode.argumentExpression) ||
ts.isNumericLiteral(scanNode.argumentExpression)
Expand All @@ -41,11 +64,12 @@ export function findGreatestAccessExpression(node: ts.Identifier, matchPropertyA
throw new Error(`Expected node to be ${nextPropertyAccess} but got ${propName}`);
}
if (!propertyAccessChain.length) {
return scanNode;
return returnType(scanNode);
}
}
}
return scanNode;

return returnType(scanNode);
}

export function matchPropertyAccessExpression(node: ts.PropertyAccessExpression, match: string): boolean {
Expand Down
12 changes: 10 additions & 2 deletions src/linter/ui5Types/SourceFileLinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -938,7 +938,8 @@ export default class SourceFileLinter {
ts.isPropertyAccessExpression(exprNode) ||
ts.isCallExpression(exprNode)) {
fixHints = this.getJquerySapFixHints(exprNode) ??
this.getCoreFixHints(exprNode, deprecationInfo.ui5TypeInfo);
this.getCoreFixHints(exprNode, deprecationInfo.ui5TypeInfo) ??
this.getConfigFixHints(exprNode, deprecationInfo.ui5TypeInfo);
}
this.#reporter.addMessage(MESSAGE.DEPRECATED_FUNCTION_CALL, {
functionName: propName,
Expand Down Expand Up @@ -1468,7 +1469,10 @@ export default class SourceFileLinter {
propertyName: deprecationInfo.symbol.escapedName as string,
namespace,
details: deprecationInfo.messageDetails,
}, {node, ui5TypeInfo: deprecationInfo.ui5TypeInfo});
}, {
node, ui5TypeInfo: deprecationInfo.ui5TypeInfo,
fixHints: this.getConfigFixHints(node, deprecationInfo.ui5TypeInfo),
});
}
return true;
}
Expand Down Expand Up @@ -1817,4 +1821,8 @@ export default class SourceFileLinter {
getCoreFixHints(node: ts.CallExpression | ts.AccessExpression, ui5TypeInfo?: Ui5TypeInfo) {
return this.#fixHintsGenerator?.getCoreFixHints(node, ui5TypeInfo);
}

getConfigFixHints(node: ts.CallExpression | ts.AccessExpression, ui5TypeInfo?: Ui5TypeInfo) {
return this.#fixHintsGenerator?.getConfigFixHints(node, ui5TypeInfo);
}
}
170 changes: 170 additions & 0 deletions src/linter/ui5Types/fixHints/ConfigurationFixHintsGenerator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import ts from "typescript";
import type {ExportCodeToBeUsed, FixHints} from "./FixHints.js";
import {isExpectedValueExpression} from "../utils/utils.js";
import {getModuleTypeInfo, Ui5TypeInfo, Ui5TypeInfoKind} from "../Ui5TypeInfo.js";

const configurationModulesReplacements = new Map<string, FixHints>([
// https://github.com/SAP/ui5-linter/issues/620
["getAccessibility", {
moduleName: "sap/ui/core/ControlBehavior", exportNameToBeUsed: "isAccessibilityEnabled",
}],
["getActiveTerminologies", {
moduleName: "sap/base/i18n/Localization", exportNameToBeUsed: "getActiveTerminologies",
}],
["getAllowlistService", {
moduleName: "sap/ui/security/Security", exportNameToBeUsed: "getAllowlistService",
}],
["getAnimationMode", {
moduleName: "sap/ui/core/ControlBehavior", exportNameToBeUsed: "getAnimationMode",
}],
["getCalendarType", {
moduleName: "sap/base/i18n/Formatting", exportNameToBeUsed: "getCalendarType",
}],
["getCalendarWeekNumbering", {
moduleName: "sap/base/i18n/Formatting", exportNameToBeUsed: "getCalendarWeekNumbering",
}],
["getFrameOptions", {
moduleName: "sap/ui/security/Security", exportNameToBeUsed: "getFrameOptions",
}],
["getLanguage", {
moduleName: "sap/base/i18n/Localization", exportNameToBeUsed: "getLanguage",
}],
["getRTL", {
moduleName: "sap/base/i18n/Localization", exportNameToBeUsed: "getRTL",
}],
["getSAPLogonLanguage", {
moduleName: "sap/base/i18n/Localization", exportNameToBeUsed: "getSAPLogonLanguage",
}],
["getSecurityTokenHandlers", {
moduleName: "sap/ui/security/Security", exportNameToBeUsed: "getSecurityTokenHandlers",
}],
["getTheme", {
moduleName: "sap/ui/core/Theming", exportNameToBeUsed: "getTheme",
}],
["getTimezone", {
moduleName: "sap/base/i18n/Localization", exportNameToBeUsed: "getTimezone",
}],
["getUIDPrefix", {
moduleName: "sap/ui/base/ManagedObjectMetadata", exportNameToBeUsed: "getUIDPrefix",
}],
["getWhitelistService", {
moduleName: "sap/ui/security/Security", exportNameToBeUsed: "getAllowlistService",
}],
["setAnimationMode", {
moduleName: "sap/ui/core/ControlBehavior", exportNameToBeUsed: "setAnimationMode",
}],
["AnimationMode", {
moduleName: "sap/ui/core/AnimationMode", propertyAccess: "AnimationMode",
}],
["setSecurityTokenHandlers", {
moduleName: "sap/ui/security/Security", exportNameToBeUsed: "setSecurityTokenHandlers",
}],

// TODO: Complex replacements: Old API returns this, but new API returns undefined.
// setCalendarType()
// setCalendarWeekNumbering()
// setFormatLocale()
// setLanguage()
// setRTL()
// setTheme()
// setTimezone()

["getLanguageTag", {
moduleName: "sap/base/i18n/Localization", exportCodeToBeUsed: "$moduleIdentifier.getLanguageTag().toString()",
}],

// TODO: Complex replacement: Discuss: Old API returns boolean, new API returns AnimationMode. How to migrate?
// (-> 2 new module imports) How to setup this map entry?
// getAnimation()

["getFormatLocale", {
moduleName: "sap/base/i18n/Formatting", exportCodeToBeUsed: "$moduleIdentifier.getLanguageTag().toString()",
}],

// TODO: Complex replacement:
// "Configuration.getLocale()" needs to be replaced with "new Locale(Localization.getLanguageTag())".
// (-> 2 new module imports) How to setup this map entry?
// getLocale()

// Migration not possible
// Old API is sync and new API is async
// getVersion()
]);

export default class ConfigurationFixHintsGenerator {
getFixHints(node: ts.CallExpression | ts.AccessExpression, ui5TypeInfo?: Ui5TypeInfo): FixHints | undefined {
if (!ts.isPropertyAccessExpression(node) || !ui5TypeInfo) {
return undefined;
}

const moduleTypeInfo = getModuleTypeInfo(ui5TypeInfo);
if (!moduleTypeInfo || moduleTypeInfo.name !== "sap/ui/core/Configuration") {
return undefined;
}

if (
(ui5TypeInfo.kind !== Ui5TypeInfoKind.Method &&
ui5TypeInfo.kind !== Ui5TypeInfoKind.Property) ||
ui5TypeInfo.parent.kind !== Ui5TypeInfoKind.Class ||
ui5TypeInfo.parent.name !== "Configuration"
) {
return undefined;
}

const methodName = ui5TypeInfo.name;
const moduleReplacement = configurationModulesReplacements.get(methodName);
if (!moduleReplacement) {
return undefined;
}

if (moduleReplacement.propertyAccess) {
const chain: string[] = [];
let curNode: ts.Node | undefined = node;
while (curNode) {
if (ts.isPropertyAccessExpression(curNode)) {
chain.unshift(curNode.name.getText());
curNode = curNode.expression;
} else if (ts.isIdentifier(curNode)) {
chain.unshift(curNode.getText());
break;
} else if (ts.isCallExpression(curNode)) {
curNode = curNode.expression;
} else {
break;
}
}

// Get the full chained property access expression chain
moduleReplacement.propertyAccess = chain.join(".");
}

let exportCodeToBeUsed;
if (moduleReplacement.exportCodeToBeUsed) {
exportCodeToBeUsed = {
name: moduleReplacement.exportCodeToBeUsed,
// Check whether the return value of the call expression is assigned to a variable,
// passed to another function or used elsewhere.
isExpectedValue: isExpectedValueExpression(node),
} as ExportCodeToBeUsed;

let callExpression;
if (ts.isCallExpression(node.parent) &&
// if a prop is wrapped in a function, then current.parent is the call expression
// which is wrong. That's why check if parent expression is actually the current node
// which would ensure that the prop is actually a call expression.
node.parent.expression === node) {
callExpression = node.parent;
}

// Extract arguments from the call expression
if (callExpression) {
exportCodeToBeUsed.args = callExpression.arguments.map((arg) => ({
value: arg.getText(),
kind: arg?.kind,
}));
}
}

return {...moduleReplacement, exportCodeToBeUsed};
}
}
8 changes: 8 additions & 0 deletions src/linter/ui5Types/fixHints/FixHintsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import JquerySapFixHintsGenerator from "./JquerySapFixHintsGenerator.js";
import CoreFixHintsGenerator from "./CoreFixHintsGenerator.js";
import {FixHints} from "./FixHints.js";
import {Ui5TypeInfo} from "../Ui5TypeInfo.js";
import ConfigurationFixHintsGenerator from "./ConfigurationFixHintsGenerator.js";

export default class FixHintsGenerator {
private globalsGenerator: GlobalsFixHintsGenerator;
private jquerySapGenerator: JquerySapFixHintsGenerator;
private coreGenerator: CoreFixHintsGenerator;
private configGenerator: ConfigurationFixHintsGenerator;

constructor(
resourcePath: string,
Expand All @@ -18,6 +20,7 @@ export default class FixHintsGenerator {
this.globalsGenerator = new GlobalsFixHintsGenerator(resourcePath, ambientModuleCache);
this.jquerySapGenerator = new JquerySapFixHintsGenerator();
this.coreGenerator = new CoreFixHintsGenerator(ambientModuleCache);
this.configGenerator = new ConfigurationFixHintsGenerator();
}

public getGlobalsFixHints(node: ts.CallExpression | ts.AccessExpression): FixHints | undefined {
Expand All @@ -34,4 +37,9 @@ export default class FixHintsGenerator {
ui5TypeInfo?: Ui5TypeInfo): FixHints | undefined {
return this.coreGenerator.getFixHints(node, ui5TypeInfo);
}

public getConfigFixHints(node: ts.CallExpression | ts.AccessExpression,
ui5TypeInfo?: Ui5TypeInfo): FixHints | undefined {
return this.configGenerator.getFixHints(node, ui5TypeInfo);
}
}
36 changes: 36 additions & 0 deletions test/e2e/snapshots/runtime.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,42 @@ Generated by [AVA](https://avajs.dev).
> stdout

[
{
errorCount: 4,
fatalErrorCount: 0,
filePath: 'test/qunit/Configuration.qunit.js',
messages: [
{
column: 2,
line: 3,
message: 'Import of deprecated module \'sap/ui/core/Configuration\'',
ruleId: 'no-deprecated-api',
severity: 2,
},
{
column: 28,
line: 16,
message: 'Access of global variable \'sap\' (sap.ui.getCore)',
ruleId: 'no-globals',
severity: 2,
},
{
column: 35,
line: 16,
message: 'Call to deprecated function \'getCore\' (sap.ui.getCore)',
ruleId: 'no-deprecated-api',
severity: 2,
},
{
column: 45,
line: 16,
message: 'Call to deprecated function \'getConfiguration\' of class \'Core\'',
ruleId: 'no-deprecated-api',
severity: 2,
},
],
warningCount: 0,
},
{
errorCount: 8,
fatalErrorCount: 0,
Expand Down
Binary file modified test/e2e/snapshots/runtime.ts.snap
Binary file not shown.
Loading