Skip to content

Commit

Permalink
feat(eslint-plugin): [component-selector] handle shadow dom component…
Browse files Browse the repository at this point in the history
…s properly (#559)
  • Loading branch information
GavinWu1991 committed Jul 13, 2021
1 parent 149bf2f commit ecbe684
Show file tree
Hide file tree
Showing 3 changed files with 106 additions and 3 deletions.
2 changes: 2 additions & 0 deletions packages/eslint-plugin/docs/rules/component-selector.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ interface Options {

The Angular styleguide recommends setting `type` to `element` and `style` to `kebab-case`, as well as setting at least a prefix.

**Note**: The rule ignores the `style` config for ShadowDom-encapsulated components and just forces it to be `kebab-case`.

## Examples

In the examples below, we will use the following configuration:
Expand Down
43 changes: 40 additions & 3 deletions packages/eslint-plugin/src/rules/component-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,27 @@ import type { SelectorStyle } from '../utils/utils';
import {
arrayify,
getDecoratorPropertyValue,
isMemberExpression,
OPTION_STYLE_CAMEL_CASE,
OPTION_STYLE_KEBAB_CASE,
} from '../utils/utils';
import { ASTUtils } from '@typescript-eslint/experimental-utils';

const VIEW_ENCAPSULATION_SHADOW_DOM = 'ShadowDom';
const VIEW_ENCAPSULATION = 'ViewEncapsulation';
export const RULE_NAME = 'component-selector';
export type MessageIds = 'prefixFailure' | 'styleFailure' | 'typeFailure';
export type MessageIds =
| 'prefixFailure'
| 'styleFailure'
| 'typeFailure'
| 'shadowDomEncapsulatedStyleFailure';
const STYLE_GUIDE_PREFIX_LINK =
'https://angular.io/guide/styleguide#style-02-07';
const STYLE_GUIDE_STYLE_LINK =
'https://angular.io/guide/styleguide#style-05-02';
const STYLE_GUIDE_TYPE_LINK = 'https://angular.io/guide/styleguide#style-05-03';
const SHADOW_DOM_ENCAPSULATED_STYLE_LINK =
'https://github.com/angular-eslint/angular-eslint/issues/534';

export default createESLintRule<Options, MessageIds>({
name: RULE_NAME,
Expand Down Expand Up @@ -67,6 +77,7 @@ export default createESLintRule<Options, MessageIds>({
prefixFailure: `The selector should start with one of these prefixes: {{prefix}} (${STYLE_GUIDE_PREFIX_LINK})`,
styleFailure: `The selector should be {{style}} (${STYLE_GUIDE_STYLE_LINK})`,
typeFailure: `The selector should be used as an {{type}} (${STYLE_GUIDE_TYPE_LINK})`,
shadowDomEncapsulatedStyleFailure: `The selector of a ShadowDom-encapsulated component should be \`${OPTION_STYLE_KEBAB_CASE}\` (${SHADOW_DOM_ENCAPSULATED_STYLE_LINK})`,
},
},
defaultOptions: [
Expand All @@ -91,11 +102,18 @@ export default createESLintRule<Options, MessageIds>({
return;
}

// override `style` for ShadowDom-encapsulated components. See https://github.com/angular-eslint/angular-eslint/issues/534.
const overrideStyle =
style !== OPTION_STYLE_KEBAB_CASE &&
hasEncapsulationShadowDomProperty(node)
? OPTION_STYLE_KEBAB_CASE
: style;

const hasExpectedSelector = checkSelector(
rawSelectors,
type,
arrayify<string>(prefix),
style as SelectorStyle,
overrideStyle as SelectorStyle,
);

if (hasExpectedSelector === null) {
Expand All @@ -105,11 +123,30 @@ export default createESLintRule<Options, MessageIds>({
if (!hasExpectedSelector.hasExpectedType) {
reportTypeError(rawSelectors, type, context);
} else if (!hasExpectedSelector.hasExpectedStyle) {
reportStyleError(rawSelectors, style, context);
if (style === overrideStyle) {
reportStyleError(rawSelectors, style, context);
} else {
context.report({
node: rawSelectors,
messageId: 'shadowDomEncapsulatedStyleFailure',
});
}
} else if (!hasExpectedSelector.hasExpectedPrefix) {
reportPrefixError(rawSelectors, prefix, context);
}
},
};
},
});

function hasEncapsulationShadowDomProperty(node: TSESTree.Decorator) {
const encapsulationValue = getDecoratorPropertyValue(node, 'encapsulation');
return (
encapsulationValue &&
isMemberExpression(encapsulationValue) &&
ASTUtils.isIdentifier(encapsulationValue.object) &&
encapsulationValue.object.name === VIEW_ENCAPSULATION &&
ASTUtils.isIdentifier(encapsulationValue.property) &&
encapsulationValue.property.name === VIEW_ENCAPSULATION_SHADOW_DOM
);
}
64 changes: 64 additions & 0 deletions packages/eslint-plugin/tests/rules/component-selector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ const ruleTester = new RuleTester({
const messageIdPrefixFailure: MessageIds = 'prefixFailure';
const messageIdStyleFailure: MessageIds = 'styleFailure';
const messageIdTypeFailure: MessageIds = 'typeFailure';
const messageIdShadowDomEncapsulatedStyleFailure: MessageIds =
'shadowDomEncapsulatedStyleFailure';

ruleTester.run(RULE_NAME, rule, {
valid: [
Expand Down Expand Up @@ -186,6 +188,40 @@ ruleTester.run(RULE_NAME, rule, {
},
],
},
{
// https://github.com/angular-eslint/angular-eslint/issues/534
code: `
@Component({
selector: \`app-foo-bar\`,
encapsulation: ViewEncapsulation.ShadowDom
})
class Test {}
`,
options: [
{
type: ['element'],
prefix: ['app'],
style: 'camelCase',
},
],
},
{
// https://github.com/angular-eslint/angular-eslint/issues/534
code: `
@Component({
selector: \`app-foo-bar\`,
encapsulation: ViewEncapsulation.ShadowDom
})
class Test {}
`,
options: [
{
type: ['element'],
prefix: ['app'],
style: 'kebab-case',
},
],
},
{
code: `
@Directive({
Expand Down Expand Up @@ -341,5 +377,33 @@ ruleTester.run(RULE_NAME, rule, {
],
data: { type: 'attribute' },
}),
convertAnnotatedSourceToFailureCase({
// https://github.com/angular-eslint/angular-eslint/issues/534
description: `it should fail if a ShadowDom-encapsulated component's selector is not kebab-cased`,
annotatedSource: `
@Component({
encapsulation: ViewEncapsulation.ShadowDom,
selector: 'appFooBar'
~~~~~~~~~~~
})
class Test {}
`,
messageId: messageIdShadowDomEncapsulatedStyleFailure,
options: [{ type: 'element', prefix: ['app'], style: 'camelCase' }],
}),
convertAnnotatedSourceToFailureCase({
// https://github.com/angular-eslint/angular-eslint/issues/534
description: `it should fail if a ShadowDom-encapsulated component's selector doesn't contain hyphen`,
annotatedSource: `
@Component({
encapsulation: ViewEncapsulation.ShadowDom,
selector: 'app'
~~~~~
})
class Test {}
`,
messageId: messageIdShadowDomEncapsulatedStyleFailure,
options: [{ type: 'element', prefix: ['app'], style: 'camelCase' }],
}),
],
});

0 comments on commit ecbe684

Please sign in to comment.