Skip to content

Commit

Permalink
feat: add no-deprecated-components rule
Browse files Browse the repository at this point in the history
  • Loading branch information
kelsos committed Jan 22, 2024
1 parent 7b33b48 commit 00ce936
Show file tree
Hide file tree
Showing 9 changed files with 314 additions and 13 deletions.
1 change: 1 addition & 0 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ hero:
- theme: alt
text: Get started
link: /started
---
42 changes: 42 additions & 0 deletions docs/rules/no-deprecated-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
---
title: '@rotki/no-deprecated-components'
description: description
since: v0.1.0
---

# @rotki/no-deprecated-components

> description
## :book: Rule Details

This rule reports ???.

<eslint-code-block>

<!-- eslint-skip -->

```vue
<script>
/* eslint @rotki/no-deprecated-components: "error" */
</script>
<!-- ✓ GOOD -->
<!-- ✗ BAD -->
```

</eslint-code-block>

## :gear: Options

```json
{
"@rotki/no-deprecated-components": ["error", {
"legacy": true
}]
}
```

-
1 change: 1 addition & 0 deletions docs/started.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ pnpm install --D --save-exact eslint @rotki/eslint-plugin
```

### Requirements

- ESLint 8.x
- NodeJS 18.x

Expand Down
1 change: 1 addition & 0 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export default rotki({
tsconfigPath: 'tsconfig.json',
},
stylistic: true,
formatters: true,
}, {
files: ['src/**/*.ts'],
rules: {
Expand Down
104 changes: 104 additions & 0 deletions src/rules/no-deprecated-components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import createDebug from 'debug';
import { pascalCase, snakeCase } from 'scule';
import { createEslintRule, defineTemplateBodyVisitor, getSourceCode } from '../utils';
import type { VElement } from 'vue-eslint-parser/ast';

const debug = createDebug('@rotki/eslint-plugin:no-deprecated-components');

export const RULE_NAME = 'no-deprecated-components';

export type MessageIds = 'removed' | 'replacedWith' | 'deprecated';

export type Options = [{ legacy: boolean }];

const replacements = {
DataTable: true,
Fragment: false,
} as const;

const skipInLegacy = [
'Fragment',
];

function hasReplacement(tag: string): tag is (keyof typeof replacements) {
return Object.prototype.hasOwnProperty.call(replacements, tag);
}

export default createEslintRule<Options, MessageIds>({
create(context, optionsWithDefault) {
const options = optionsWithDefault[0] || {};
const legacy = options.legacy;
return defineTemplateBodyVisitor(context, {
VElement(element: VElement) {
const tag = pascalCase(element.rawName);

const sourceCode = getSourceCode(context);
if (!('getTemplateBodyTokenStore' in sourceCode.parserServices))
throw new Error('cannot find getTemplateBodyTokenStore in parserServices');

if (!hasReplacement(tag))
return;

if (legacy && skipInLegacy.includes(tag)) {
debug(`${tag} is skipped in legacy mode`);
return;
}

const replacement = replacements[tag];

if (!replacement) {
debug(`${tag} has will be removed`);
context.report({
data: {
name: snakeCase(tag),
},
fix(fixer) {
return [
fixer.remove(element.startTag),
...(element.endTag ? [fixer.remove(element.endTag)] : []),
];
},
messageId: 'removed',
node: element,
});
}
else {
debug(`${tag} has been deprecated`);
context.report({
data: {
name: snakeCase(tag),
},
messageId: 'deprecated',
node: element,
});
}
},
});
},
defaultOptions: [{ legacy: false }],
meta: {
docs: {
description: 'Removes deprecated classes that do not exist anymore',
recommended: 'recommended',
},
fixable: 'code',
messages: {
deprecated: `'{{ name }}' has been deprecated`,
removed: `'{{ name }}' has been removed`,
replacedWith: `'{{ a }}' has been replaced with '{{ b }}'`,
},
schema: [
{
additionalProperties: false,
properties: {
legacy: {
type: 'boolean',
},
},
type: 'object',
},
],
type: 'problem',
},
name: RULE_NAME,
});
116 changes: 110 additions & 6 deletions src/types/eslint.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,120 @@
import type { AST, Rule } from 'eslint';
import type { Node } from 'vue-eslint-parser/ast/nodes';
import type { SourceCode } from './vue-parser-services';
import type { Rule } from 'eslint';
import type { RuleContext as TSESLintRuleContext } from '@typescript-eslint/utils/ts-eslint';
import type { ESLintUtils, TSESLint, TSESTree } from '@typescript-eslint/utils';

export interface RuleCreateAndOptions<TOptions extends readonly unknown[], TMessageIds extends string> {
create: (context: Readonly<RuleContext<TMessageIds, TOptions>>, optionsWithDefault: Readonly<TOptions>) => TSESLint.RuleListener;
defaultOptions: Readonly<TOptions>;
}

export interface RuleWithMeta<TOptions extends readonly unknown[], TMessageIds extends string> extends RuleCreateAndOptions<TOptions, TMessageIds> {
meta: TSESLint.RuleMetaData<TMessageIds>;
}

export interface RuleWithMetaAndName<TOptions extends readonly unknown[], TMessageIds extends string> extends RuleCreateAndOptions<TOptions, TMessageIds> {
meta: ESLintUtils.NamedCreateRuleMeta<TMessageIds>;
name: string;
}

interface RuleFix {
range: Readonly<AST.Range>;
text: string;
}

type NodeOrToken = TSESTree.Node | TSESTree.Token | Node;

interface RuleFixer {
insertTextAfter(nodeOrToken: NodeOrToken, text: string): RuleFix;
insertTextAfterRange(range: Readonly<AST.Range>, text: string): RuleFix;
insertTextBefore(nodeOrToken: NodeOrToken, text: string): RuleFix;
insertTextBeforeRange(range: Readonly<AST.Range>, text: string): RuleFix;
remove(nodeOrToken: NodeOrToken): RuleFix;
removeRange(range: Readonly<AST.Range>): RuleFix;
replaceText(nodeOrToken: NodeOrToken, text: string): RuleFix;
replaceTextRange(range: Readonly<AST.Range>, text: string): RuleFix;
}

interface SuggestionReportDescriptor<TMessageIds extends string> extends Omit<ReportDescriptorBase<TMessageIds>, 'fix'> {
readonly fix: ReportFixFunction;
}

type ReportFixFunction = (fixer: RuleFixer) => IterableIterator<RuleFix> | RuleFix | readonly RuleFix[] | null;

type ReportSuggestionArray<TMessageIds extends string> = SuggestionReportDescriptor<TMessageIds>[];

type ReportDescriptorMessageData = Readonly<Record<string, unknown>>;

interface ReportDescriptorBase<TMessageIds extends string> {
/**
* The parameters for the message string associated with `messageId`.
*/
readonly data?: ReportDescriptorMessageData;
/**
* The fixer function.
*/
readonly fix?: ReportFixFunction | null;
/**
* The messageId which is being reported.
*/
readonly messageId: TMessageIds;
}

interface ReportDescriptorWithSuggestion<TMessageIds extends string> extends ReportDescriptorBase<TMessageIds> {
/**
* 6.7's Suggestions API
*/
readonly suggest?: Readonly<ReportSuggestionArray<TMessageIds>> | null;
}

interface ReportDescriptorNodeOptionalLoc {
/**
* The Node or AST Token which the report is being attached to
*/
readonly node: NodeOrToken;
/**
* An override of the location of the report
*/
readonly loc?: Readonly<TSESTree.Position> | Readonly<TSESTree.SourceLocation>;
}

interface ReportDescriptorLocOnly {
/**
* An override of the location of the report
*/
loc: Readonly<TSESTree.Position> | Readonly<TSESTree.SourceLocation>;
}

export type ReportDescriptor<TMessageIds extends string> = ReportDescriptorWithSuggestion<TMessageIds> & (ReportDescriptorLocOnly | ReportDescriptorNodeOptionalLoc);

export interface RuleModule<
T extends readonly unknown[],
> extends Rule.RuleModule {
defaultOptions: T;
TMessageIds extends string,
TOptions extends readonly unknown[] = [],
TRuleListener extends TSESLint.RuleListener = TSESLint.RuleListener,
> {
/**
* Default options the rule will be run with
*/
defaultOptions: TOptions;
/**
* Metadata about the rule
*/
meta: TSESLint.RuleMetaData<TMessageIds>;
/**
* Function which returns an object with methods that ESLint calls to “visit”
* nodes while traversing the abstract syntax tree.
*/
create(context: Readonly<RuleContext<TMessageIds, TOptions>>): TRuleListener;
}

export interface PluginRuleModule<TOptions extends readonly unknown[] = []> extends Rule.RuleModule {
defaultOptions: TOptions;
}

export interface RuleContext<
TMessageIds extends string,
TOptions extends readonly unknown[],
> extends Omit<TSESLintRuleContext<TMessageIds, TOptions>, 'sourceCode'> {
> extends Omit<TSESLint.RuleContext<TMessageIds, TOptions>, 'sourceCode' | 'report'> {
sourceCode: Readonly<SourceCode>;
report(descriptor: ReportDescriptor<TMessageIds>): void;
}
16 changes: 11 additions & 5 deletions src/utils/rule.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import type { RuleContext, RuleListener, RuleModule } from '../types';
import type { RuleWithMeta, RuleWithMetaAndName } from '@typescript-eslint/utils/eslint-utils';
import type {
PluginRuleModule,
RuleContext,
RuleListener,
RuleModule,
RuleWithMeta,
RuleWithMetaAndName,
} from '../types';

const blobUrl = 'https://rotki.github.io/eslint-plugin/rules/';

Expand All @@ -19,7 +25,7 @@ function RuleCreator(urlCreator: (ruleName: string) => string) {
meta,
name,
...rule
}: Readonly<RuleWithMetaAndName<TOptions, TMessageIds>>): RuleModule<TOptions> {
}: Readonly<RuleWithMetaAndName<TOptions, TMessageIds>>): RuleModule<TMessageIds, TOptions> {
return createRule<TOptions, TMessageIds>({
meta: {
...meta,
Expand All @@ -46,7 +52,7 @@ function createRule<
create,
defaultOptions,
meta,
}: Readonly<RuleWithMeta<TOptions, TMessageIds>>): RuleModule<TOptions> {
}: Readonly<RuleWithMeta<TOptions, TMessageIds>>): RuleModule<TMessageIds, TOptions> {
return {
create: ((
context: Readonly<RuleContext<TMessageIds, TOptions>>,
Expand All @@ -64,4 +70,4 @@ function createRule<

export const createEslintRule = RuleCreator(
ruleName => `${blobUrl}${ruleName}`,
) as any as <TOptions extends readonly unknown[], TMessageIds extends string>({ meta, name, ...rule }: Readonly<RuleWithMetaAndName<TOptions, TMessageIds>>) => RuleModule<TOptions>;
) as any as <TOptions extends readonly unknown[], TMessageIds extends string>({ meta, name, ...rule }: Readonly<RuleWithMetaAndName<TOptions, TMessageIds>>) => PluginRuleModule<TOptions>;
3 changes: 1 addition & 2 deletions src/utils/visitor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { extname } from 'node:path';
import { getFilename, getSourceCode } from './compat';
import type { RuleContext, RuleListener, TemplateBodyVisitor } from '../types';
import type { ReportDescriptor } from '@typescript-eslint/utils/ts-eslint';
import type { ReportDescriptor, RuleContext, RuleListener, TemplateBodyVisitor } from '../types';

/**
* Register the given visitor to parser services. from GitHub `vuejs/eslint-plugin-vue` repo
Expand Down
43 changes: 43 additions & 0 deletions tests/rules/no-deprecated-components.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { RuleTester } from 'eslint';
import rule from '../../src/rules/no-deprecated-components';

const vueParser = require.resolve('vue-eslint-parser');

const tester = new RuleTester({
parser: vueParser,
parserOptions: {
ecmaVersion: 2020,
sourceType: 'module',
},
});

tester.run('no-deprecated-components', rule as never, {
valid: [
{
filename: 'test.vue',
code: `<template><Fragment><div></div></Fragment></template>`,
options: [
{
legacy: true,
},
],
},
],
invalid: [
{
filename: 'test.vue',
code: `<template><Fragment><div></div></Fragment></template>`,
output: `<template><div></div></template>`,
errors: [
{ messageId: 'removed' },
],
},
{
filename: 'test.vue',
code: `<template><DataTable><div></div></DataTable></template>`,
errors: [
{ messageId: 'deprecated' },
],
},
],
});

0 comments on commit 00ce936

Please sign in to comment.