Skip to content

Commit

Permalink
feat(eslint-plugin): [use-component-view-encapsulation] add suggestion (
Browse files Browse the repository at this point in the history
  • Loading branch information
rafaelss95 committed Jun 12, 2021
1 parent 0fd16e3 commit ea9e98d
Show file tree
Hide file tree
Showing 3 changed files with 147 additions and 35 deletions.
Expand Up @@ -2,52 +2,70 @@ import type { TSESTree } from '@typescript-eslint/experimental-utils';
import { createESLintRule } from '../utils/create-eslint-rule';
import { COMPONENT_CLASS_DECORATOR } from '../utils/selectors';
import {
getDecoratorPropertyValue,
isIdentifier,
isMemberExpression,
getImportDeclarations,
getImportRemoveFix,
getNodeToCommaRemoveFix,
isNotNullOrUndefined,
} from '../utils/utils';

type Options = [];
export type MessageIds = 'useComponentViewEncapsulation';
export type MessageIds =
| 'useComponentViewEncapsulation'
| 'suggestRemoveViewEncapsulationNone';
export const RULE_NAME = 'use-component-view-encapsulation';

const NONE = 'None';
const VIEW_ENCAPSULATION_NONE = 'ViewEncapsulation.None';

export default createESLintRule<Options, MessageIds>({
name: RULE_NAME,
meta: {
type: 'suggestion',
docs: {
description: `Disallows using ViewEncapsulation.${NONE}`,
description: `Disallows using \`${VIEW_ENCAPSULATION_NONE}\``,
category: 'Best Practices',
recommended: false,
},
schema: [],
messages: {
useComponentViewEncapsulation: `Using ViewEncapsulation.${NONE} makes your styles global, which may have an unintended effect`,
useComponentViewEncapsulation: `Using \`${VIEW_ENCAPSULATION_NONE}\` makes your styles global, which may have an unintended effect`,
suggestRemoveViewEncapsulationNone: `Remove \`${VIEW_ENCAPSULATION_NONE}\``,
},
},
defaultOptions: [],
create(context) {
return {
[COMPONENT_CLASS_DECORATOR](node: TSESTree.Decorator) {
const encapsulationExpression = getDecoratorPropertyValue(
node,
'encapsulation',
);

if (
!encapsulationExpression ||
(isMemberExpression(encapsulationExpression) &&
isIdentifier(encapsulationExpression.property) &&
encapsulationExpression.property.name !== NONE)
) {
return;
}
const sourceCode = context.getSourceCode();

return {
[`${COMPONENT_CLASS_DECORATOR} Property[key.name='encapsulation'] > MemberExpression[object.name='ViewEncapsulation'] > Identifier[name='None']`](
node: TSESTree.Identifier & {
parent: TSESTree.MemberExpression & { parent: TSESTree.Property };
},
) {
context.report({
node: encapsulationExpression,
node,
messageId: 'useComponentViewEncapsulation',
suggest: [
{
messageId: 'suggestRemoveViewEncapsulationNone',
fix: (fixer) => {
const importDeclarations =
getImportDeclarations(node, '@angular/core') ?? [];

return [
getNodeToCommaRemoveFix(
sourceCode,
node.parent.parent,
fixer,
),
getImportRemoveFix(
sourceCode,
importDeclarations,
'ViewEncapsulation',
fixer,
),
].filter(isNotNullOrUndefined);
},
},
],
});
},
};
Expand Down
13 changes: 13 additions & 0 deletions packages/eslint-plugin/src/utils/utils.ts
@@ -1,4 +1,5 @@
import type { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';
import { ASTUtils } from '@typescript-eslint/experimental-utils';

export const objectKeys = Object.keys as <T>(
o: T,
Expand Down Expand Up @@ -360,6 +361,18 @@ export function getImplementsRemoveFix(
]);
}

export function getNodeToCommaRemoveFix(
sourceCode: Readonly<TSESLint.SourceCode>,
node: TSESTree.Node,
fixer: TSESLint.RuleFixer,
): TSESLint.RuleFix {
const tokenAfterNode = sourceCode.getTokenAfter(node);

return tokenAfterNode && ASTUtils.isCommaToken(tokenAfterNode)
? fixer.removeRange([node.range[0], tokenAfterNode.range[1]])
: fixer.remove(node);
}

function getImportDeclarationSpecifier(
importDeclarations: readonly TSESTree.ImportDeclaration[],
importedName: string,
Expand Down
Expand Up @@ -14,8 +14,9 @@ import rule, {
const ruleTester = new RuleTester({
parser: '@typescript-eslint/parser',
});

const messageId: MessageIds = 'useComponentViewEncapsulation';
const suggestRemoveViewEncapsulationNone: MessageIds =
'suggestRemoveViewEncapsulationNone';

ruleTester.run(RULE_NAME, rule, {
valid: [
Expand All @@ -25,46 +26,126 @@ ruleTester.run(RULE_NAME, rule, {
selector: 'app-foo-bar'
})
export class Test {}
`,
`,
`
@Component({
encapsulation: ViewEncapsulation.Native,
selector: 'app-foo-bar'
selector: 'app-foo-bar',
})
export class Test {}
`,
`,
`
@Component({
encapsulation: ViewEncapsulation.ShadowDom,
selector: 'app-foo-bar'
})
export class Test {}
`,
`,
`
function encapsulation() {
return ViewEncapsulation.None;
}
@Component({
selector: 'app-foo-bar'
encapsulation: encapsulation()
})
export class Test {}
`,
`
const encapsulation = 'templateUrl';
@Component({
[encapsulation]: '../a.html'
})
export class Test {}
`,
`
const encapsulation = 'templateUrl';
@Component({
encapsulation
})
export class Test {}
`,
`
const test = 'test';
@Component({
encapsulation: test,
})
export class Test {}
`,
`,
`
@Component({
encapsulation: undefined,
})
export class Test {}
`,
`
const options = {};
@Component(options)
export class Test {}
`,
`
@NgModule({
bootstrap: [Foo]
})
export class Test {}
`,
`,
],
invalid: [
convertAnnotatedSourceToFailureCase({
description: 'it should fail if ViewEncapsulation.None is set',
description: 'it should fail if `ViewEncapsulation.None` is set',
annotatedSource: `
@Component({
encapsulation: ViewEncapsulation.None,
~~~~~~~~~~~~~~~~~~~~~~
~~~~
selector: 'app-foo-bar',
})
export class Test {}
`,
messageId,
suggestions: [
{
messageId: suggestRemoveViewEncapsulationNone,
output: `
@Component({
selector: 'app-foo-bar',
})
export class Test {}
`,
},
],
}),
convertAnnotatedSourceToFailureCase({
description:
'it should fail if `ViewEncapsulation.None` is set and the suggestions should remove it along with its import',
annotatedSource: `
import { ViewEncapsulation } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-foo-bar',
encapsulation: ViewEncapsulation.None
~~~~
})
export class Test {}
`,
messageId,
suggestions: [
{
messageId: suggestRemoveViewEncapsulationNone,
output: `
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-foo-bar',
})
export class Test {}
`,
},
],
}),
],
});

0 comments on commit ea9e98d

Please sign in to comment.