Skip to content

feat: Offer autofix for deprecated Core APIs #639

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

Open
wants to merge 21 commits into
base: autofix-deprecated-core-base
Choose a base branch
from
Open
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
99 changes: 99 additions & 0 deletions src/autofix/solutions/codeReplacer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,11 +314,110 @@ function patchMessageFixHints(fixHints?: FixHints, apiName?: string) {
`$moduleIdentifier.${fnName}(${cleanRedundantArguments(fixHints.exportCodeToBeUsed.args)})`;
}
}
} else if (apiName === "applyTheme" && fixHints?.moduleName === "sap/ui/core/Theming") {
if ((fixHints?.exportCodeToBeUsed?.args?.length ?? 0) > 1 &&
fixHints?.exportCodeToBeUsed?.args?.[1]?.value !== "undefined") {
fixHints = undefined; // We cannot handle this case
log.verbose(`Autofix skipped for ${apiName}. Transpilation is too ambiguous.`);
}
} else if (["attachIntervalTimer", "detachIntervalTimer"].includes(apiName ?? "")) {
if ((fixHints?.exportCodeToBeUsed?.args?.length ?? 0) > 1) {
fixHints = undefined; // We cannot handle this case
log.verbose(`Autofix skipped for ${apiName}. Transpilation is too ambiguous.`);
}
} else if (apiName === "loadLibrary") {
if (fixHints?.exportCodeToBeUsed?.args?.[1]?.kind === SyntaxKind.ObjectLiteralExpression) {
const libOptionsExpression =
extractKeyValuePairs(fixHints.exportCodeToBeUsed.args[1].value) as {async: boolean; url?: string};
if (libOptionsExpression.async === true) {
const newArg = {
name: fixHints.exportCodeToBeUsed.args[0].value.replace(/^['"]+|['"]+$/g, ""),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using back-ticks as first arg (library name) results into quoted backticks.

url: libOptionsExpression.url,
};
fixHints.exportCodeToBeUsed.args[0].value = JSON.stringify(newArg);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here the comments in the code get lost. Also, the used quotes in the code will be always replaced by double-quotes.

fixHints.exportCodeToBeUsed.args[0].kind = SyntaxKind.ObjectLiteralExpression;
} else {
fixHints = undefined; // We cannot handle this case
log.verbose(`Autofix skipped for ${apiName}. Transpilation is too ambiguous.`);
}
} else if (fixHints?.exportCodeToBeUsed?.args?.[1]?.kind === SyntaxKind.TrueKeyword) {
const newArg = {
name: fixHints.exportCodeToBeUsed.args[0].value.replace(/^['"]+|['"]+$/g, ""),
};
fixHints.exportCodeToBeUsed.args[0].value = JSON.stringify(newArg);
fixHints.exportCodeToBeUsed.args[0].kind = SyntaxKind.ObjectLiteralExpression;
} else {
fixHints = undefined; // We cannot handle this case
log.verbose(`Autofix skipped for ${apiName}. Transpilation is too ambiguous.`);
}
} else if (apiName === "createComponent") {
if (fixHints?.exportCodeToBeUsed?.args?.[0]?.kind === SyntaxKind.ObjectLiteralExpression) {
const componentOptionsExpression =
extractKeyValuePairs(fixHints.exportCodeToBeUsed.args[0].value);
if (componentOptionsExpression.async !== true) {
fixHints = undefined; // We cannot handle this case
log.verbose(`Autofix skipped for ${apiName}. Transpilation is too ambiguous.`);
}
} else {
fixHints = undefined; // We cannot handle this case
log.verbose(`Autofix skipped for ${apiName}. Transpilation is too ambiguous.`);
}
} else if (apiName === "getLibraryResourceBundle") {
// Handling fallback in the legacy API
if (!fixHints?.exportCodeToBeUsed?.args?.[0] ||
fixHints?.exportCodeToBeUsed?.args?.[0]?.value === "undefined") {
fixHints.exportCodeToBeUsed.args ??= [];
fixHints.exportCodeToBeUsed.args[0] = {
value: "\"sap.ui.core\"", kind: SyntaxKind.StringLiteral, ui5Type: "library",
};
}

fixHints.exportCodeToBeUsed.name = `$moduleIdentifier` +
`.getResourceBundleFor(${cleanRedundantArguments(fixHints.exportCodeToBeUsed.args ?? [])})`;

// If any of the arguments is a boolean with value true, the return value is a promise
// and is not compatible with the new API
if ([fixHints?.exportCodeToBeUsed?.args?.[0]?.kind,
fixHints?.exportCodeToBeUsed?.args?.[1]?.kind,
fixHints?.exportCodeToBeUsed?.args?.[2]?.kind].includes(SyntaxKind.TrueKeyword) ||
fixHints?.exportCodeToBeUsed?.args?.[0]?.ui5Type !== "library") {
fixHints = undefined; // The new API is async
log.verbose(`Autofix skipped for ${apiName}. Transpilation is too ambiguous.`);
}
}

return fixHints;
}

function extractKeyValuePairs(jsonLikeStr: string) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already have the relevant information accessible via AST nodes, so I don't think we should parse the argument ourselves here. The input is based on node.getText(). We even have already code that can analyze object structures, which is used for certain checks in SourceFileLinter.

const regex = /["']?([\w$]+)["']?\s*:\s*(true|false|null|["'][^"']*["']|[+,\-./0-9:;<=>?@A-Z\\[\\\\]^_`a-e\]+)/g;
const pairs = {} as Record<string, unknown>;
let match;
while ((match = regex.exec(jsonLikeStr)) !== null) {
const key = match[1];
const rawValue = match[2];
let value;

// Convert to appropriate JS type
if (/^["'].*["']$/.test(rawValue)) {
value = rawValue.slice(1, -1); // remove quotes
} else if (rawValue === "true") {
value = true;
} else if (rawValue === "false") {
value = false;
} else if (rawValue === "null") {
value = null;
} else if (!isNaN(Number(rawValue))) {
value = parseFloat(rawValue);
} else {
value = rawValue; // fallback
}

pairs[key] = value;
}
return pairs;
}

function cleanRedundantArguments(availableArgs: {value: string}[]) {
const args = [];
for (let i = 1; i <= availableArgs.length; i++) {
Expand Down
4 changes: 3 additions & 1 deletion src/linter/ui5Types/TypeLinter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ export default class TypeLinter {
continue;
}
let manifestContent;
if (sourceFile.fileName.endsWith("/Component.js") || sourceFile.fileName.endsWith("/Component.ts")) {
if (sourceFile.fileName.endsWith("/Component.js") || sourceFile.fileName.endsWith("/Component.ts") ||
// Manifest contains information that is needed for autofixing
applyAutofix) {
const res = await this.#workspace.byPath(path.dirname(sourceFile.fileName) + "/manifest.json");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if the current sourceFile is within a sub-folder of the component project? Then, the manifest.json is accessed at the wrong location and won't be found.

Please add a test-case for that (e.g. within the existing application project)

if (res) {
manifestContent = await res.getString();
Expand Down
262 changes: 262 additions & 0 deletions src/linter/ui5Types/fixHints/CoreFixHintsGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,268 @@ const coreModulesReplacements = new Map<string, FixHints>([
// ["unregisterPlugin", {}],
]);

const coreModulesReplacements = new Map<string, FixHints>([
// https://github.com/SAP/ui5-linter/issues/619
["attachInit", {
moduleName: "sap/ui/core/Core", exportNameToBeUsed: "ready",
}],
["attachInitEvent", {
moduleName: "sap/ui/core/Core", exportNameToBeUsed: "ready",
}],
["getControl", {
moduleName: "sap/ui/core/Element", exportNameToBeUsed: "getElementById",
}],
["getElementById", {
moduleName: "sap/ui/core/Element", exportNameToBeUsed: "getElementById",
}],
["byId", {
moduleName: "sap/ui/core/Element", exportNameToBeUsed: "getElementById",
}],
["getEventBus", {
moduleName: "sap/ui/core/EventBus", exportNameToBeUsed: "getInstance",
}],
["getStaticAreaRef", {
moduleName: "sap/ui/core/StaticArea", exportNameToBeUsed: "getDomRef",
}],
["initLibrary", {
moduleName: "sap/ui/core/Lib", exportNameToBeUsed: "init",
}],
["isMobile", {
moduleName: "sap/ui/Device", exportCodeToBeUsed: "$moduleIdentifier.browser.mobile",
}],
["notifyContentDensityChanged", {
moduleName: "sap/ui/core/Theming", exportNameToBeUsed: "notifyContentDensityChanged",
}],
["byFieldGroupId", {
exportCodeToBeUsed: "sap.ui.core.Control.getControlsByFieldGroupId($1)",
}],
["getCurrentFocusedControlId", {
moduleName: "sap/ui/core/Element", exportNameToBeUsed: "getActiveElement()?.getId",
}],
["isStaticAreaRef", {
moduleName: "sap/ui/core/StaticArea",
exportCodeToBeUsed: "$moduleIdentifier.getDomRef() === $1",
}],
// Migrate only if second argument is omitted or undefined
["applyTheme", {
moduleName: "sap/ui/core/Theming",
exportCodeToBeUsed: "$moduleIdentifier.setTheme($1)",
}],
// Individual arguments must be mapped to "options" object
// The new API has no sync loading option, replacement is only safe when the options contain async:true
["loadLibrary", {
moduleName: "sap/ui/core/Lib", exportCodeToBeUsed: "$moduleIdentifier.load($1)",
}],
// Individual arguments must be mapped to "options" object.
// The old API defaults to sync component creation. It then cannot be safely replaced with Component.create.
// Only when the first argument is an object defining async: true a migration is possible.
["createComponent", {
moduleName: "sap/ui/core/Component", exportCodeToBeUsed: "$moduleIdentifier.create($1)",
}],
// Note that alternative replacement Component.get is meanwhile deprecated, too
["getComponent", {
moduleName: "sap/ui/core/Component", exportNameToBeUsed: "getComponentById",
}],
// Parameter bAsync has to be omitted or set to false since the new API returns
// the resource bundle synchronously. When bAsync is true, the new API is not a replacement
// as it does not return a promise. In an await expression, it would be okay, but otherwise not.
// TODO: To be discussed: sLibrary must be a library, that might not be easy to check
["getLibraryResourceBundle", {
moduleName: "sap/ui/core/Lib", exportCodeToBeUsed: "$moduleIdentifier.getResourceBundleFor($1, $2)",
}],

// TODO: Can't be safely migrated for now. The callback function might have code
// that has to be migrated, too. MagicString will throw an exception.
// The same as jQuery.sap.delayedCall case
//
// // Do not migrate if second argument is provided.
// // We can't generate a ".bind" call since detaching wouldn't be possible anymore
// ["attachIntervalTimer", {
// moduleName: "sap/ui/core/IntervalTrigger",
// exportCodeToBeUsed: "$moduleIdentifier.addListener($1)",
// }],
// // Do not migrate if second argument is provided.
// // We can't generate a ".bind" call since detaching wouldn't be possible anymore
// ["detachIntervalTimer", {
// moduleName: "sap/ui/core/IntervalTrigger",
// exportCodeToBeUsed: "$moduleIdentifier.removeListener($1, $2)",
// }],

// No direct replacement available
// ... but further calls on the result should be fixable. Can we detect and remove remaining calls (dead code)?
// ["getConfiguration", {
// // https://github.com/SAP/ui5-linter/issues/620
// }],

// Migration to sap/ui/core/tmpl/Template.byId(sId) not possible
// Template is deprecated, there is no valid replacement in UI5 2.0
// ["getTemplate", {}],

// Migration to sap/base/i18n/Localization.attachChange(fnFunction) not possible
// The Event object has a different API than on the Core facade. There is no more getParameters().
// Since we can't analyze the callback function with enough certainty, no migration shall be attempted.
// Also no migration is possible if the second argument is provided. We can't generate a ".bind" call
// since detaching wouldn't be possible anymore.
// ["attachLocalizationChanged", {}],

// Migration to sap/ui/core/Theming.attachApplied(fnFunction) not possible
// The Event object has a different API than on the Core facade. There is no more getParameters().
// Since we can't analyze the callback function with enough certainty, no migration shall be attempted.
// Also no migration is possible the second argument is provided. We can't generate a ".bind" call since
// detaching wouldn't be possible anymore.
// ["attachThemeChanged", {}],

// Migration not possible. See attach method.
// ["detachLocalizationChanged", {}],

// Migration not possible
// ["detachThemeChanged", {}],

// Migration not possible
// ["applyChanges", {}],

// Migration not possible
// ["attachControlEvent", {}],

// Migration not possible
// ["attachFormatError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["attachParseError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["attachValidationError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["attachValidationSuccess", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// ["createRenderManager", {}],

// Migration not possible
// Unclear which control to use
// ["createUIArea", {}],

// Migration not possible
// ["detachControlEvent", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["detachFormatError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["detachParseError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["detachValidationError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["detachValidationSuccess", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["fireFormatError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["fireParseError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["fireValidationError", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["fireValidationSuccess", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// ["getApplication", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// There is a public replacement for the most common use case that checks the
// result for a single library (Library.isLoaded(name))
// ["getLoadedLibraries", {}],

// Migration not possible
// Different return types -> Manual migration necessary
// ["getMessageManager", {}],

// Migration not possible
// ["getModel", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// ["getRenderManager", {}],

// Migration not possible
// ["getRootComponent", {}],

// Migration not possible
// We can't determine whether the static UIArea is requested
// ["getUIArea", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// ["getUIDirty", {}],

// Migration not possible
// Recommended replacement only available on ManagedObject
// ["hasModel", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// ["includeLibraryTheme", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// ["isInitialized", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// ["isLocked", {}],

// Migration not possible
// Developers should migrate to the theme-applied event
// ["isThemeApplied", {}],

// Migration not possible
// API has been removed, migration likely involves more than removing the usage
// ["lock", {}],

// Migration not possible
// ["registerPlugin", {}],

// Migration not possible
// ["sap.ui.core.Core.extend", {}],

// Migration not possible
// ["sap.ui.core.Core.getMetadata", {}],

// Migration not possible
// ["setModel", {}],

// Migration not possible
// ["setRoot", {}],

// Migration not possible
// ["setThemeRoot", {}],

// Migration not possible
// ["unlock", {}],

// Migration not possible
// ["unregisterPlugin", {}],
]);

export default class CoreFixHintsGenerator {
constructor(
private ambientModuleCache: AmbientModuleCache,
Expand Down
Loading