From fae6bc7b3d1bd4b9d1b40eb9e63bbfa5b46b43a8 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 | 13 +++ 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 -> 5178 bytes 6 files changed, 210 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..ddcd888f0 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,18 @@ 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: () => `Using this method is no longer recommended. Synchronous DOM updates via this method have \ +several drawbacks: they only work when the control has been rendered before (no initial rendering possible), \ +multiple state changes won't be combined automatically into a single re-rendering, they might cause additional \ +layout trashing, standard invalidation might cause another async re-rendering.The recommended alternative is to rely \ +on invalidation and standard re-rendering.`, + }, + [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..26f8e6155 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: 'Using this method is no longer recommended. Synchronous DOM updates via this method have several drawbacks: they only work when the control has been rendered before (no initial rendering possible), multiple state changes won\'t be combined automatically into a single re-rendering, they might cause additional layout trashing, standard invalidation might cause another async re-rendering.The recommended alternative is to rely on invalidation and standard re-rendering.', + 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: 'Using this method is no longer recommended. Synchronous DOM updates via this method have several drawbacks: they only work when the control has been rendered before (no initial rendering possible), multiple state changes won\'t be combined automatically into a single re-rendering, they might cause additional layout trashing, standard invalidation might cause another async re-rendering.The recommended alternative is to rely on invalidation and standard re-rendering.', + 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: 'Using this method is no longer recommended. Synchronous DOM updates via this method have several drawbacks: they only work when the control has been rendered before (no initial rendering possible), multiple state changes won\'t be combined automatically into a single re-rendering, they might cause additional layout trashing, standard invalidation might cause another async re-rendering.The recommended alternative is to rely on invalidation and standard re-rendering.', + 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: 'Using this method is no longer recommended. Synchronous DOM updates via this method have several drawbacks: they only work when the control has been rendered before (no initial rendering possible), multiple state changes won\'t be combined automatically into a single re-rendering, they might cause additional layout trashing, standard invalidation might cause another async re-rendering.The recommended alternative is to rely on invalidation and standard re-rendering.', + 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..a211d40fa2058e4fa2b30fb293798803e75f48ff 100644 GIT binary patch literal 5178 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 literal 4273 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