Skip to content

Commit 9fb7b66

Browse files
authored
fix: allow decorated properties with class fields (#201)
Allows properties decorated with lit property decorators to have class fields alongside. This loosens the strictness of the rule under the assumption you have set `useDefineForClassFields: false` in typescript. If you use `declare` or `accessor`, those fields will already be ignored by this rule. Examples: ```ts // Error class X extends LitElement { fieldA; static properties = {fieldA: {type: String}}; } // Works now, errored before class X extends LitElement { @Property() fieldA; } // Worked before, works now class X extends LitElement { @Property() declare fieldA; } // Worked before, works now class X extends LitElement { declare fieldA; static properties = {fieldA: {type: String}}; } // Worked before, works now class X extends LitElement { @Property() accessor fieldA; } ``` Fixes #193.
1 parent ab2fb54 commit 9fb7b66

File tree

8 files changed

+608
-411
lines changed

8 files changed

+608
-411
lines changed

docs/rules/no-classfield-shadowing.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
# Disallows class fields with same name as static properties
1+
# Disallows class fields with same name as reactive properties
22

3-
Class fields set with same names as static properties will not trigger updates as expected. They will overwrite
3+
Class fields set with same names as reactive properties will not trigger updates as expected. They will overwrite
44
accessors used for detecting changes. See https://lit.dev/msg/class-field-shadowing for more information.
55

66
## Rule Details
77

8-
This rule disallows class fields with same name as static properties.
8+
This rule disallows class fields with same name as reactive properties.
99

1010
The following patterns are considered warnings:
1111

package-lock.json

Lines changed: 517 additions & 394 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,8 +49,8 @@
4949
"eslint": ">= 5"
5050
},
5151
"devDependencies": {
52-
"@babel/eslint-parser": "^7.18.9",
53-
"@babel/plugin-proposal-decorators": "^7.18.10",
52+
"@babel/eslint-parser": "^7.24.5",
53+
"@babel/plugin-proposal-decorators": "^7.24.1",
5454
"@types/chai": "^4.2.16",
5555
"@types/eslint": "^8.4.6",
5656
"@types/estree": "^1.0.0",

src/rules/no-classfield-shadowing.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55

66
import {Rule} from 'eslint';
77
import * as ESTree from 'estree';
8-
import {getClassFields, getPropertyMap, isLitClass} from '../util';
8+
import {
9+
getClassFields,
10+
getPropertyMap,
11+
isLitClass,
12+
hasLitPropertyDecorator
13+
} from '../util';
914

1015
//------------------------------------------------------------------------------
1116
// Rule Definition
@@ -22,7 +27,7 @@ const rule: Rule.RuleModule = {
2227
messages: {
2328
noClassfieldShadowing:
2429
'The {{ prop }} property is a class field which has the same name as ' +
25-
'static property which could have unintended side-effects.'
30+
'a reactive property which could have unintended side-effects.'
2631
}
2732
},
2833

@@ -34,7 +39,8 @@ const rule: Rule.RuleModule = {
3439
const classMembers = getClassFields(node);
3540

3641
for (const [prop, {key}] of propertyMap.entries()) {
37-
if (classMembers.has(prop)) {
42+
const member = classMembers.get(prop);
43+
if (member && !hasLitPropertyDecorator(member)) {
3844
context.report({
3945
node: key,
4046
messageId: 'noClassfieldShadowing',

src/test/rules/attribute-names_test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,7 @@ const parser = require.resolve('@babel/eslint-parser');
1717
const parserOptions = {
1818
requireConfigFile: false,
1919
babelOptions: {
20-
plugins: [
21-
['@babel/plugin-proposal-decorators', {decoratorsBeforeExport: true}]
22-
]
20+
plugins: [['@babel/plugin-proposal-decorators', {version: '2023-11'}]]
2321
}
2422
};
2523

src/test/rules/no-classfield-shadowing_test.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@ const ruleTester = new RuleTester({
2121
}
2222
});
2323

24+
const parser = require.resolve('@babel/eslint-parser');
25+
const parserOptions = {
26+
requireConfigFile: false,
27+
babelOptions: {
28+
plugins: [
29+
[
30+
'@babel/plugin-proposal-decorators',
31+
{
32+
version: '2023-11'
33+
}
34+
]
35+
]
36+
}
37+
};
38+
39+
const tsParser = require.resolve('@typescript-eslint/parser');
40+
2441
ruleTester.run('no-classfield-shadowing', rule, {
2542
valid: [
2643
`class MyElement extends LitElement {
@@ -39,7 +56,32 @@ ruleTester.run('no-classfield-shadowing', rule, {
3956
properties = {
4057
foo: { type: String }
4158
}
42-
}`
59+
}`,
60+
{
61+
code: `class MyElement extends LitElement {
62+
@property()
63+
foo;
64+
}`,
65+
parser,
66+
parserOptions
67+
},
68+
{
69+
code: `class MyElement extends LitElement {
70+
@property()
71+
accessor foo;
72+
}`,
73+
parser,
74+
parserOptions
75+
},
76+
{
77+
code: `class MyElement extends LitElement {
78+
declare foo;
79+
static properties = {
80+
foo: { type: String }
81+
};
82+
}`,
83+
parser: tsParser
84+
}
4385
],
4486

4587
invalid: [

src/test/rules/no-property-change-update_test.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,9 +25,7 @@ const parser = require.resolve('@babel/eslint-parser');
2525
const parserOptions = {
2626
requireConfigFile: false,
2727
babelOptions: {
28-
plugins: [
29-
['@babel/plugin-proposal-decorators', {decoratorsBeforeExport: true}]
30-
]
28+
plugins: [['@babel/plugin-proposal-decorators', {version: '2023-11'}]]
3129
}
3230
};
3331

src/util.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ export interface BabelProperty extends ESTree.MethodDefinition {
99
decorators?: BabelDecorator[];
1010
}
1111

12+
export type DecoratedNode = ESTree.Node & {
13+
decorators?: BabelDecorator[];
14+
};
15+
1216
/**
1317
* Returns if given node has a lit identifier
1418
* @param {ESTree.Node} node
@@ -139,6 +143,9 @@ export function getClassFields(
139143
return result;
140144
}
141145

146+
const propertyDecorators = ['state', 'property', 'internalProperty'];
147+
const internalDecorators = ['state', 'internalProperty'];
148+
142149
/**
143150
* Get the properties object of an element class
144151
*
@@ -149,8 +156,6 @@ export function getPropertyMap(
149156
node: ESTree.Class
150157
): ReadonlyMap<string, PropertyMapEntry> {
151158
const result = new Map<string, PropertyMapEntry>();
152-
const propertyDecorators = ['state', 'property', 'internalProperty'];
153-
const internalDecorators = ['state', 'internalProperty'];
154159

155160
for (const member of node.body.body) {
156161
if (
@@ -243,6 +248,31 @@ export function getPropertyMap(
243248
return result;
244249
}
245250

251+
/**
252+
* Determines if a node has a lit property decorator
253+
* @param {ESTree.Node} node Node to test
254+
* @return {boolean}
255+
*/
256+
export function hasLitPropertyDecorator(node: ESTree.Node): boolean {
257+
const decoratedNode = node as DecoratedNode;
258+
259+
if (!decoratedNode.decorators || !Array.isArray(decoratedNode.decorators)) {
260+
return false;
261+
}
262+
263+
for (const decorator of decoratedNode.decorators) {
264+
if (
265+
decorator.expression.type === 'CallExpression' &&
266+
decorator.expression.callee.type === 'Identifier' &&
267+
propertyDecorators.includes(decorator.expression.callee.name)
268+
) {
269+
return true;
270+
}
271+
}
272+
273+
return false;
274+
}
275+
246276
/**
247277
* Generates a placeholder string for a given quasi
248278
*

0 commit comments

Comments
 (0)