From 46a3adba05498642ea99137006c6bf5129c16c2c Mon Sep 17 00:00:00 2001 From: Matthias Osswald Date: Tue, 26 Nov 2024 09:45:11 +0100 Subject: [PATCH] feat: Detect override of control "rerender" JIRA: CPOUI5FOUNDATION-939 --- src/linter/messages.ts | 11 + src/linter/ui5Types/SourceFileLinter.ts | 12 +- src/linter/ui5Types/utils.ts | 32 +++ .../sap.ui.core/src/sap/ui/core/Control.js | 3 + .../sap.ui.core/src/sap/ui/core/Element.js | 3 + .../rules/renderer/ControlRerenderOverride.js | 122 +++++++++ .../ControlRerenderOverrideTypeScript.ts | 125 +++++++++ .../lib/linter/rules/snapshots/renderer.ts.md | 257 ++++++++++++++++++ .../linter/rules/snapshots/renderer.ts.snap | Bin 4273 -> 5863 bytes 9 files changed, 564 insertions(+), 1 deletion(-) create mode 100644 test/fixtures/linter/rules/renderer/ControlRerenderOverride.js create mode 100644 test/fixtures/linter/rules/renderer/ControlRerenderOverrideTypeScript.ts diff --git a/src/linter/messages.ts b/src/linter/messages.ts index 95657621b..8049e6e6f 100644 --- a/src/linter/messages.ts +++ b/src/linter/messages.ts @@ -47,6 +47,7 @@ export enum MESSAGE { HTML_IN_XML, LIB_INIT_API_VERSION, MISSING_BOOTSTRAP_PARAM, + NO_CONTROL_RERENDER_OVERRIDE, NO_DEPRECATED_RENDERER, NO_DIRECT_DATATYPE_ACCESS, NO_DIRECT_ENUM_ACCESS, @@ -316,6 +317,16 @@ export const MESSAGE_INFO = { details: () => `{@link sap.ui.core.Lib.init Lib.init}`, }, + [MESSAGE.NO_CONTROL_RERENDER_OVERRIDE]: { + severity: LintMessageSeverity.Error, + ruleId: RULES["no-deprecated-api"], + message: ({className}: {className: string}) => + `Override of deprecated method 'rerender' in control '${className}'`, + details: () => `Starting from UI5 1.121 the framework no longer calls 'rerender', ` + + `so there is no point in overriding it. ` + + `Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.`, + }, + [MESSAGE.NO_DEPRECATED_RENDERER]: { severity: LintMessageSeverity.Error, ruleId: RULES["no-deprecated-api"], diff --git a/src/linter/ui5Types/SourceFileLinter.ts b/src/linter/ui5Types/SourceFileLinter.ts index 26c45b725..e86c61420 100644 --- a/src/linter/ui5Types/SourceFileLinter.ts +++ b/src/linter/ui5Types/SourceFileLinter.ts @@ -6,7 +6,7 @@ import LinterContext, {ResourcePath, CoverageCategory} from "../LinterContext.js import {MESSAGE} from "../messages.js"; import analyzeComponentJson from "./asyncComponentFlags.js"; import {deprecatedLibraries, deprecatedThemes} from "../../utils/deprecations.js"; -import {getPropertyName, getSymbolForPropertyInConstructSignatures} from "./utils.js"; +import {findClassInstanceMethod, getPropertyName, getSymbolForPropertyInConstructSignatures} from "./utils.js"; import {taskStart} from "../../utils/perf.js"; import {getPositionsForNode} from "../../utils/nodePosition.js"; import {TraceMap} from "@jridgewell/trace-mapping"; @@ -153,6 +153,7 @@ export default class SourceFileLinter { ts.forEachChild(node, visitMetadataNodes); } else if (this.isUi5ClassDeclaration(node, "sap/ui/core/Control")) { this.analyzeControlRendererDeclaration(node); + this.analyzeControlRerenderMethod(node); } else if (ts.isPropertyAssignment(node) && removeQuotes(node.name.getText()) === "theme") { this.analyzeTestsuiteThemeProperty(node); } @@ -539,6 +540,15 @@ export default class SourceFileLinter { } } + analyzeControlRerenderMethod(node: ts.ClassDeclaration) { + const className = node.name?.getText() ?? ""; + const rerenderMethod = findClassInstanceMethod(node, "rerender", this.checker); + if (!rerenderMethod) { + return; + } + this.#reporter.addMessage(MESSAGE.NO_CONTROL_RERENDER_OVERRIDE, {className}, rerenderMethod); + } + analyzeMetadataProperty(type: string, node: ts.PropertyAssignment) { const analyzeMetadataDone = taskStart(`analyzeMetadataProperty: ${type}`, this.resourcePath, true); if (type === "interfaces") { diff --git a/src/linter/ui5Types/utils.ts b/src/linter/ui5Types/utils.ts index 5aa2fcb08..dfa550bca 100644 --- a/src/linter/ui5Types/utils.ts +++ b/src/linter/ui5Types/utils.ts @@ -23,3 +23,35 @@ export function getSymbolForPropertyInConstructSignatures( } return undefined; } + +export function findClassInstanceMethod( + node: ts.ClassDeclaration, methodName: string, checker: ts.TypeChecker +): ts.ClassElement | undefined { + return node.members.find((member) => { + if (!member.name) { + return false; + } + const name = getPropertyName(member.name); + if (name !== methodName) { + return false; + } + if (ts.isMethodDeclaration(member)) { + return true; + } + if (ts.isPropertyDeclaration(member)) { + if (!member.initializer) { + return false; + } + if (ts.isFunctionExpression(member.initializer) || ts.isArrowFunction(member.initializer)) { + return true; + }; + if (ts.isIdentifier(member.initializer)) { + const symbol = checker.getSymbolAtLocation(member.initializer); + if (symbol?.valueDeclaration && ts.isFunctionDeclaration(symbol.valueDeclaration)) { + return true; + } + } + } + return false; + }); +} diff --git a/test/fixtures/linter/projects/sap.ui.core/src/sap/ui/core/Control.js b/test/fixtures/linter/projects/sap.ui.core/src/sap/ui/core/Control.js index e21b1cc59..4849d8e44 100644 --- a/test/fixtures/linter/projects/sap.ui.core/src/sap/ui/core/Control.js +++ b/test/fixtures/linter/projects/sap.ui.core/src/sap/ui/core/Control.js @@ -7,6 +7,9 @@ sap.ui.define(["./Element"], function(Element) { library: "sap.ui.core", }, + // Should not be detected as declaration / override of a deprecated method + rerender: function() {}, + renderer : null // Control has no renderer }); return Control; diff --git a/test/fixtures/linter/projects/sap.ui.core/src/sap/ui/core/Element.js b/test/fixtures/linter/projects/sap.ui.core/src/sap/ui/core/Element.js index 07fc6c6aa..eee8501bf 100644 --- a/test/fixtures/linter/projects/sap.ui.core/src/sap/ui/core/Element.js +++ b/test/fixtures/linter/projects/sap.ui.core/src/sap/ui/core/Element.js @@ -7,6 +7,9 @@ sap.ui.define(["../base/ManagedObject"], function(ManagedObject) { library : "sap.ui.core" }, + // Should not be detected as declaration / override of a deprecated method + rerender: function() {}, + renderer : null // Element has no renderer }); return Element; diff --git a/test/fixtures/linter/rules/renderer/ControlRerenderOverride.js b/test/fixtures/linter/rules/renderer/ControlRerenderOverride.js new file mode 100644 index 000000000..3ee9311e0 --- /dev/null +++ b/test/fixtures/linter/rules/renderer/ControlRerenderOverride.js @@ -0,0 +1,122 @@ +sap.ui.define(["sap/ui/core/Control"], function(Control) { + const Example1 = Control.extend("sap.ui.demo.linter.controls.Example1", { + metadata: {}, + + rerender: function() { + console.log("Overriding rerender method"); + return Control.prototype.rerender.apply(this, arguments); + }, + + renderer: { + apiVersion: 2, + render: function(oRm, oControl) { + oRm.openStart("div", oControl); + oRm.openEnd(); + oRm.close("div"); + } + } + }); + + const Example2 = Control.extend("sap.ui.demo.linter.controls.Example2", { + metadata: {}, + + "rerender": function() { + console.log("Overriding rerender method without calling super method"); + }, + + renderer: { + apiVersion: 2, + render: function(oRm, oControl) { + oRm.openStart("div", oControl); + oRm.openEnd(); + oRm.close("div"); + } + } + }); + + const Example3 = Control.extend("sap.ui.demo.linter.controls.Example3", { + metadata: {}, + + rerender: () => { + console.log("Overriding rerender method without calling super method"); + }, + + renderer: { + apiVersion: 2, + render: function(oRm, oControl) { + oRm.openStart("div", oControl); + oRm.openEnd(); + oRm.close("div"); + } + } + }); + + const Example4 = Control.extend("sap.ui.demo.linter.controls.Example4", { + metadata: {}, + + rerender() { + console.log("Overriding rerender method without calling super method"); + }, + + renderer: { + apiVersion: 2, + render: function(oRm, oControl) { + oRm.openStart("div", oControl); + oRm.openEnd(); + oRm.close("div"); + } + } + }); + + const Example5 = Control.extend("sap.ui.demo.linter.controls.Example5", { + metadata: {}, + + renderer: { + apiVersion: 2, + render: function(oRm, oControl) { + oRm.openStart("div", oControl); + oRm.openEnd(); + oRm.close("div"); + } + } + }); + // TODO detect: Check why this override is currently not detected in JavaScript files. + // The same code is detected properly in ControlRerenderOverrideTypeScript.ts. + Example5.prototype.rerender = function() { + console.log("Overriding rerender method without calling super method"); + }; + + function rerender() { + console.log("Overriding rerender method without calling super method"); + } + + const Example6 = Control.extend("sap.ui.demo.linter.controls.Example6", { + metadata: {}, + + rerender, + + renderer: { + apiVersion: 2, + render: function(oRm, oControl) { + oRm.openStart("div", oControl); + oRm.openEnd(); + oRm.close("div"); + } + } + }); + + const Example7 = Control.extend("sap.ui.demo.linter.controls.Example7", { + metadata: {}, + + rerender: rerender, + + renderer: { + apiVersion: 2, + render: function(oRm, oControl) { + oRm.openStart("div", oControl); + oRm.openEnd(); + oRm.close("div"); + } + } + }); +}); diff --git a/test/fixtures/linter/rules/renderer/ControlRerenderOverrideTypeScript.ts b/test/fixtures/linter/rules/renderer/ControlRerenderOverrideTypeScript.ts new file mode 100644 index 000000000..f58548f0e --- /dev/null +++ b/test/fixtures/linter/rules/renderer/ControlRerenderOverrideTypeScript.ts @@ -0,0 +1,125 @@ +import Control from "sap/ui/core/Control"; +import type { MetadataOptions } from "sap/ui/core/Element"; +import RenderManager from "sap/ui/core/RenderManager"; + +/** + * @namespace sap.ui.demo.linter.controls + */ +class Example1 extends Control { + static readonly metadata: MetadataOptions = {} + + rerender() { + console.log("Overriding rerender method"); + return super.rerender(); + } + + static renderer = { + apiVersion: 2, + render: function(rm: RenderManager, control: Example1) { + rm.openStart("div", control); + rm.openEnd(); + rm.close("div"); + } + } +} + +/** + * @namespace sap.ui.demo.linter.controls + */ +class Example2 extends Control { + static readonly metadata: MetadataOptions = {} + + rerender() { + console.log("Overriding rerender method without calling super method"); + } + + static renderer = { + apiVersion: 2, + render: function(rm: RenderManager, control: Example2) { + rm.openStart("div", control); + rm.openEnd(); + rm.close("div"); + } + } +} + +/** + * @namespace sap.ui.demo.linter.controls + */ +class Example3 extends Control { + static readonly metadata: MetadataOptions = {} + + rerender = () => { + console.log("Overriding rerender method without calling super method"); + } + + static renderer = { + apiVersion: 2, + render: function(rm: RenderManager, control: Example3) { + rm.openStart("div", control); + rm.openEnd(); + rm.close("div"); + } + } +} + +/** + * @namespace sap.ui.demo.linter.controls + */ +class Example4 extends Control { + static readonly metadata: MetadataOptions = {} + + rerender = function() { + console.log("Overriding rerender method without calling super method"); + } + + static renderer = { + apiVersion: 2, + render: function(rm: RenderManager, control: Example4) { + rm.openStart("div", control); + rm.openEnd(); + rm.close("div"); + } + } +} + +/** + * @namespace sap.ui.demo.linter.controls + */ +class Example5 extends Control { + static readonly metadata: MetadataOptions = {} + + static renderer = { + apiVersion: 2, + render: function(rm: RenderManager, control: Example5) { + rm.openStart("div", control); + rm.openEnd(); + rm.close("div"); + } + } +} +Example5.prototype.rerender = function() { + console.log("Overriding rerender method without calling super method"); +}; + +function rerender() { + console.log("Overriding rerender method without calling super method"); +} + +/** + * @namespace sap.ui.demo.linter.controls + */ +class Example6 extends Control { + static readonly metadata: MetadataOptions = {} + + rerender = rerender + + static renderer = { + apiVersion: 2, + render: function(rm: RenderManager, control: Example6) { + rm.openStart("div", control); + rm.openEnd(); + rm.close("div"); + } + } +} diff --git a/test/lib/linter/rules/snapshots/renderer.ts.md b/test/lib/linter/rules/snapshots/renderer.ts.md index ff4bd1f9f..2ba67a1a4 100644 --- a/test/lib/linter/rules/snapshots/renderer.ts.md +++ b/test/lib/linter/rules/snapshots/renderer.ts.md @@ -475,6 +475,263 @@ Generated by [AVA](https://avajs.dev). ], warningCount: 0, }, + { + coverageInfo: [ + { + category: 1, + column: 5, + line: 13, + message: 'Unable to analyze this method call because the type of identifier "openStart" in "oRm.openStart("div", oControl)"" could not be determined', + }, + { + category: 1, + column: 5, + line: 14, + message: 'Unable to analyze this method call because the type of identifier "openEnd" in "oRm.openEnd()"" could not be determined', + }, + { + category: 1, + column: 5, + line: 15, + message: 'Unable to analyze this method call because the type of identifier "close" in "oRm.close("div")"" could not be determined', + }, + { + category: 1, + column: 5, + line: 30, + message: 'Unable to analyze this method call because the type of identifier "openStart" in "oRm.openStart("div", oControl)"" could not be determined', + }, + { + category: 1, + column: 5, + line: 31, + message: 'Unable to analyze this method call because the type of identifier "openEnd" in "oRm.openEnd()"" could not be determined', + }, + { + category: 1, + column: 5, + line: 32, + message: 'Unable to analyze this method call because the type of identifier "close" in "oRm.close("div")"" could not be determined', + }, + { + category: 1, + column: 5, + line: 47, + message: 'Unable to analyze this method call because the type of identifier "openStart" in "oRm.openStart("div", oControl)"" could not be determined', + }, + { + category: 1, + column: 5, + line: 48, + message: 'Unable to analyze this method call because the type of identifier "openEnd" in "oRm.openEnd()"" could not be determined', + }, + { + category: 1, + column: 5, + line: 49, + message: 'Unable to analyze this method call because the type of identifier "close" in "oRm.close("div")"" could not be determined', + }, + { + category: 1, + column: 5, + line: 64, + message: 'Unable to analyze this method call because the type of identifier "openStart" in "oRm.openStart("div", oControl)"" could not be determined', + }, + { + category: 1, + column: 5, + line: 65, + message: 'Unable to analyze this method call because the type of identifier "openEnd" in "oRm.openEnd()"" could not be determined', + }, + { + category: 1, + column: 5, + line: 66, + message: 'Unable to analyze this method call because the type of identifier "close" in "oRm.close("div")"" could not be determined', + }, + { + category: 1, + column: 5, + line: 77, + message: 'Unable to analyze this method call because the type of identifier "openStart" in "oRm.openStart("div", oControl)"" could not be determined', + }, + { + category: 1, + column: 5, + line: 78, + message: 'Unable to analyze this method call because the type of identifier "openEnd" in "oRm.openEnd()"" could not be determined', + }, + { + category: 1, + column: 5, + line: 79, + message: 'Unable to analyze this method call because the type of identifier "close" in "oRm.close("div")"" could not be determined', + }, + { + category: 1, + column: 5, + line: 101, + message: 'Unable to analyze this method call because the type of identifier "openStart" in "oRm.openStart("div", oControl)"" could not be determined', + }, + { + category: 1, + column: 5, + line: 102, + message: 'Unable to analyze this method call because the type of identifier "openEnd" in "oRm.openEnd()"" could not be determined', + }, + { + category: 1, + column: 5, + line: 103, + message: 'Unable to analyze this method call because the type of identifier "close" in "oRm.close("div")"" could not be determined', + }, + { + category: 1, + column: 5, + line: 116, + message: 'Unable to analyze this method call because the type of identifier "openStart" in "oRm.openStart("div", oControl)"" could not be determined', + }, + { + category: 1, + column: 5, + line: 117, + message: 'Unable to analyze this method call because the type of identifier "openEnd" in "oRm.openEnd()"" could not be determined', + }, + { + category: 1, + column: 5, + line: 118, + message: 'Unable to analyze this method call because the type of identifier "close" in "oRm.close("div")"" could not be determined', + }, + ], + errorCount: 7, + fatalErrorCount: 0, + filePath: 'ControlRerenderOverride.js', + messages: [ + { + column: 3, + line: 5, + message: 'Override of deprecated method \'rerender\' in control \'Example1\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 11, + line: 7, + message: 'Use of deprecated property \'rerender\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 3, + line: 23, + message: 'Override of deprecated method \'rerender\' in control \'Example2\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 3, + line: 40, + message: 'Override of deprecated method \'rerender\' in control \'Example3\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 3, + line: 57, + message: 'Override of deprecated method \'rerender\' in control \'Example4\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 3, + line: 96, + message: 'Override of deprecated method \'rerender\' in control \'Example6\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 3, + line: 111, + message: 'Override of deprecated method \'rerender\' in control \'Example7\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + }, + { + coverageInfo: [], + errorCount: 7, + fatalErrorCount: 0, + filePath: 'ControlRerenderOverrideTypeScript.ts', + messages: [ + { + column: 2, + line: 11, + message: 'Override of deprecated method \'rerender\' in control \'Example1\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 16, + line: 13, + message: 'Call to deprecated function \'rerender\' (super.rerender)', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 2, + line: 32, + message: 'Override of deprecated method \'rerender\' in control \'Example2\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 2, + line: 52, + message: 'Override of deprecated method \'rerender\' in control \'Example3\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 2, + line: 72, + message: 'Override of deprecated method \'rerender\' in control \'Example4\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 1, + line: 101, + message: 'Use of deprecated property \'rerender\'', + messageDetails: 'Deprecated test message', + ruleId: 'no-deprecated-api', + severity: 2, + }, + { + column: 2, + line: 115, + message: 'Override of deprecated method \'rerender\' in control \'Example6\'', + messageDetails: 'Starting from UI5 1.121 the framework no longer calls \'rerender\', so there is no point in overriding it. Move any rendering related code to the renderer or into onBeforeRendering/onAfterRendering.', + ruleId: 'no-deprecated-api', + severity: 2, + }, + ], + warningCount: 0, + }, { coverageInfo: [], errorCount: 3, diff --git a/test/lib/linter/rules/snapshots/renderer.ts.snap b/test/lib/linter/rules/snapshots/renderer.ts.snap index 5d71d97c6bb4b24b8ec972a1705ec475bdfea746..0e1f909bcc7ea28a25f619b8709807a9a19c92c8 100644 GIT binary patch literal 5863 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