From 6279338d685f33faddc72dff5d66003e5e9931ba 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 | 14 +++ .../rules/renderer/ControlRerenderOverride.js | 36 ++++++ .../rules/renderer/ControlRerenderOverride.ts | 44 ++++++++ .../lib/linter/rules/snapshots/renderer.ts.md | 103 ++++++++++++++++++ .../linter/rules/snapshots/renderer.ts.snap | Bin 4273 -> 5007 bytes 6 files changed, 208 insertions(+) create mode 100644 test/fixtures/linter/rules/renderer/ControlRerenderOverride.js create mode 100644 test/fixtures/linter/rules/renderer/ControlRerenderOverride.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 d11d650dd..f1aba43ed 100644 --- a/src/linter/ui5Types/SourceFileLinter.ts +++ b/src/linter/ui5Types/SourceFileLinter.ts @@ -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,19 @@ export default class SourceFileLinter { } } + analyzeControlRerenderMethod(node: ts.ClassDeclaration) { + const className = node.name?.getText() ?? ""; + const rerenderMethod = node.members.find((member) => { + return ts.isMethodDeclaration(member) && + (ts.isIdentifier(member.name) || ts.isStringLiteral(member.name)) && + member.name.text === "rerender"; + }); + 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/test/fixtures/linter/rules/renderer/ControlRerenderOverride.js b/test/fixtures/linter/rules/renderer/ControlRerenderOverride.js new file mode 100644 index 000000000..a43304778 --- /dev/null +++ b/test/fixtures/linter/rules/renderer/ControlRerenderOverride.js @@ -0,0 +1,36 @@ +sap.ui.define(["sap/ui/core/Control"], function(Control, Button) { + 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"); + } + } + }); +}); diff --git a/test/fixtures/linter/rules/renderer/ControlRerenderOverride.ts b/test/fixtures/linter/rules/renderer/ControlRerenderOverride.ts new file mode 100644 index 000000000..b77ac049d --- /dev/null +++ b/test/fixtures/linter/rules/renderer/ControlRerenderOverride.ts @@ -0,0 +1,44 @@ +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: Example1) { + 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..98344ce96 100644 --- a/test/lib/linter/rules/snapshots/renderer.ts.md +++ b/test/lib/linter/rules/snapshots/renderer.ts.md @@ -475,6 +475,109 @@ 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', + }, + ], + errorCount: 3, + 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, + }, + ], + warningCount: 0, + }, + { + coverageInfo: [], + errorCount: 3, + fatalErrorCount: 0, + filePath: 'ControlRerenderOverride.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, + }, + ], + 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..28ec0c135839f2a1c136e2d5527793833a950973 100644 GIT binary patch literal 5007 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