From a1f916ae4cf9a7d618516646e6629ae3a58f3d68 Mon Sep 17 00:00:00 2001 From: fshovchko <60318924+fshovchko@users.noreply.github.com> Date: Fri, 12 Jul 2024 18:54:03 +0300 Subject: [PATCH] feat(lint): deprecation rule for `ESlMediaRuleList.parse` (#2509) --- eslint/README.md | 2 + eslint/src/core/deprecated-class-method.ts | 56 +++++ eslint/src/rules/4/all.rules.ts | 3 + .../4/deprecated.media-rule-list-parse.ts | 16 ++ eslint/test/deprecated-class-method.test.ts | 209 ++++++++++++++++++ src/modules/esl-image/core/esl-image.ts | 6 +- .../test/esl-media-rule-list.test.ts | 12 +- .../esl-panel-group/core/esl-panel-group.ts | 8 +- src/modules/esl-tab/core/esl-tabs.ts | 2 +- 9 files changed, 300 insertions(+), 14 deletions(-) create mode 100644 eslint/src/core/deprecated-class-method.ts create mode 100644 eslint/src/rules/4/deprecated.media-rule-list-parse.ts create mode 100644 eslint/test/deprecated-class-method.test.ts diff --git a/eslint/README.md b/eslint/README.md index 82996a850..15f1ca376 100644 --- a/eslint/README.md +++ b/eslint/README.md @@ -64,6 +64,8 @@ The ESLint plugin provides a separate rule for each deprecated utility within th - `@exadel/esl/deprecated-4/toggleable-action-params` - Rule for deprecated `ToggleableActionParams` alias for `ESLToggleableActionParams`. - `@exadel/esl/deprecated-4/tooltip-action-params` - Rule for deprecated `TooltipActionParams` alias for `ESLTooltipActionParams`. +- `@exadel/esl/deprecated-4/media-rule-list-parse` - Rule for deprecated `ESLMediaRuleList.parse` alias for `ESLMediaRuleList.parseQuery` or `ESLMediaRuleList.parseTuple`. + - `@exadel/esl/deprecated-4/base-decorators-path` - Rule for deprecated `@attr`, `@prop`, `@boolAttr`, `@jsonAttr`, `@listen` import paths. - `@exadel/esl/deprecated-5/alert-action-params` - Rule for deprecated `AlertActionParams` alias for `ESLAlertActionParams`. diff --git a/eslint/src/core/deprecated-class-method.ts b/eslint/src/core/deprecated-class-method.ts new file mode 100644 index 000000000..81e89e5c7 --- /dev/null +++ b/eslint/src/core/deprecated-class-method.ts @@ -0,0 +1,56 @@ +import type * as ESTree from 'estree'; +import type {Rule} from 'eslint'; + +const meta: Rule.RuleModule['meta'] = { + type: 'suggestion', + docs: { + description: 'replace deprecated class static methods with recommended ones', + recommended: true + }, + fixable: 'code' +}; + +export interface replacementMethodCfg { + replacement?: string; + message: string; +} + +export interface ESLintDeprecationStaticMethodCfg { + /** Class name */ + className: string; + /** Deprecated static method name */ + deprecatedMethod: string; + /** Function that returns recommended method */ + getReplacemetMethod: (expression: ESTree.CallExpression) => replacementMethodCfg; +} + +type StaticMethodNode = ESTree.MemberExpression & Rule.NodeParentExtension; + +/** Builds deprecation rule from {@link ESLintDeprecationStaticMethodCfg} object */ +export function buildRule(config: ESLintDeprecationStaticMethodCfg): Rule.RuleModule { + const create = (context: Rule.RuleContext): Rule.RuleListener => { + return { + MemberExpression(node: StaticMethodNode): null { + if (isDeprecatedMethod(node, config)) handleCallExpression(node, context, config); + return null; + } + }; + }; + + return {meta, create}; +} + +function isDeprecatedMethod(node: StaticMethodNode, config: ESLintDeprecationStaticMethodCfg): boolean { + const {object, property} = node; + return object.type === 'Identifier' && property.type === 'Identifier' && object.name === config.className && property.name === config.deprecatedMethod; +} + +function handleCallExpression(node: StaticMethodNode, context: Rule.RuleContext, config: ESLintDeprecationStaticMethodCfg): void { + const {replacement, message} = config.getReplacemetMethod(node.parent as ESTree.CallExpression); + + context.report({ + node, + message: `[ESL Lint]: Deprecated static method ${config.className}.${config.deprecatedMethod}, use ${config.className}.${message} instead`, + fix: replacement ? (fixer): Rule.Fix => fixer.replaceText(node.property, replacement) : undefined + }); +} diff --git a/eslint/src/rules/4/all.rules.ts b/eslint/src/rules/4/all.rules.ts index f39092f3a..ead4003a9 100644 --- a/eslint/src/rules/4/all.rules.ts +++ b/eslint/src/rules/4/all.rules.ts @@ -6,6 +6,8 @@ import deprecatedToggleableActionParams from './deprecated.toggleable-action-par import deprecatedBaseDecoratorsPath from './deprecated.base-decorators-path'; +import deprecatedMediaRuleListParse from './deprecated.media-rule-list-parse'; + export default { // Aliases 'deprecated-4/generate-uid': deprecatedGenerateUid, @@ -13,6 +15,7 @@ export default { 'deprecated-4/event-utils': deprecatedEventUtils, 'deprecated-4/traversing-query': deprecatedTraversingQuery, 'deprecated-4/toggleable-action-params': deprecatedToggleableActionParams, + 'deprecated-4/media-rule-list-parse': deprecatedMediaRuleListParse, // Paths 'deprecated-4/base-decorators-path': deprecatedBaseDecoratorsPath }; diff --git a/eslint/src/rules/4/deprecated.media-rule-list-parse.ts b/eslint/src/rules/4/deprecated.media-rule-list-parse.ts new file mode 100644 index 000000000..d34e403fd --- /dev/null +++ b/eslint/src/rules/4/deprecated.media-rule-list-parse.ts @@ -0,0 +1,16 @@ +import {buildRule} from '../../core/deprecated-class-method'; +import type {replacementMethodCfg} from '../../core/deprecated-class-method'; + +/** + * Rule for deprecated 'parse' method of {@link ESLMediaRuleList} + */ +export default buildRule({ + className: 'ESLMediaRuleList', + deprecatedMethod: 'parse', + getReplacemetMethod: (expression): replacementMethodCfg => { + const args = expression.arguments; + if (expression.type !== 'CallExpression') return {message: 'parseQuery or parseTuple'}; + const methodName = args.length === 1 || (args[1]?.type !== 'Literal' && args[1]?.type !== 'TemplateLiteral') ? 'parseQuery' : 'parseTuple'; + return {message: methodName, replacement: methodName}; + } +}); diff --git a/eslint/test/deprecated-class-method.test.ts b/eslint/test/deprecated-class-method.test.ts new file mode 100644 index 000000000..9f4318a0b --- /dev/null +++ b/eslint/test/deprecated-class-method.test.ts @@ -0,0 +1,209 @@ +import {RuleTester} from 'eslint'; +import {buildRule} from '../src/core/deprecated-class-method'; + +import deprecatedMediaRuleListParse from '../src/rules/4/deprecated.media-rule-list-parse'; + +const VALID_CASES = [ + { + code: ` + ESLMediaRuleList.parseQuery('1 | 2'); + ` + }, { + code: ` + ESLMediaRuleList.parseQuery('1 | 2', String); + ` + }, { + code: ` + ESLMediaRuleList.parseTuple('1 | 2', '3|4'); + ` + }, { + code: ` + ESLMediaRuleList.parseTuple('1 | 2', '3|4', String); + ` + }, { + code: ` + TestClass.newMethodNoArgs(); + ` + }, { + code: ` + TestClass.newMethodOneArg('test'); + ` + }, { + code: ` + TestClass.newMethodMultipleArgs('test', 42); + ` + }, { + code: ` + TestClass.newMethodMultipleArgsNonLiteral(1, 2); + ` + }, { + code: ` + AnotherClass.modernMethod(); + ` + }, { + code: ` + AnotherClass.modernMethodForManyArgs(1, 2, 3); + ` + }, { + code: ` + AnotherClass.modernMethod; + ` + }, { + code: ` + const method = AnotherClass.modernMethod; + ` + } +]; + +const INVALID_CASES_TEST_CLASS = [ + { + code: ` + TestClass.oldMethod(); + `, + errors: [ + '[ESL Lint]: Deprecated static method TestClass.oldMethod, use TestClass.newMethodNoArgs instead' + ], + output: ` + TestClass.oldMethod(); + ` + }, { + code: ` + TestClass.oldMethod(1, () => {}); + `, + errors: [ + '[ESL Lint]: Deprecated static method TestClass.oldMethod, use TestClass.newMethodMultipleArgsNonLiteral instead' + ], + output: ` + TestClass.newMethodMultipleArgsNonLiteral(1, () => {}); + ` + }, { + code: ` + TestClass.oldMethod('test'); + `, + errors: [ + '[ESL Lint]: Deprecated static method TestClass.oldMethod, use TestClass.newMethodOneArg instead' + ], + output: ` + TestClass.newMethodOneArg('test'); + ` + }, { + code: ` + TestClass.oldMethod('test', 42); + `, + errors: [ + '[ESL Lint]: Deprecated static method TestClass.oldMethod, use TestClass.newMethodMultipleArgs instead' + ], + output: ` + TestClass.newMethodMultipleArgs('test', 42); + ` + } +]; + +const INVALID_CASES_RULE_LIST = [ + { + code: ` + const t = ESLMediaRuleList.parse; + `, + errors: [ + '[ESL Lint]: Deprecated static method ESLMediaRuleList.parse, use ESLMediaRuleList.parseQuery or parseTuple instead' + ], + output: ` + const t = ESLMediaRuleList.parse; + ` + }, { + code: ` + ESLMediaRuleList.parse; + `, + errors: [ + '[ESL Lint]: Deprecated static method ESLMediaRuleList.parse, use ESLMediaRuleList.parseQuery or parseTuple instead' + ], + output: ` + ESLMediaRuleList.parse; + ` + }, { + code: ` + ESLMediaRuleList.parse('1 | 2'); + `, + errors: [ + '[ESL Lint]: Deprecated static method ESLMediaRuleList.parse, use ESLMediaRuleList.parseQuery instead' + ], + output: ` + ESLMediaRuleList.parseQuery('1 | 2'); + ` + }, { + code: ` + ESLMediaRuleList.parse('1 | 2', String); + `, + errors: [ + '[ESL Lint]: Deprecated static method ESLMediaRuleList.parse, use ESLMediaRuleList.parseQuery instead' + ], + output: ` + ESLMediaRuleList.parseQuery('1 | 2', String); + ` + }, { + code: ` + ESLMediaRuleList.parse('1 | 2', '3|4'); + `, + errors: [ + '[ESL Lint]: Deprecated static method ESLMediaRuleList.parse, use ESLMediaRuleList.parseTuple instead' + ], + output: ` + ESLMediaRuleList.parseTuple('1 | 2', '3|4'); + ` + }, { + code: ` + ESLMediaRuleList.parse('1 | 2', \`3|4\`); + `, + errors: [ + '[ESL Lint]: Deprecated static method ESLMediaRuleList.parse, use ESLMediaRuleList.parseTuple instead' + ], + output: ` + ESLMediaRuleList.parseTuple('1 | 2', \`3|4\`); + ` + }, { + code: ` + ESLMediaRuleList.parse('1 | 2', '3|4', String); + `, + errors: [ + '[ESL Lint]: Deprecated static method ESLMediaRuleList.parse, use ESLMediaRuleList.parseTuple instead' + ], + output: ` + ESLMediaRuleList.parseTuple('1 | 2', '3|4', String); + ` + } +]; + +describe('ESL Migration Rules: Deprecated Static Method: valid', () => { + const rule = buildRule({ + className: 'TestClass', + deprecatedMethod: 'oldMethod', + getReplacemetMethod: (expression) => { + const args = expression.arguments; + if (args.length === 0) return {message: 'newMethodNoArgs'}; + + let methodName; + if (args.length === 1) methodName = 'newMethodOneArg'; + else if (args.length > 1 && args[args.length - 1].type !== 'Literal' && args[args.length - 1].type !== 'TemplateLiteral') { + methodName = 'newMethodMultipleArgsNonLiteral'; + } + else methodName = 'newMethodMultipleArgs'; + return {message: methodName, replacement: methodName}; + } + }); + + const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser') + }); + + ruleTester.run('deprecated-static-method', rule, {valid: VALID_CASES, invalid: INVALID_CASES_TEST_CLASS}); +}); + +describe('ESL Migration Rules: Deprecated Static Method: valid', () => { + const rule = deprecatedMediaRuleListParse; + + const ruleTester = new RuleTester({ + parser: require.resolve('@typescript-eslint/parser') + }); + + ruleTester.run('deprecated-static-method', rule, {valid: VALID_CASES, invalid: INVALID_CASES_RULE_LIST}); +}); diff --git a/src/modules/esl-image/core/esl-image.ts b/src/modules/esl-image/core/esl-image.ts index 93d216d6e..eb043a0c4 100644 --- a/src/modules/esl-image/core/esl-image.ts +++ b/src/modules/esl-image/core/esl-image.ts @@ -68,7 +68,7 @@ export class ESLImage extends ESLBaseElement { this.alt = this.alt || this.getAttribute('aria-label') || this.getAttribute('data-alt') || ''; this.updateA11y(); - this.srcRules = ESLMediaRuleList.parse(this.src); + this.srcRules = ESLMediaRuleList.parseQuery(this.src); if (this.lazyObservable) { this.removeAttribute('lazy-triggered'); getIObserver().observe(this); @@ -100,7 +100,7 @@ export class ESLImage extends ESLBaseElement { this.updateA11y(); break; case 'data-src': - this.srcRules = ESLMediaRuleList.parse(newVal); + this.srcRules = ESLMediaRuleList.parseQuery(newVal); this.refresh(); break; case 'data-src-base': @@ -117,7 +117,7 @@ export class ESLImage extends ESLBaseElement { public get srcRules(): ESLMediaRuleList { if (!this._srcRules) { - this.srcRules = ESLMediaRuleList.parse(this.src); + this.srcRules = ESLMediaRuleList.parseQuery(this.src); } return this._srcRules; } diff --git a/src/modules/esl-media-query/test/esl-media-rule-list.test.ts b/src/modules/esl-media-query/test/esl-media-rule-list.test.ts index bf89f6abf..cce13af4d 100644 --- a/src/modules/esl-media-query/test/esl-media-rule-list.test.ts +++ b/src/modules/esl-media-query/test/esl-media-rule-list.test.ts @@ -8,7 +8,7 @@ describe('ESLMediaRuleList', () => { describe('Integration cases:', () => { test('Basic case: "1 | @sm => 2 | @md => 3" parsed correctly', () => { - const mrl = ESLMediaRuleList.parse('1 | @sm => 2 | @md => 3'); + const mrl = ESLMediaRuleList.parseQuery('1 | @sm => 2 | @md => 3'); expect(mrl.rules.length).toBe(3); mockSmMatchMedia.matches = false; @@ -30,7 +30,7 @@ describe('ESLMediaRuleList', () => { }); test('Extended media case parsed correctly: "1 | @sm and @md => 2"', () => { - const mrl = ESLMediaRuleList.parse('1 | @sm and @md => 2'); + const mrl = ESLMediaRuleList.parseQuery('1 | @sm and @md => 2'); const listener = jest.fn(); expect(mrl.rules.length).toBe(2); @@ -57,7 +57,7 @@ describe('ESLMediaRuleList', () => { }); test('Extended media case parsed correctly: "1 | @sm or @md => 2"', () => { - const mrl = ESLMediaRuleList.parse('1 | @sm or @md => 2'); + const mrl = ESLMediaRuleList.parseQuery('1 | @sm or @md => 2'); const listener = jest.fn(); expect(mrl.rules.length).toBe(2); @@ -112,7 +112,7 @@ describe('ESLMediaRuleList', () => { describe('Basic cases:', () => { test('Single value parsed to the single "all" rule', () => { - const mrl = ESLMediaRuleList.parse('123'); + const mrl = ESLMediaRuleList.parseQuery('123'); expect(mrl.rules.length).toBe(1); expect(mrl.active.length).toBeGreaterThan(0); expect(mrl.value).toBe('123'); @@ -120,12 +120,12 @@ describe('ESLMediaRuleList', () => { }); test('Single rule with media query "@sm => 1"', () => { - const mrl = ESLMediaRuleList.parse('@sm => 1'); + const mrl = ESLMediaRuleList.parseQuery('@sm => 1'); expect(mrl.rules.length).toBe(1); }); test('Single rule "@sm => 1" response to the matcher correctly', () => { - const mrl = ESLMediaRuleList.parse('@sm => 1'); + const mrl = ESLMediaRuleList.parseQuery('@sm => 1'); mockSmMatchMedia.matches = false; expect(mrl.value).toBe(undefined); diff --git a/src/modules/esl-panel-group/core/esl-panel-group.ts b/src/modules/esl-panel-group/core/esl-panel-group.ts index 42b9b8e52..6b916bf7d 100644 --- a/src/modules/esl-panel-group/core/esl-panel-group.ts +++ b/src/modules/esl-panel-group/core/esl-panel-group.ts @@ -132,25 +132,25 @@ export class ESLPanelGroup extends ESLBaseElement { /** @returns ESLMediaRuleList instance of the mode mapping */ @memoize() public get modeRules(): ESLMediaRuleList { - return ESLMediaRuleList.parse(this.mode); + return ESLMediaRuleList.parseQuery(this.mode); } /** @returns ESLMediaRuleList instance of the min-open-items mapping */ @memoize() public get minValueRules(): ESLMediaRuleList { - return ESLMediaRuleList.parse(this.minOpenItems, parseCount); + return ESLMediaRuleList.parseQuery(this.minOpenItems, parseCount); } /** @returns ESLMediaRuleList instance of the max-open-items mapping */ @memoize() public get maxValueRules(): ESLMediaRuleList { - return ESLMediaRuleList.parse(this.maxOpenItems, parseCount); + return ESLMediaRuleList.parseQuery(this.maxOpenItems, parseCount); } /** @returns ESLMediaRuleList instance of the refresh-strategy mapping */ @memoize() public get refreshRules(): ESLMediaRuleList { - return ESLMediaRuleList.parse(this.refreshStrategy); + return ESLMediaRuleList.parseQuery(this.refreshStrategy); } /** @returns current mode */ diff --git a/src/modules/esl-tab/core/esl-tabs.ts b/src/modules/esl-tab/core/esl-tabs.ts index b16fb6759..012c5ccf5 100644 --- a/src/modules/esl-tab/core/esl-tabs.ts +++ b/src/modules/esl-tab/core/esl-tabs.ts @@ -43,7 +43,7 @@ export class ESLTabs extends ESLBaseElement { /** ESLMediaRuleList instance of the scrollable type mapping */ @memoize() public get scrollableTypeRules(): ESLMediaRuleList { - return ESLMediaRuleList.parse(this.scrollable); + return ESLMediaRuleList.parseQuery(this.scrollable); } /** @returns current scrollable type */