Skip to content

Commit

Permalink
feat: improve LitElement identification (#206)
Browse files Browse the repository at this point in the history
  • Loading branch information
jpradelle authored Aug 29, 2024
1 parent 427627c commit 50f05d2
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 16 deletions.
7 changes: 2 additions & 5 deletions src/rules/lifecycle-super.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import {Rule} from 'eslint';
import * as ESTree from 'estree';
import {isLitClass} from '../util';

const methodNames = ['connectedCallback', 'disconnectedCallback', 'update'];

Expand Down Expand Up @@ -44,11 +45,7 @@ const rule: Rule.RuleModule = {
* @return {void}
*/
function classEnter(node: ESTree.Class): void {
if (
!node.superClass ||
node.superClass.type !== 'Identifier' ||
node.superClass.name !== 'LitElement'
) {
if (!isLitClass(node)) {
return;
}

Expand Down
8 changes: 2 additions & 6 deletions src/rules/no-property-change-update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

import {Rule} from 'eslint';
import * as ESTree from 'estree';
import {getPropertyMap, PropertyMapEntry} from '../util';
import {getPropertyMap, isLitClass, PropertyMapEntry} from '../util';

const superUpdateQuery =
'CallExpression' +
Expand Down Expand Up @@ -49,11 +49,7 @@ const rule: Rule.RuleModule = {
* @return {void}
*/
function classEnter(node: ESTree.Class): void {
if (
!node.superClass ||
node.superClass.type !== 'Identifier' ||
node.superClass.name !== 'LitElement'
) {
if (!isLitClass(node)) {
return;
}

Expand Down
7 changes: 2 additions & 5 deletions src/rules/no-this-assign-in-render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import {Rule} from 'eslint';
import * as ESTree from 'estree';
import {isLitClass} from '../util';

//------------------------------------------------------------------------------
// Rule Definition
Expand Down Expand Up @@ -38,11 +39,7 @@ const rule: Rule.RuleModule = {
* @return {void}
*/
function classEnter(node: ESTree.Class): void {
if (
!node.superClass ||
node.superClass.type !== 'Identifier' ||
node.superClass.name !== 'LitElement'
) {
if (!isLitClass(node)) {
return;
}

Expand Down
34 changes: 34 additions & 0 deletions src/test/rules/attribute-names_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -125,6 +125,40 @@ ruleTester.run('attribute-names', rule, {
messageId: 'casedPropertyWithoutAttribute'
}
]
},
{
code: `@customElement('foo-bar')
class FooBar extends FooElement {
@property({ type: String })
camelCase = 'foo';
}`,
parser,
parserOptions,
errors: [
{
line: 4,
column: 9,
messageId: 'casedPropertyWithoutAttribute'
}
]
},
{
code: `@foo
@customElement('foo-bar')
@bar
class FooBar extends FooElement {
@property({ type: String })
camelCase = 'foo';
}`,
parser,
parserOptions,
errors: [
{
line: 6,
column: 9,
messageId: 'casedPropertyWithoutAttribute'
}
]
}
]
});
26 changes: 26 additions & 0 deletions src/test/rules/lifecycle-super_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const ruleTester = new RuleTester({
}
});

const parser = require.resolve('@babel/eslint-parser');
const parserOptions = {
requireConfigFile: false,
babelOptions: {
plugins: [['@babel/plugin-proposal-decorators', {version: '2023-11'}]]
}
};

ruleTester.run('lifecycle-super', rule, {
valid: [
'class Foo { }',
Expand Down Expand Up @@ -184,6 +192,24 @@ ruleTester.run('lifecycle-super', rule, {
column: 9
}
]
},
{
code: `@customElement('foo')
class Foo extends FooElement {
connectedCallback() {
super.foo.connectedCallback();
}
}`,
parser,
parserOptions,
errors: [
{
messageId: 'callSuper',
data: {method: 'connectedCallback'},
line: 3,
column: 9
}
]
}
]
});
21 changes: 21 additions & 0 deletions src/test/rules/no-property-change-update_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,27 @@ ruleTester.run('no-property-change-update', rule, {
column: 11
}
]
},
{
code: `@customElement('foo')
class Foo extends FooElement {
static get properties() {
return { prop: { type: String } };
}
update(change) {
super.update();
this.prop = 'foo';
}
}`,
parser,
parserOptions,
errors: [
{
messageId: 'propertyChange',
line: 8,
column: 11
}
]
}
]
});
25 changes: 25 additions & 0 deletions src/test/rules/no-this-assign-in-render_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ const ruleTester = new RuleTester({
}
});

const parser = require.resolve('@babel/eslint-parser');
const parserOptions = {
requireConfigFile: false,
babelOptions: {
plugins: [['@babel/plugin-proposal-decorators', {version: '2023-11'}]]
}
};

ruleTester.run('no-this-assign-in-render', rule, {
valid: [
'const x = 808;',
Expand Down Expand Up @@ -126,6 +134,23 @@ ruleTester.run('no-this-assign-in-render', rule, {
column: 11
}
]
},
{
code: `@customElement('foo')
class Foo extends FooElement {
render() {
this['prop'] = 'foo';
}
}`,
parser,
parserOptions,
errors: [
{
messageId: 'noThis',
line: 4,
column: 11
}
]
}
]
});
28 changes: 28 additions & 0 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,31 @@ export type DecoratedNode = ESTree.Node & {
decorators?: BabelDecorator[];
};

/**
* Returns if given node has a customElement decorator
* @param {ESTree.Class} node
* @return {boolean}
*/
function hasCustomElementDecorator(node: ESTree.Class): boolean {
const decoratedNode = node as DecoratedNode;

if (!decoratedNode.decorators || !Array.isArray(decoratedNode.decorators)) {
return false;
}

for (const decorator of decoratedNode.decorators) {
if (
decorator.expression.type === 'CallExpression' &&
decorator.expression.callee.type === 'Identifier' &&
decorator.expression.callee.name === 'customElement'
) {
return true;
}
}

return false;
}

/**
* Returns if given node has a lit identifier
* @param {ESTree.Node} node
Expand Down Expand Up @@ -45,6 +70,9 @@ function isLitByExpression(node: ESTree.Node): boolean {
* @return { boolean }
*/
export function isLitClass(clazz: ESTree.Class): boolean {
if (hasCustomElementDecorator(clazz)) {
return true;
}
if (clazz.superClass) {
return (
hasLitIdentifier(clazz.superClass) || isLitByExpression(clazz.superClass)
Expand Down

0 comments on commit 50f05d2

Please sign in to comment.