diff --git a/src/linter/ui5Types/fix/AccessExpressionFix.ts b/src/linter/ui5Types/fix/AccessExpressionFix.ts index f43969f9e..cf555bb4b 100644 --- a/src/linter/ui5Types/fix/AccessExpressionFix.ts +++ b/src/linter/ui5Types/fix/AccessExpressionFix.ts @@ -16,22 +16,13 @@ export default class AccessExpressionFix extends AccessExpressionBaseFix { if (this.startPos === undefined || this.endPos === undefined) { throw new Error("Start and end position are not defined"); } - if (this.params.moduleName && !this.moduleIdentifierName) { - // The identifier for the requested module has not been set - // This can happen for example if the position of the autofix is not inside - // a module definition or require block. Therefore the required dependency can not be added - // and the fix can not be applied. - return; - } - if (this.params.globalName && !this.globalIdentifierName) { - // This should not happen - throw new Error("Global identifier has not been provided"); - } - let value = this.globalIdentifierName ?? this.moduleIdentifierName; - if (!value) { + + const identifier = this.getIdentifiersForSingleRequest(this.params.moduleName, this.params.globalName); + if (!identifier) { return; } + let value = identifier; if (this.params.propertyAccess) { // If a property is defined, we need to access it on the identifier value = `${value}.${this.params.propertyAccess}`; diff --git a/src/linter/ui5Types/fix/AccessExpressionGeneratorFix.ts b/src/linter/ui5Types/fix/AccessExpressionGeneratorFix.ts index 3a95298c8..3a3006b43 100644 --- a/src/linter/ui5Types/fix/AccessExpressionGeneratorFix.ts +++ b/src/linter/ui5Types/fix/AccessExpressionGeneratorFix.ts @@ -2,6 +2,26 @@ import {ChangeAction} from "../../../autofix/autofix.js"; import AccessExpressionBaseFix, {AccessExpressionBaseFixParams} from "./AccessExpressionBaseFix.js"; export interface AccessExpressionGeneratorFixParams extends AccessExpressionBaseFixParams { + /** + * Modules to import. If this parameter is specified, the standalone parameters "moduleName" and + * "preferredIdentifier" must not be provided. + */ + moduleImports?: { + moduleName: string; + preferredIdentifier?: string; + }[]; + + /** + * Names of a global variable to use in the fix (e.g. "document"). If this parameter is specified, parameter + * "globalName" must not be provided. + * + * The fix will be provided with the identifier names or property access strings to use via + * the setIdentifierForGlobal method. + * + * For example, if there is already a conflicting identifier within the same file, + * the fix will be provided with an alternative like "globalThis.document" + */ + globalNames?: string[]; /** * The generator function will be used to determine the value of the replacement, affecting @@ -9,7 +29,7 @@ export interface AccessExpressionGeneratorFixParams extends AccessExpressionBase * * If the return value is undefined, no change will be generated */ - generator: (identifierName: string | undefined) => string | undefined; + generator: (identifierNames: string[]) => string | undefined; } /** @@ -21,24 +41,71 @@ export default class AccessExpressionGeneratorFix extends AccessExpressionBaseFi super(params); } + getNewModuleDependencies() { + if (this.params.moduleName && this.params.moduleImports) { + throw new Error( + "Parameters 'moduleName' and 'moduleImports' are both defined. Only one may be used at a time."); + } else if (this.params.moduleName) { + return super.getNewModuleDependencies(); + } else if (!this.params.moduleImports) { + return; + } + + const usagePosition = this.startPos; + if (usagePosition === undefined) { + throw new Error("Start position is not defined"); + } + return this.params.moduleImports.map((moduleImport) => { + return { + ...moduleImport, + usagePosition, + }; + }); + } + + getNewGlobalAccess() { + if (this.params.globalName && this.params.globalNames) { + throw new Error( + "Parameters 'globalName' and 'globalNames' are both defined. Only one may be used at a time."); + } else if (this.params.globalName) { + return super.getNewGlobalAccess(); + } else if (!this.params.globalNames) { + return; + } + const usagePosition = this.startPos; + if (usagePosition === undefined) { + throw new Error("Start position is not defined"); + } + + return this.params.globalNames.map((globalName) => { + return { + globalName, + usagePosition, + }; + }); + } + generateChanges() { if (this.startPos === undefined || this.endPos === undefined) { throw new Error("Start and end position are not defined"); } - if (this.params.moduleName && !this.moduleIdentifierName) { - // The identifier for the requested module has not been set - // This can happen for example if the position of the autofix is not inside - // a module definition or require block. Therefore the required dependency can not be added - // and the fix can not be applied. - return; + + let moduleNames: string[] | undefined; + if (this.params.moduleName) { + moduleNames = [this.params.moduleName]; + } else if (this.params.moduleImports) { + moduleNames = this.params.moduleImports.map((moduleImport) => moduleImport.moduleName); } - if (this.params.globalName && !this.globalIdentifierName) { - // This should not happen - throw new Error("Global identifier has not been provided"); + + const globalNames = this.params.globalName ? [this.params.globalName] : this.params.globalNames; + + const identifiers = this.getIdentifiersForMultipleRequests(moduleNames, globalNames); + if (!identifiers) { + return; } // If a generator function is provided, use it to generate the change - const value = this.params.generator(this.globalIdentifierName ?? this.moduleIdentifierName); + const value = this.params.generator(identifiers); if (!value) { return; } diff --git a/src/linter/ui5Types/fix/BaseFix.ts b/src/linter/ui5Types/fix/BaseFix.ts index ee0354d5b..7f604295c 100644 --- a/src/linter/ui5Types/fix/BaseFix.ts +++ b/src/linter/ui5Types/fix/BaseFix.ts @@ -1,26 +1,24 @@ import ts from "typescript"; import {PositionInfo} from "../../LinterContext.js"; -import Fix from "./Fix.js"; +import Fix, {GlobalAccessRequest, ModuleDependencyRequest} from "./Fix.js"; export interface BaseFixParams { /** * Name of the module to import for use in the fix */ moduleName?: string; + /** * The preferred identifier to use for the module import. * If not provided, the identifier will be derived from the module name */ preferredIdentifier?: string; - // Not yet implemented: Requesting multiple modules - // modules?: Omit[]; - /** * Name of a global variable to use in the fix (e.g. "document") * * The fix will be provided with the identifier name or property access to use via - * the setIdentifierForDependency method. + * the setIdentifierForGlobal method. * * For example, if there is already a conflicting identifier within the same file, * the fix will be provided with an alternative like "globalThis.document" @@ -73,8 +71,8 @@ export enum FixScope { export default abstract class BaseFix extends Fix { protected startPos: number | undefined; protected endPos: number | undefined; - protected moduleIdentifierName: string | undefined; - protected globalIdentifierName: string | undefined; + protected moduleIdentifierNames: Map | undefined; + protected globalIdentifierNames: Map | undefined; protected sourcePosition: PositionInfo | undefined; protected nodeTypes: ts.SyntaxKind[] = []; @@ -105,15 +103,79 @@ export default abstract class BaseFix extends Fix { }; } - setIdentifierForDependency(identifier: string) { - this.moduleIdentifierName = identifier; + setIdentifierForDependency(identifier: string, moduleName: string) { + this.moduleIdentifierNames ??= new Map(); + this.moduleIdentifierNames.set(moduleName, identifier); + } + + setIdentifierForGlobal(identifier: string, globalName: string) { + this.globalIdentifierNames ??= new Map(); + this.globalIdentifierNames.set(globalName, identifier); + } + + protected getIdentifiersForSingleRequest( + moduleName: string | undefined, globalName: string | undefined + ): string | undefined { + if (moduleName) { + if (!this.moduleIdentifierNames?.has(moduleName)) { + // The identifier for the requested module has not been set + // This can happen for example if the position of the autofix is not inside + // a module definition or require block. Therefore the required dependency can not be added + // and the fix can not be applied. + return; + } + return this.moduleIdentifierNames.get(moduleName)!; + } else if (globalName) { + if (!this.globalIdentifierNames?.has(globalName)) { + // This should not happen, globals can always be provided + throw new Error("Global identifier has not been provided"); + } + return this.globalIdentifierNames.get(globalName)!; + } } - setIdentifierForGlobal(identifier: string) { - this.globalIdentifierName = identifier; + /** + * Helper method for fix classes that feature multiple imports/globals. + * + * Returns undefined if any of the requested identifiers could not be set, indicating that the + * fix must not be applied + */ + protected getIdentifiersForMultipleRequests( + moduleNames: string[] | undefined, globalNames: string[] | undefined + ): string[] | undefined { + const identifiers = []; // Modules first, then globals. Both in the order they have been requested in + if (moduleNames?.length) { + if (!this.moduleIdentifierNames) { + // No modules have been set. Change can not be applied + return; + } + for (const moduleName of moduleNames) { + if (!this.moduleIdentifierNames.has(moduleName)) { + // The identifier for the requested module has not been set + // Change can not be applied + return; + } + identifiers.push(this.moduleIdentifierNames.get(moduleName)!); + } + } + + if (globalNames?.length) { + if (!this.globalIdentifierNames) { + // This should not happen, globals can always be provided + throw new Error("Global identifier has not been provided"); + } + for (const globalName of globalNames) { + if (!this.globalIdentifierNames.has(globalName)) { + // This should not happen, globals can always be provided + throw new Error("Global identifier has not been provided"); + } + identifiers.push(this.globalIdentifierNames.get(globalName)!); + } + } + return identifiers; } - getNewModuleDependencies() { + getNewModuleDependencies(): ModuleDependencyRequest | ModuleDependencyRequest[] | undefined { if (this.startPos === undefined) { throw new Error("Start position is not defined"); } @@ -127,7 +189,7 @@ export default abstract class BaseFix extends Fix { }; } - getNewGlobalAccess() { + getNewGlobalAccess(): GlobalAccessRequest | GlobalAccessRequest[] | undefined { if (this.startPos === undefined) { throw new Error("Start position is not defined"); } diff --git a/src/linter/ui5Types/fix/CallExpressionFix.ts b/src/linter/ui5Types/fix/CallExpressionFix.ts index 0cc3c3832..576092bb5 100644 --- a/src/linter/ui5Types/fix/CallExpressionFix.ts +++ b/src/linter/ui5Types/fix/CallExpressionFix.ts @@ -57,23 +57,13 @@ export default class CallExpressionFix extends CallExpressionBaseFix { if (this.startPos === undefined || this.endPos === undefined) { throw new Error("Start or end position is not defined"); } - if (this.params.moduleName && !this.moduleIdentifierName) { - // The identifier for the requested module has not been set - // This can happen for example if the position of the autofix is not inside - // a module definition or require block. Therefore the required dependency can not be added - // and the fix can not be applied. - return; - } - if (this.params.globalName && !this.globalIdentifierName) { - // This should not happen - throw new Error("Global identifier has not been provided"); - } - let value = this.globalIdentifierName ?? this.moduleIdentifierName; - if (!value) { + const identifier = this.getIdentifiersForSingleRequest(this.params.moduleName, this.params.globalName); + if (!identifier) { return; } + let value = identifier; if (this.params.propertyAccess) { // If a property is defined, we need to access it on the identifier value = `${value}.${this.params.propertyAccess}`; diff --git a/src/linter/ui5Types/fix/CallExpressionGeneratorFix.ts b/src/linter/ui5Types/fix/CallExpressionGeneratorFix.ts index f8bf66456..db98beb3a 100644 --- a/src/linter/ui5Types/fix/CallExpressionGeneratorFix.ts +++ b/src/linter/ui5Types/fix/CallExpressionGeneratorFix.ts @@ -10,14 +10,42 @@ export interface CallExpressionGeneratorFixParams string | undefined; /** @@ -67,24 +95,71 @@ export default class CallExpressionGeneratorFix return true; } + getNewModuleDependencies() { + if (this.params.moduleName && this.params.moduleImports) { + throw new Error( + "Parameters 'moduleName' and 'moduleImports' are both defined. Only one may be used at a time."); + } else if (this.params.moduleName) { + return super.getNewModuleDependencies(); + } else if (!this.params.moduleImports) { + return; + } + + const usagePosition = this.startPos; + if (usagePosition === undefined) { + throw new Error("Start position is not defined"); + } + return this.params.moduleImports.map((moduleImport) => { + return { + ...moduleImport, + usagePosition, + }; + }); + } + + getNewGlobalAccess() { + if (this.params.globalName && this.params.globalNames) { + throw new Error( + "Parameters 'globalName' and 'globalNames' are both defined. Only one may be used at a time."); + } else if (this.params.globalName) { + return super.getNewGlobalAccess(); + } else if (!this.params.globalNames) { + return; + } + const usagePosition = this.startPos; + if (usagePosition === undefined) { + throw new Error("Start position is not defined"); + } + + return this.params.globalNames.map((globalName) => { + return { + globalName, + usagePosition, + }; + }); + } + generateChanges() { if (this.startPos === undefined || this.endPos === undefined || !this.generatorArgs) { throw new Error("Start, end position or arguments are not defined"); } - if (this.params.moduleName && !this.moduleIdentifierName) { - // The identifier for the requested module has not been set - // This can happen for example if the position of the autofix is not inside - // a module definition or require block. Therefore the required dependency can not be added - // and the fix can not be applied. - return; + + let moduleNames: string[] | undefined; + if (this.params.moduleName) { + moduleNames = [this.params.moduleName]; + } else if (this.params.moduleImports) { + moduleNames = this.params.moduleImports.map((moduleImport) => moduleImport.moduleName); } - if (this.params.globalName && !this.globalIdentifierName) { - // This should not happen - throw new Error("Global identifier has not been provided"); + + const globalNames = this.params.globalName ? [this.params.globalName] : this.params.globalNames; + + const identifiers = this.getIdentifiersForMultipleRequests(moduleNames, globalNames); + if (!identifiers) { + return; } const value = this.params.generator( - this.generatorContext, this.globalIdentifierName ?? this.moduleIdentifierName, ...this.generatorArgs); + this.generatorContext, identifiers, ...this.generatorArgs); if (!value) { return; } diff --git a/src/linter/ui5Types/fix/collections/jqueryFixes.ts b/src/linter/ui5Types/fix/collections/jqueryFixes.ts index 0de3e5985..68eacc134 100644 --- a/src/linter/ui5Types/fix/collections/jqueryFixes.ts +++ b/src/linter/ui5Types/fix/collections/jqueryFixes.ts @@ -21,14 +21,17 @@ export default t; t.declareModule("jQuery", [ t.namespace("sap", [ + // jQuery.sap.assert => sap.base.assert t.namespace("assert", accessExpressionFix({ // https://github.com/SAP/ui5-linter/issues/520 moduleName: "sap/base/assert", })), t.namespace("log", [ // https://github.com/SAP/ui5-linter/issues/522 + // jQuery.sap.log.Level|LogLevel => Log.Level ...t.namespaces(["Level", "LogLevel"], accessExpressionFix({ moduleName: "sap/base/Log", propertyAccess: "Level", })), + // jQuery.sap.log.debug|warn|info|error|fatal|trace() => Log.debug|warn|info|error|fatal|trace() ...t.namespaces(["debug", "error", "fatal", "info", "trace", "warning"], callExpressionFix({ moduleName: "sap/base/Log", @@ -38,10 +41,12 @@ t.declareModule("jQuery", [ // is only safe if the return value is not used in the code mustNotUseReturnValue: true, })), + // jQuery.sap.log.getLevel => Log.getLevel t.namespace("getLevel", accessExpressionFix({ moduleName: "sap/base/Log", scope: FixScope.FirstChild, })), + // jQuery.sap.log.getLog => Log.getLogEntries t.namespace("getLog", accessExpressionFix({ moduleName: "sap/base/Log", propertyAccess: "getLogEntries", @@ -71,6 +76,7 @@ t.declareModule("jQuery", [ scope: FixScope.FirstChild, })), ]), + // jQuery.sap.resources => ResourceBundle.create t.namespace("resources", accessExpressionFix({ // https://github.com/SAP/ui5-linter/issues/521 moduleName: "sap/base/i18n/ResourceBundle", propertyAccess: "create", @@ -130,7 +136,29 @@ t.declareModule("jQuery", [ t.namespace("hyphen", accessExpressionFix({ moduleName: "sap/base/strings/hyphenate", })), - t.namespace("isStringNFC", () => new IsStringNfcFix()), + // jQuery.sap.isStringNFC("foo") => "foo".normalize("NFC") === "foo" + // Note: This API only exists in old UI5 releases + t.namespace("isStringNFC", callExpressionGeneratorFix({ + validateArguments(ctx, checker, ...args) { + if (!args.length) { + return false; + } + // Ensure that the first argument is a string literal + const firstArg = args[0]; + if (ts.isStringLiteralLike(firstArg)) { + return true; + } else if (ts.isIdentifier(firstArg)) { + const argType = checker.getTypeAtLocation(firstArg); + if (argType.isStringLiteral()) { + return true; + } + } + return false; + }, + generator(ctx, identifierNames, ...args) { + return `${args[0]}.normalize("NFC") === ${args[0]}`; + }, + })), t.namespace("arraySymbolDiff", accessExpressionFix({ // https://github.com/SAP/ui5-linter/issues/528 moduleName: "sap/base/util/array/diff", })), @@ -174,7 +202,7 @@ t.declareModule("jQuery", [ } return true; }, - generator(ctx, moduleIdentifierName, _, arg2, arg3) { + generator(ctx, [moduleIdentifierName], _, arg2, arg3) { return `${moduleIdentifierName}(${arg2.trim()},${arg3})`; }, })), @@ -207,7 +235,7 @@ t.declareModule("jQuery", [ } return true; }, - generator(ctx, moduleIdentifier, ...args) { + generator(ctx, [moduleIdentifier], ...args) { if (ctx.shortCircuit) { args[0] = `(${args[0]} || "")`; // Short-circuit the first argument to avoid undefined/null } @@ -283,7 +311,7 @@ t.declareModule("jQuery", [ t.namespace("measure", [ // https://github.com/SAP/ui5-linter/issues/555 t.namespace("getRequestTimings", callExpressionGeneratorFix({ globalName: "performance", - generator(ctx, moduleIdentifierName) { + generator(ctx, [moduleIdentifierName]) { return `${moduleIdentifierName}.getEntriesByType("resource")`; }, })), @@ -600,7 +628,7 @@ t.declareModule("jQuery", [ } return true; }, - generator: (ctx, moduleIdentifier, arg1, arg2) => { + generator: (ctx, [moduleIdentifier], arg1, arg2) => { if (ctx.replaceWithNull) { return "null"; } @@ -660,7 +688,7 @@ t.declareModule("jQuery", [ } return true; }, - generator: (ctx, moduleIdentifier, arg1, arg2) => { + generator: (ctx, [moduleIdentifier], arg1, arg2) => { if (ctx.argValue) { // Use the modified value arg1 = ctx.argValue; @@ -677,7 +705,7 @@ t.declareModule("jQuery", [ })), t.namespace("getResourcePath", callExpressionGeneratorFix({ globalName: "sap.ui.require", - generator: (ctx, moduleIdentifier, arg1, arg2) => { + generator: (ctx, [moduleIdentifier], arg1, arg2) => { let res = `${moduleIdentifier}.toUrl(${arg1})`; if (arg2) { res = `${res} +${arg2}`; @@ -730,28 +758,28 @@ t.declareModule("jQuery", [ })), t.namespace("android_phone", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => ( + generator: ([moduleIdentifier]) => ( `${moduleIdentifier}.os.android && ` + `${moduleIdentifier}.system.phone` ), })), t.namespace("android_tablet", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => ( + generator: ([moduleIdentifier]) => ( `${moduleIdentifier}.os.android && ` + `${moduleIdentifier}.system.tablet` ), })), t.namespace("iphone", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => ( + generator: ([moduleIdentifier]) => ( `${moduleIdentifier}.os.ios && ` + `${moduleIdentifier}.system.phone` ), })), t.namespace("ipad", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => ( + generator: ([moduleIdentifier]) => ( `${moduleIdentifier}.os.ios && ` + `${moduleIdentifier}.system.tablet` ), @@ -773,31 +801,31 @@ t.declareModule("jQuery", [ })), t.namespace("Android", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => `${moduleIdentifier}.os.name === "Android"`, + generator: ([moduleIdentifier]) => `${moduleIdentifier}.os.name === "Android"`, })), t.namespace("bb", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => `${moduleIdentifier}.os.name === "bb"`, + generator: ([moduleIdentifier]) => `${moduleIdentifier}.os.name === "bb"`, })), t.namespace("iOS", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => `${moduleIdentifier}.os.name === "iOS"`, + generator: ([moduleIdentifier]) => `${moduleIdentifier}.os.name === "iOS"`, })), t.namespace("winphone", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => `${moduleIdentifier}.os.name === "winphone"`, + generator: ([moduleIdentifier]) => `${moduleIdentifier}.os.name === "winphone"`, })), t.namespace("win", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => `${moduleIdentifier}.os.name === "win"`, + generator: ([moduleIdentifier]) => `${moduleIdentifier}.os.name === "win"`, })), t.namespace("linux", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => `${moduleIdentifier}.os.name === "linux"`, + generator: ([moduleIdentifier]) => `${moduleIdentifier}.os.name === "linux"`, })), t.namespace("mac", accessExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (moduleIdentifier) => `${moduleIdentifier}.os.name === "mac"`, + generator: ([moduleIdentifier]) => `${moduleIdentifier}.os.name === "mac"`, })), ]), ]); @@ -880,56 +908,6 @@ function validatePadLeftRightArguments( return true; }; -/** - * Fix for jQuery.sap.isStringNFC - * Source: jQuery.sap.isStringNFC("foo") - * Target: "foo".normalize("NFC") === "foo" - */ -class IsStringNfcFix extends CallExpressionFix { - private arg?: string; - - constructor() { - super({ - scope: FixScope.FullExpression, - }); - } - - // visitLinterNode(node: ts.Node, sourcePosition: PositionInfo, checker: ts.TypeChecker) { - // // TODO: Check whether argument is identifier or string literal - // // In case of identifier, we need to check whether it's type is a string literal - // } - - visitAutofixNode(node: ts.Node, position: number, sourceFile: ts.SourceFile) { - if (!super.visitAutofixNode(node, position, sourceFile)) { - return false; - } - if (!ts.isCallExpression(node) || !node.arguments.length) { - return false; - } - - if (!ts.isStringLiteralLike(node.arguments[0])) { - return false; - } - - this.arg = node.arguments[0].text; - return true; - } - - generateChanges() { - if (this.startPos === undefined || this.endPos === undefined || this.arg === undefined) { - throw new Error("Start or end position or argument is not defined"); - } - - const value = `"${this.arg}".normalize("NFC") === "${this.arg}"`; - return { - action: ChangeAction.REPLACE, - start: this.startPos, - end: this.endPos, - value, - }; - } -} - /** * Fix for jQuery.sap.charToUpperCase * Source: jQuery.sap.charToUpperCase("foo") @@ -1036,13 +1014,14 @@ class CharToUpperCaseFix extends CallExpressionFix { throw new Error("Start or end position or argument is not defined"); } - if (!this.moduleIdentifierName || (this.argIdentifierName === undefined && this.argStringValue === undefined)) { + if (!this.moduleIdentifierNames?.has("sap/base/strings/capitalize") || + (this.argIdentifierName === undefined && this.argStringValue === undefined)) { // Identifier has not been set. This can happen if the relevant position is not inside a // module definition or require block. Therefore the fix can not be applied. return; } const arg = this.argStringValue ?? this.argIdentifierName; - const value = `${this.moduleIdentifierName}(${arg})`; + const value = `${this.moduleIdentifierNames.get("sap/base/strings/capitalize")!}(${arg})`; return { action: ChangeAction.REPLACE, start: this.startPos, diff --git a/src/linter/ui5Types/fix/collections/sapUiCoreFixes.ts b/src/linter/ui5Types/fix/collections/sapUiCoreFixes.ts index f31c615be..ee3cfb1fa 100644 --- a/src/linter/ui5Types/fix/collections/sapUiCoreFixes.ts +++ b/src/linter/ui5Types/fix/collections/sapUiCoreFixes.ts @@ -18,10 +18,22 @@ t.declareModule("sap/ui/core/Configuration", [ moduleName: "sap/base/i18n/Localization", mustNotUseReturnValue: true, })), + t.method("getLocale", callExpressionGeneratorFix({ + moduleImports: [{ + moduleName: "sap/ui/core/Locale", + }, { + moduleName: "sap/base/i18n/Localization", + }], + generator(ctx, identifierNames) { + return `new ${identifierNames[0]}(${identifierNames[1]}.getLanguageTag())`; + }, + })), ]), ]); + t.declareModule("sap/ui/core/Core", [ t.class("Core", [ + // Core.attachInit|attachInitEvent => Core.ready ...t.methods(["attachInit", "attachInitEvent"], accessExpressionFix({ scope: FixScope.FullExpression, moduleName: "sap/ui/core/Core", @@ -32,51 +44,58 @@ t.declareModule("sap/ui/core/Core", [ moduleName: "sap/ui/core/Element", propertyAccess: "getElementById", })), + // Core.getEventBus => EventBus.getInstance t.method("getEventBus", accessExpressionFix({ scope: FixScope.FullExpression, moduleName: "sap/ui/core/EventBus", propertyAccess: "getInstance", })), + // Core.getConfiguration() => Configuration t.method("getConfiguration", callExpressionFix({ scope: FixScope.FullExpression, moduleName: "sap/ui/core/Configuration", })), + // Core.getStaticAreaRef => StaticArea.getDomRef t.method("getStaticAreaRef", accessExpressionFix({ scope: FixScope.FullExpression, moduleName: "sap/ui/core/StaticArea", propertyAccess: "getDomRef", })), + // Core.initLibrary => Lib.init t.method("initLibrary", accessExpressionFix({ scope: FixScope.FullExpression, moduleName: "sap/ui/core/Lib", propertyAccess: "init", })), + // Core.isMobile() => Device.browser.mobile t.method("isMobile", callExpressionGeneratorFix({ moduleName: "sap/ui/Device", - generator: (ctx, moduleIdentifier) => { + generator: (ctx, [moduleIdentifier]) => { return `${moduleIdentifier}.browser.mobile`; }, })), + // Core.notifyContentDensityChanged => Theming.notifyContentDensityChanged t.method("notifyContentDensityChanged", accessExpressionFix({ scope: FixScope.FirstChild, moduleName: "sap/ui/core/Theming", })), + // Core.getCurrentFocusedControlId => Element.getActiveElement()?.getId() || null t.method("getCurrentFocusedControlId", callExpressionGeneratorFix({ moduleName: "sap/ui/core/Element", // The legacy API used to return null if no control was focused. - generator: (ctx, moduleIdentifier) => { + generator: (ctx, [moduleIdentifier]) => { return `${moduleIdentifier}.getActiveElement()?.getId() || null`; }, })), t.method("byFieldGroupId", callExpressionGeneratorFix({ moduleName: "sap/ui/core/Control", - generator: (ctx, moduleIdentifier, arg1) => { + generator: (ctx, [moduleIdentifier], arg1) => { return `${moduleIdentifier}.getControlsByFieldGroupId(${arg1})`; }, })), t.method("isStaticAreaRef", callExpressionGeneratorFix({ moduleName: "sap/ui/core/StaticArea", - generator: (ctx, moduleIdentifier, arg1) => { + generator: (ctx, [moduleIdentifier], arg1) => { return `${moduleIdentifier}.getDomRef() === ${arg1}`; }, })), @@ -89,7 +108,7 @@ t.declareModule("sap/ui/core/Core", [ } return false; }, - generator: (ctx, moduleIdentifier, arg1) => { + generator: (ctx, [moduleIdentifier], arg1) => { return `${moduleIdentifier}.setTheme(${arg1})`; }, })), diff --git a/test/fixtures/autofix/Configuration.js b/test/fixtures/autofix/Configuration.js index 0b4b23544..74ecc9bf1 100644 --- a/test/fixtures/autofix/Configuration.js +++ b/test/fixtures/autofix/Configuration.js @@ -5,4 +5,6 @@ sap.ui.define(["sap/ui/core/Core", "sap/ui/core/Configuration"], function(Core, // setRTL is not chainable and setLanguage can't be applied because // its expression contains a non-side-effect-free call expression Configuration.setRTL(false).setLanguage("en"); + + Configuration.getLocale(); }); \ No newline at end of file diff --git a/test/fixtures/autofix/jQuerySap.js b/test/fixtures/autofix/jQuerySap.js index 2aaf9c2fd..e952f0ea7 100644 --- a/test/fixtures/autofix/jQuerySap.js +++ b/test/fixtures/autofix/jQuerySap.js @@ -276,4 +276,9 @@ sap.ui.define(["sap/base/strings/NormalizePolyfill"], async function (NormalizeP var parseError = jQuery.sap.getParseError(jQuery.sap.parseXML(errorXML)); var validXML = ""; var xmlDocument = jQuery.sap.serializeXML(jQuery.sap.parseXML(validXML)); + + const str = "test NFC string"; + jQuery.sap.isStringNFC(`test NFC string`); + jQuery.sap.isStringNFC(str); + jQuery.sap.isStringNFC(unknownVar); }); diff --git a/test/lib/autofix/snapshots/autofix.fixtures.ts.md b/test/lib/autofix/snapshots/autofix.fixtures.ts.md index b8c9337f4..f7ef4d8b0 100644 --- a/test/lib/autofix/snapshots/autofix.fixtures.ts.md +++ b/test/lib/autofix/snapshots/autofix.fixtures.ts.md @@ -8,13 +8,15 @@ Generated by [AVA](https://avajs.dev). > AutofixResult: /Configuration.js - `sap.ui.define(["sap/ui/core/Core", "sap/ui/core/Configuration"], function(Core, Configuration) {␊ + `sap.ui.define(["sap/ui/core/Core", "sap/ui/core/Configuration", "sap/base/i18n/Localization", "sap/ui/core/Locale"], function(Core, Configuration, Localization, Locale) {␊ Configuration.setRTL(false).setLanguage("en");␊ ␊ // Currently not fixable:␊ // setRTL is not chainable and setLanguage can't be applied because␊ // its expression contains a non-side-effect-free call expression␊ Configuration.setRTL(false).setLanguage("en");␊ + ␊ + new Locale(Localization.getLanguageTag());␊ });` > LintResult: Configuration.js @@ -4177,6 +4179,11 @@ Generated by [AVA](https://avajs.dev). var parseError = XMLHelper.getParseError(XMLHelper.parse(errorXML));␊ var validXML = "";␊ var xmlDocument = XMLHelper.serialize(XMLHelper.parse(validXML));␊ + ␊ + const str = "test NFC string";␊ + \`test NFC string\`.normalize("NFC") === \`test NFC string\`;␊ + str.normalize("NFC") === str;␊ + jQuery.sap.isStringNFC(unknownVar);␊ });␊ ` @@ -4695,8 +4702,14 @@ Generated by [AVA](https://avajs.dev). line: 265, message: 'Unable to analyze this method call because the type of identifier "isActive" in "jQuery.sap.act.isActive()"" could not be determined', }, + { + category: 1, + column: 2, + line: 293, + message: 'Unable to analyze this method call because the type of identifier "isStringNFC" in "jQuery.sap.isStringNFC(unknownVar)"" could not be determined', + }, ], - errorCount: 52, + errorCount: 53, fatalErrorCount: 0, filePath: 'jQuerySap.js', messages: [ @@ -5116,6 +5129,14 @@ Generated by [AVA](https://avajs.dev). ruleId: 'no-deprecated-api', severity: 2, }, + { + column: 2, + line: 293, + message: 'Use of deprecated API \'jQuery.sap.isStringNFC\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, ], warningCount: 0, }, diff --git a/test/lib/autofix/snapshots/autofix.fixtures.ts.snap b/test/lib/autofix/snapshots/autofix.fixtures.ts.snap index 9e9f06478..b07e489ca 100644 Binary files a/test/lib/autofix/snapshots/autofix.fixtures.ts.snap and b/test/lib/autofix/snapshots/autofix.fixtures.ts.snap differ