Repository: exadel-inc/esl
Pull request #2509 (2024-07-12 10:55:04)

feat(lint): deprecation rule for `ESlMediaRuleList.parse`
Closes: #2500 

codeclimate[bot]: Code Climate has analyzed commit 6bd11f04 and detected **5 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Complexity | 3 |
| Duplication | 2 |


The test coverage on the diff in this pull request is **62.5%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **65.3%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2509).

codeclimate[bot]: Code Climate has analyzed commit 37e16a6a and detected **5 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Complexity | 3 |
| Duplication | 2 |


The test coverage on the diff in this pull request is **62.5%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **65.3%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2509).

codeclimate[bot]: Code Climate has analyzed commit 286c1d25 and detected **5 issues** on this pull request.

Here's the issue category breakdown:

| Category | Count |
|:---:|:---:|
| Complexity | 3 |
| Duplication | 2 |


The test coverage on the diff in this pull request is **62.5%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **65.3%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2509).

codeclimate[bot]: Code Climate has analyzed commit 193625d9 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **62.5%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **65.3%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2509).

codeclimate[bot]: Code Climate has analyzed commit e94e0e18 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **62.5%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **65.3%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2509).

codeclimate[bot]: Code Climate has analyzed commit 33af4eca and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **62.5%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **65.3%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2509).

codeclimate[bot]: Code Climate has analyzed commit 952fef74 and detected **0 issues** on this pull request.

The test coverage on the diff in this pull request is **62.5%** (**50%** is the threshold).

This pull request will bring the total coverage in the repository to **65.3%** (**0.0%** change).

View more on [Code Climate](https://codeclimate.com/github/exadel-inc/esl/pull/2509).


Patch:
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/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<string> {
     if (!this._srcRules) {
-      this.srcRules = ESLMediaRuleList.parse(this.src);
+      this.srcRules = ESLMediaRuleList.parseQuery(this.src);
     }
     return this._srcRules;
   }
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<string> {
-    return ESLMediaRuleList.parse(this.mode);
+    return ESLMediaRuleList.parseQuery(this.mode);
   }
 
   /** @returns ESLMediaRuleList instance of the min-open-items mapping */
   @memoize()
   public get minValueRules(): ESLMediaRuleList<number> {
-    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<number> {
-    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<string> {
-    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<string> {
-    return ESLMediaRuleList.parse(this.scrollable);
+    return ESLMediaRuleList.parseQuery(this.scrollable);
   }
 
   /** @returns current scrollable type */


Test patch:
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-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);