eslint-plugin-lit 2.2.1 → 2.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/docs/rules/prefer-query-decorators.md +86 -0
- package/lib/configs/all.js +1 -0
- package/lib/configs/legacy-all.js +1 -0
- package/lib/index.d.ts +4 -4
- package/lib/index.js +2 -0
- package/lib/rules/attribute-names.js +6 -1
- package/lib/rules/attribute-value-entities.js +0 -1
- package/lib/rules/ban-attributes.js +2 -2
- package/lib/rules/no-legacy-imports.js +2 -1
- package/lib/rules/no-legacy-template-syntax.js +0 -1
- package/lib/rules/no-private-properties.js +3 -4
- package/lib/rules/no-this-assign-in-render.js +1 -1
- package/lib/rules/no-useless-template-literals.js +1 -2
- package/lib/rules/prefer-query-decorators.d.ts +6 -0
- package/lib/rules/prefer-query-decorators.js +229 -0
- package/lib/rules/prefer-static-styles.js +2 -1
- package/lib/rules/quoted-expressions.js +2 -1
- package/lib/rules/value-after-constraints.js +2 -1
- package/lib/template-analyzer.d.ts +16 -16
- package/lib/template-analyzer.js +8 -6
- package/lib/util.d.ts +10 -0
- package/lib/util.js +4 -4
- package/package.json +8 -11
package/README.md
CHANGED
|
@@ -105,6 +105,7 @@ If you want more fine-grained configuration, you can instead add a snippet like
|
|
|
105
105
|
- [lit/no-useless-template-literals](docs/rules/no-useless-template-literals.md)
|
|
106
106
|
- [lit/no-value-attribute](docs/rules/no-value-attribute.md)
|
|
107
107
|
- [lit/prefer-nothing](docs/rules/prefer-nothing.md)
|
|
108
|
+
- [lit/prefer-query-decorators](docs/rules/prefer-query-decorators.md)
|
|
108
109
|
- [lit/quoted-expressions](docs/rules/quoted-expressions.md)
|
|
109
110
|
- [lit/value-after-constraints](docs/rules/value-after-constraints.md)
|
|
110
111
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# Requires use of query decorators instead of manual DOM queries (prefer-query-decorators)
|
|
2
|
+
|
|
3
|
+
Manually calling `this.shadowRoot.querySelector()` or `this.renderRoot.querySelector()` inside a
|
|
4
|
+
LitElement is verbose and error-prone. The `@query`, `@queryAll`, `@queryAssignedElements`, and
|
|
5
|
+
`@queryAssignedNodes` decorators are the idiomatic Lit way to access elements in the shadow DOM.
|
|
6
|
+
|
|
7
|
+
## Rule Details
|
|
8
|
+
|
|
9
|
+
This rule requires use of query decorators instead of manually querying the render root.
|
|
10
|
+
|
|
11
|
+
The following patterns are considered warnings:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
class Foo extends LitElement {
|
|
15
|
+
get myEl() {
|
|
16
|
+
return this.shadowRoot.querySelector('.my-el');
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get myEls() {
|
|
20
|
+
return this.renderRoot.querySelectorAll('.my-el');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
get assignedItems() {
|
|
24
|
+
return this.shadowRoot.querySelector('slot').assignedElements();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
get assignedItemNodes() {
|
|
28
|
+
return this.renderRoot.querySelector('slot').assignedNodes();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The following patterns are not warnings:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
class Foo extends LitElement {
|
|
37
|
+
@query('.my-el') myEl;
|
|
38
|
+
|
|
39
|
+
@queryAll('.my-el') myEls;
|
|
40
|
+
|
|
41
|
+
@queryAssignedElements({slot: 'items'}) assignedItems;
|
|
42
|
+
|
|
43
|
+
@queryAssignedNodes({slot: 'items'}) assignedItemNodes;
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Options
|
|
48
|
+
|
|
49
|
+
The rule accepts an optional configuration object with boolean flags to selectively disable
|
|
50
|
+
individual checks. All flags default to `true`.
|
|
51
|
+
|
|
52
|
+
```json
|
|
53
|
+
{
|
|
54
|
+
"lit/prefer-query-decorators": [
|
|
55
|
+
"error",
|
|
56
|
+
{
|
|
57
|
+
"querySelector": true,
|
|
58
|
+
"querySelectorAll": true,
|
|
59
|
+
"assignedElements": true,
|
|
60
|
+
"assignedNodes": true
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
### querySelector
|
|
67
|
+
|
|
68
|
+
When `false`, disables the check for `this.shadowRoot.querySelector()` /
|
|
69
|
+
`this.renderRoot.querySelector()`.
|
|
70
|
+
|
|
71
|
+
### querySelectorAll
|
|
72
|
+
|
|
73
|
+
When `false`, disables the check for `this.shadowRoot.querySelectorAll()` /
|
|
74
|
+
`this.renderRoot.querySelectorAll()`.
|
|
75
|
+
|
|
76
|
+
### assignedElements
|
|
77
|
+
|
|
78
|
+
When `false`, disables the check for chained `.assignedElements()` calls.
|
|
79
|
+
|
|
80
|
+
### assignedNodes
|
|
81
|
+
|
|
82
|
+
When `false`, disables the check for chained `.assignedNodes()` calls.
|
|
83
|
+
|
|
84
|
+
## When Not To Use It
|
|
85
|
+
|
|
86
|
+
If you prefer to query the shadow DOM manually, or your project does not use Lit decorators.
|
package/lib/configs/all.js
CHANGED
|
@@ -24,6 +24,7 @@ export const configFactory = (plugin) => ({
|
|
|
24
24
|
'lit/no-useless-template-literals': 'error',
|
|
25
25
|
'lit/no-value-attribute': 'error',
|
|
26
26
|
'lit/prefer-nothing': 'error',
|
|
27
|
+
'lit/prefer-query-decorators': 'error',
|
|
27
28
|
'lit/prefer-static-styles': 'error',
|
|
28
29
|
'lit/quoted-expressions': 'error',
|
|
29
30
|
'lit/value-after-constraints': 'error'
|
|
@@ -22,6 +22,7 @@ export const config = {
|
|
|
22
22
|
'lit/no-useless-template-literals': 'error',
|
|
23
23
|
'lit/no-value-attribute': 'error',
|
|
24
24
|
'lit/prefer-nothing': 'error',
|
|
25
|
+
'lit/prefer-query-decorators': 'error',
|
|
25
26
|
'lit/prefer-static-styles': 'error',
|
|
26
27
|
'lit/quoted-expressions': 'error',
|
|
27
28
|
'lit/value-after-constraints': 'error'
|
package/lib/index.d.ts
CHANGED
|
@@ -2,9 +2,9 @@ import type { Rule, ESLint } from 'eslint';
|
|
|
2
2
|
export declare const rules: Record<string, Rule.RuleModule>;
|
|
3
3
|
declare const plugin: ESLint.Plugin;
|
|
4
4
|
export declare const configs: {
|
|
5
|
-
all: ESLint.ConfigData<import("eslint").
|
|
6
|
-
'flat/all': import("eslint").Linter.FlatConfig
|
|
7
|
-
recommended: ESLint.ConfigData<import("eslint").
|
|
8
|
-
'flat/recommended': import("eslint").Linter.FlatConfig
|
|
5
|
+
all: ESLint.ConfigData<import("@eslint/core", { with: { "resolution-mode": "require" } }).RulesConfig>;
|
|
6
|
+
'flat/all': import("eslint").Linter.FlatConfig<import("@eslint/core", { with: { "resolution-mode": "require" } }).RulesConfig>;
|
|
7
|
+
recommended: ESLint.ConfigData<import("@eslint/core", { with: { "resolution-mode": "require" } }).RulesConfig>;
|
|
8
|
+
'flat/recommended': import("eslint").Linter.FlatConfig<import("@eslint/core", { with: { "resolution-mode": "require" } }).RulesConfig>;
|
|
9
9
|
};
|
|
10
10
|
export default plugin;
|
package/lib/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { rule as ruleNoThisAssign } from './rules/no-this-assign-in-render.js';
|
|
|
23
23
|
import { rule as ruleNoUselessTemplateLiterals } from './rules/no-useless-template-literals.js';
|
|
24
24
|
import { rule as ruleNoValueAttribute } from './rules/no-value-attribute.js';
|
|
25
25
|
import { rule as rulePreferNothing } from './rules/prefer-nothing.js';
|
|
26
|
+
import { rule as rulePreferQueryDecorators } from './rules/prefer-query-decorators.js';
|
|
26
27
|
import { rule as rulePreferStaticStyles } from './rules/prefer-static-styles.js';
|
|
27
28
|
import { rule as ruleQuotedExpressions } from './rules/quoted-expressions.js';
|
|
28
29
|
import { rule as ruleValueAfterConstraints } from './rules/value-after-constraints.js';
|
|
@@ -48,6 +49,7 @@ export const rules = {
|
|
|
48
49
|
'no-useless-template-literals': ruleNoUselessTemplateLiterals,
|
|
49
50
|
'no-value-attribute': ruleNoValueAttribute,
|
|
50
51
|
'prefer-nothing': rulePreferNothing,
|
|
52
|
+
'prefer-query-decorators': rulePreferQueryDecorators,
|
|
51
53
|
'prefer-static-styles': rulePreferStaticStyles,
|
|
52
54
|
'quoted-expressions': ruleQuotedExpressions,
|
|
53
55
|
'value-after-constraints': ruleValueAfterConstraints
|
|
@@ -30,7 +30,12 @@ export const rule = {
|
|
|
30
30
|
'name (usually snake-case)',
|
|
31
31
|
casedAttributeConvention: 'Attribute should be property name written in {{convention}} ' +
|
|
32
32
|
'as "{{name}}"'
|
|
33
|
-
}
|
|
33
|
+
},
|
|
34
|
+
defaultOptions: [
|
|
35
|
+
{
|
|
36
|
+
convention: 'none'
|
|
37
|
+
}
|
|
38
|
+
]
|
|
34
39
|
},
|
|
35
40
|
create(context) {
|
|
36
41
|
var _a, _b;
|
|
@@ -41,7 +41,6 @@ export const rule = {
|
|
|
41
41
|
analyzer.traverse({
|
|
42
42
|
enterElement: (element) => {
|
|
43
43
|
var _a, _b, _c, _d;
|
|
44
|
-
// eslint-disable-next-line guard-for-in
|
|
45
44
|
for (const attr in element.attribs) {
|
|
46
45
|
const loc = analyzer.getLocationForAttribute(element, attr, source);
|
|
47
46
|
const rawValue = analyzer.getRawAttributeValue(element, attr);
|
|
@@ -22,7 +22,8 @@ export const rule = {
|
|
|
22
22
|
},
|
|
23
23
|
messages: {
|
|
24
24
|
denied: 'The attribute "{{ attr }}" is not allowed in templates'
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: []
|
|
26
27
|
},
|
|
27
28
|
create(context) {
|
|
28
29
|
const source = context.sourceCode;
|
|
@@ -36,7 +37,6 @@ export const rule = {
|
|
|
36
37
|
const analyzer = TemplateAnalyzer.create(node);
|
|
37
38
|
analyzer.traverse({
|
|
38
39
|
enterElement: (element) => {
|
|
39
|
-
// eslint-disable-next-line guard-for-in
|
|
40
40
|
for (const attr in element.attribs) {
|
|
41
41
|
let attrNormalised = attr.toLowerCase();
|
|
42
42
|
if (attrNormalised.startsWith('?')) {
|
|
@@ -51,7 +51,8 @@ export const rule = {
|
|
|
51
51
|
ImportDeclaration: (node) => {
|
|
52
52
|
if (node.source.value === 'lit-element') {
|
|
53
53
|
for (const specifier of node.specifiers) {
|
|
54
|
-
if (specifier.type === 'ImportSpecifier'
|
|
54
|
+
if (specifier.type === 'ImportSpecifier' &&
|
|
55
|
+
specifier.imported.type === 'Identifier') {
|
|
55
56
|
const replacement = legacyDecorators[specifier.imported.name];
|
|
56
57
|
if (replacement) {
|
|
57
58
|
context.report({
|
|
@@ -36,7 +36,6 @@ export const rule = {
|
|
|
36
36
|
const analyzer = TemplateAnalyzer.create(node);
|
|
37
37
|
analyzer.traverse({
|
|
38
38
|
enterElement: (element) => {
|
|
39
|
-
// eslint-disable-next-line guard-for-in
|
|
40
39
|
for (const attr in element.attribs) {
|
|
41
40
|
const loc = analyzer.getLocationForAttribute(element, attr, source);
|
|
42
41
|
if (!loc) {
|
|
@@ -20,13 +20,13 @@ export const rule = {
|
|
|
20
20
|
private: { type: 'string', minLength: 1, format: 'regex' },
|
|
21
21
|
protected: { type: 'string', minLength: 1, format: 'regex' }
|
|
22
22
|
},
|
|
23
|
-
additionalProperties: false
|
|
24
|
-
minProperties: 1
|
|
23
|
+
additionalProperties: false
|
|
25
24
|
}
|
|
26
25
|
],
|
|
27
26
|
messages: {
|
|
28
27
|
noPrivate: 'Private and protected properties should not be assigned in bindings'
|
|
29
|
-
}
|
|
28
|
+
},
|
|
29
|
+
defaultOptions: [{}]
|
|
30
30
|
},
|
|
31
31
|
create(context) {
|
|
32
32
|
const source = context.sourceCode;
|
|
@@ -52,7 +52,6 @@ export const rule = {
|
|
|
52
52
|
const analyzer = TemplateAnalyzer.create(node);
|
|
53
53
|
analyzer.traverse({
|
|
54
54
|
enterElement: (element) => {
|
|
55
|
-
// eslint-disable-next-line guard-for-in
|
|
56
55
|
for (const attr in element.attribs) {
|
|
57
56
|
const loc = analyzer.getLocationForAttribute(element, attr, source);
|
|
58
57
|
if (!loc) {
|
|
@@ -23,7 +23,7 @@ export const rule = {
|
|
|
23
23
|
},
|
|
24
24
|
create(context) {
|
|
25
25
|
const source = context.sourceCode;
|
|
26
|
-
const isAttr = /^[
|
|
26
|
+
const isAttr = /^[^.?]/;
|
|
27
27
|
const endsWithAttr = /=['"]?$/;
|
|
28
28
|
//----------------------------------------------------------------------
|
|
29
29
|
// Helpers
|
|
@@ -62,7 +62,6 @@ export const rule = {
|
|
|
62
62
|
}
|
|
63
63
|
analyzer.traverse({
|
|
64
64
|
enterElement: (element) => {
|
|
65
|
-
// eslint-disable-next-line guard-for-in
|
|
66
65
|
for (const attr in element.attribs) {
|
|
67
66
|
const loc = analyzer.getLocationForAttribute(element, attr, source);
|
|
68
67
|
if (!loc) {
|
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Requires use of query decorators instead of manual DOM queries
|
|
3
|
+
* @author Kirill Karpov <https://github.com/null0rUndefined>
|
|
4
|
+
*/
|
|
5
|
+
import { isLitClass } from '../util.js';
|
|
6
|
+
//------------------------------------------------------------------------------
|
|
7
|
+
// Selectors
|
|
8
|
+
//------------------------------------------------------------------------------
|
|
9
|
+
const querySelectorCall = 'CallExpression' +
|
|
10
|
+
'[callee.type="MemberExpression"]' +
|
|
11
|
+
'[callee.object.type="MemberExpression"]' +
|
|
12
|
+
'[callee.object.object.type="ThisExpression"]' +
|
|
13
|
+
':matches(' +
|
|
14
|
+
'[callee.object.property.name="shadowRoot"],' +
|
|
15
|
+
'[callee.object.property.name="renderRoot"]' +
|
|
16
|
+
')' +
|
|
17
|
+
':matches(' +
|
|
18
|
+
'[callee.property.name="querySelector"],' +
|
|
19
|
+
'[callee.property.name="querySelectorAll"]' +
|
|
20
|
+
')';
|
|
21
|
+
const assignedCall = 'CallExpression' +
|
|
22
|
+
'[callee.type="MemberExpression"]' +
|
|
23
|
+
':matches(' +
|
|
24
|
+
'[callee.property.name="assignedElements"],' +
|
|
25
|
+
'[callee.property.name="assignedNodes"]' +
|
|
26
|
+
')' +
|
|
27
|
+
'[callee.object.type="CallExpression"]' +
|
|
28
|
+
'[callee.object.callee.type="MemberExpression"]' +
|
|
29
|
+
'[callee.object.callee.object.type="MemberExpression"]' +
|
|
30
|
+
'[callee.object.callee.object.object.type="ThisExpression"]' +
|
|
31
|
+
':matches(' +
|
|
32
|
+
'[callee.object.callee.object.property.name="shadowRoot"],' +
|
|
33
|
+
'[callee.object.callee.object.property.name="renderRoot"]' +
|
|
34
|
+
')' +
|
|
35
|
+
'[callee.object.callee.property.name="querySelector"]';
|
|
36
|
+
//------------------------------------------------------------------------------
|
|
37
|
+
// Constants
|
|
38
|
+
//------------------------------------------------------------------------------
|
|
39
|
+
const assignedMethodNames = new Set(['assignedElements', 'assignedNodes']);
|
|
40
|
+
const renderRootProperties = new Set(['shadowRoot', 'renderRoot']);
|
|
41
|
+
const defaultOptions = {
|
|
42
|
+
querySelector: true,
|
|
43
|
+
querySelectorAll: true,
|
|
44
|
+
assignedElements: true,
|
|
45
|
+
assignedNodes: true
|
|
46
|
+
};
|
|
47
|
+
const querySelectorMessageMap = new Map([
|
|
48
|
+
['querySelector', 'querySelector'],
|
|
49
|
+
['querySelectorAll', 'querySelectorAll']
|
|
50
|
+
]);
|
|
51
|
+
const assignedMessageMap = new Map([
|
|
52
|
+
['assignedElements', 'assignedElements'],
|
|
53
|
+
['assignedNodes', 'assignedNodes']
|
|
54
|
+
]);
|
|
55
|
+
//------------------------------------------------------------------------------
|
|
56
|
+
// Helpers
|
|
57
|
+
//------------------------------------------------------------------------------
|
|
58
|
+
/**
|
|
59
|
+
* Determines if a call expression is the inner querySelector of a chained
|
|
60
|
+
* assignedElements/assignedNodes call, to avoid double-reporting.
|
|
61
|
+
*
|
|
62
|
+
* @param {ESTree.CallExpression & Rule.NodeParentExtension} node Call expression to test
|
|
63
|
+
* @return {boolean}
|
|
64
|
+
*/
|
|
65
|
+
function isChainedWithAssignedCall(node) {
|
|
66
|
+
const parent = node.parent;
|
|
67
|
+
return ((parent === null || parent === void 0 ? void 0 : parent.type) === 'MemberExpression' &&
|
|
68
|
+
parent.property.type === 'Identifier' &&
|
|
69
|
+
assignedMethodNames.has(parent.property.name));
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Returns the method name from a member expression callee, or null if the
|
|
73
|
+
* property is not a simple identifier.
|
|
74
|
+
*
|
|
75
|
+
* @param {ESTree.MemberExpression} callee Callee to inspect
|
|
76
|
+
* @return {string|null}
|
|
77
|
+
*/
|
|
78
|
+
function getMethodName(callee) {
|
|
79
|
+
return callee.property.type === 'Identifier' ? callee.property.name : null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Returns the render root property name (shadowRoot or renderRoot) from a
|
|
83
|
+
* callee whose object is a member expression on `this`, or null if it does
|
|
84
|
+
* not match the expected shape.
|
|
85
|
+
*
|
|
86
|
+
* @param {ESTree.MemberExpression} callee Callee to inspect
|
|
87
|
+
* @return {string|null}
|
|
88
|
+
*/
|
|
89
|
+
function getRenderRootName(callee) {
|
|
90
|
+
const obj = callee.object;
|
|
91
|
+
if (obj.type !== 'MemberExpression' || obj.property.type !== 'Identifier') {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
const name = obj.property.name;
|
|
95
|
+
return renderRootProperties.has(name) ? name : null;
|
|
96
|
+
}
|
|
97
|
+
//------------------------------------------------------------------------------
|
|
98
|
+
// Rule Definition
|
|
99
|
+
//------------------------------------------------------------------------------
|
|
100
|
+
export const rule = {
|
|
101
|
+
meta: {
|
|
102
|
+
type: 'suggestion',
|
|
103
|
+
docs: {
|
|
104
|
+
description: 'Requires use of query decorators instead of manual DOM queries',
|
|
105
|
+
recommended: false,
|
|
106
|
+
url: 'https://github.com/43081j/eslint-plugin-lit/blob/master/docs/rules/prefer-query-decorators.md'
|
|
107
|
+
},
|
|
108
|
+
schema: [
|
|
109
|
+
{
|
|
110
|
+
type: 'object',
|
|
111
|
+
properties: {
|
|
112
|
+
querySelector: { type: 'boolean' },
|
|
113
|
+
querySelectorAll: { type: 'boolean' },
|
|
114
|
+
assignedElements: { type: 'boolean' },
|
|
115
|
+
assignedNodes: { type: 'boolean' }
|
|
116
|
+
},
|
|
117
|
+
additionalProperties: false
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
messages: {
|
|
121
|
+
preferQuery: 'Use @query decorator instead of this.{{ root }}.querySelector()',
|
|
122
|
+
preferQueryAll: 'Use @queryAll decorator instead of this.{{ root }}.querySelectorAll()',
|
|
123
|
+
preferQueryAssignedElements: 'Use @queryAssignedElements decorator instead of' +
|
|
124
|
+
' this.{{ root }}.querySelector().assignedElements()',
|
|
125
|
+
preferQueryAssignedNodes: 'Use @queryAssignedNodes decorator instead of' +
|
|
126
|
+
' this.{{ root }}.querySelector().assignedNodes()'
|
|
127
|
+
},
|
|
128
|
+
defaultOptions: [defaultOptions]
|
|
129
|
+
},
|
|
130
|
+
create(context) {
|
|
131
|
+
const options = { ...defaultOptions, ...context.options[0] };
|
|
132
|
+
let litClassDepth = 0;
|
|
133
|
+
//----------------------------------------------------------------------
|
|
134
|
+
// Helpers
|
|
135
|
+
//----------------------------------------------------------------------
|
|
136
|
+
/**
|
|
137
|
+
* Class entered
|
|
138
|
+
*
|
|
139
|
+
* @param {ESTree.Class} node Node entered
|
|
140
|
+
* @return {void}
|
|
141
|
+
*/
|
|
142
|
+
function classEnter(node) {
|
|
143
|
+
if (isLitClass(node, context)) {
|
|
144
|
+
litClassDepth++;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Class exited
|
|
149
|
+
*
|
|
150
|
+
* @param {ESTree.Class} node Node exited
|
|
151
|
+
* @return {void}
|
|
152
|
+
*/
|
|
153
|
+
function classExit(node) {
|
|
154
|
+
if (isLitClass(node, context)) {
|
|
155
|
+
litClassDepth--;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* querySelector or querySelectorAll call found
|
|
160
|
+
*
|
|
161
|
+
* @param {ESTree.CallExpression} node Node entered
|
|
162
|
+
* @return {void}
|
|
163
|
+
*/
|
|
164
|
+
function handleQuerySelectorCall(node) {
|
|
165
|
+
if (litClassDepth === 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
if (isChainedWithAssignedCall(node) ||
|
|
169
|
+
node.callee.type !== 'MemberExpression') {
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
const callee = node.callee;
|
|
173
|
+
const methodName = getMethodName(callee);
|
|
174
|
+
const rootName = getRenderRootName(callee);
|
|
175
|
+
if (!methodName || !rootName) {
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
const optionKey = querySelectorMessageMap.get(methodName);
|
|
179
|
+
if (!optionKey || !options[optionKey]) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
const messageId = methodName === 'querySelector' ? 'preferQuery' : 'preferQueryAll';
|
|
183
|
+
context.report({ node, messageId, data: { root: rootName } });
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* assignedElements or assignedNodes call found
|
|
187
|
+
*
|
|
188
|
+
* @param {ESTree.CallExpression} node Node entered
|
|
189
|
+
* @return {void}
|
|
190
|
+
*/
|
|
191
|
+
function handleAssignedCall(node) {
|
|
192
|
+
if (litClassDepth === 0 || node.callee.type !== 'MemberExpression')
|
|
193
|
+
return;
|
|
194
|
+
const callee = node.callee;
|
|
195
|
+
const methodName = getMethodName(callee);
|
|
196
|
+
if (!methodName || callee.object.type !== 'CallExpression') {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
const querySelectorCallExpr = callee.object;
|
|
200
|
+
if (querySelectorCallExpr.callee.type !== 'MemberExpression') {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
const querySelectorCallee = querySelectorCallExpr.callee;
|
|
204
|
+
const rootName = getRenderRootName(querySelectorCallee);
|
|
205
|
+
if (!rootName) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const optionKey = assignedMessageMap.get(methodName);
|
|
209
|
+
if (!optionKey || !options[optionKey]) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const messageId = methodName === 'assignedElements'
|
|
213
|
+
? 'preferQueryAssignedElements'
|
|
214
|
+
: 'preferQueryAssignedNodes';
|
|
215
|
+
context.report({ node, messageId, data: { root: rootName } });
|
|
216
|
+
}
|
|
217
|
+
//----------------------------------------------------------------------
|
|
218
|
+
// Public
|
|
219
|
+
//----------------------------------------------------------------------
|
|
220
|
+
return {
|
|
221
|
+
ClassExpression: (node) => classEnter(node),
|
|
222
|
+
ClassDeclaration: (node) => classEnter(node),
|
|
223
|
+
'ClassExpression:exit': (node) => classExit(node),
|
|
224
|
+
'ClassDeclaration:exit': (node) => classExit(node),
|
|
225
|
+
[querySelectorCall]: (node) => handleQuerySelectorCall(node),
|
|
226
|
+
[assignedCall]: (node) => handleAssignedCall(node)
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
};
|
|
@@ -22,7 +22,8 @@ export const rule = {
|
|
|
22
22
|
messages: {
|
|
23
23
|
always: 'Static styles should be used instead of inline style tags',
|
|
24
24
|
never: 'Inline style tags should be used instead of static styles'
|
|
25
|
-
}
|
|
25
|
+
},
|
|
26
|
+
defaultOptions: ['always']
|
|
26
27
|
},
|
|
27
28
|
create(context) {
|
|
28
29
|
const source = context.sourceCode;
|
|
@@ -44,7 +44,8 @@ export const rule = {
|
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
const valueName = element.attribs['.value'] ? '.value' : 'value';
|
|
47
|
-
const attrLocs = element.sourceCodeLocation
|
|
47
|
+
const attrLocs = element.sourceCodeLocation
|
|
48
|
+
.attrs;
|
|
48
49
|
const valueLoc = attrLocs[valueName];
|
|
49
50
|
const valueAttr = element.attribs[valueName];
|
|
50
51
|
if (!valueAttr ||
|
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
import * as ESTree from 'estree';
|
|
2
2
|
import * as parse5 from 'parse5';
|
|
3
3
|
import { SourceCode } from 'eslint';
|
|
4
|
-
import
|
|
4
|
+
import { type Parse5Node, type Parse5DocumentFragment, type Parse5Element, type Parse5CommentNode, type Parse5TextNode } from './util.js';
|
|
5
5
|
export interface RawAttribute {
|
|
6
6
|
name: string;
|
|
7
7
|
value?: string;
|
|
8
8
|
quotedValue?: string;
|
|
9
9
|
}
|
|
10
10
|
export interface Visitor {
|
|
11
|
-
enter: (node:
|
|
12
|
-
exit: (node:
|
|
13
|
-
enterElement: (node:
|
|
14
|
-
enterDocumentFragment: (node:
|
|
15
|
-
enterCommentNode: (node:
|
|
16
|
-
enterTextNode: (node:
|
|
11
|
+
enter: (node: Parse5Node, parent: Parse5Node | null) => void;
|
|
12
|
+
exit: (node: Parse5Node, parent: Parse5Node | null) => void;
|
|
13
|
+
enterElement: (node: Parse5Element, parent: Parse5Node | null) => void;
|
|
14
|
+
enterDocumentFragment: (node: Parse5DocumentFragment, parent: Parse5Node | null) => void;
|
|
15
|
+
enterCommentNode: (node: Parse5CommentNode, parent: Parse5Node | null) => void;
|
|
16
|
+
enterTextNode: (node: Parse5TextNode, parent: Parse5Node | null) => void;
|
|
17
17
|
}
|
|
18
|
-
export interface ParseError extends parse5.Location {
|
|
18
|
+
export interface ParseError extends parse5.Token.Location {
|
|
19
19
|
code: string;
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
@@ -26,7 +26,7 @@ export declare class TemplateAnalyzer {
|
|
|
26
26
|
errors: ReadonlyArray<ParseError>;
|
|
27
27
|
source: string;
|
|
28
28
|
protected _node: ESTree.TaggedTemplateExpression;
|
|
29
|
-
protected _ast:
|
|
29
|
+
protected _ast: Parse5DocumentFragment;
|
|
30
30
|
/**
|
|
31
31
|
* Create an analyzer instance for a given node
|
|
32
32
|
*
|
|
@@ -43,32 +43,32 @@ export declare class TemplateAnalyzer {
|
|
|
43
43
|
/**
|
|
44
44
|
* Returns the ESTree location equivalent of a given attribute
|
|
45
45
|
*
|
|
46
|
-
* @param {
|
|
46
|
+
* @param {Parse5Element} element Element which owns this attribute
|
|
47
47
|
* @param {string} attr Attribute name to retrieve
|
|
48
48
|
* @param {SourceCode} source Source code from ESLint
|
|
49
49
|
* @return {?ESTree.SourceLocation}
|
|
50
50
|
*/
|
|
51
|
-
getLocationForAttribute(element:
|
|
51
|
+
getLocationForAttribute(element: Parse5Element, attr: string, source: SourceCode): ESTree.SourceLocation | null | undefined;
|
|
52
52
|
/**
|
|
53
53
|
* Returns the value of the specified attribute.
|
|
54
54
|
* If this is an expression, the expression will be returned. Otherwise,
|
|
55
55
|
* the raw value will be returned.
|
|
56
56
|
* NOTE: if an attribute has multiple expressions in its value, this will
|
|
57
57
|
* return the *first* expression.
|
|
58
|
-
* @param {
|
|
58
|
+
* @param {Parse5Element} element Element which owns this attribute
|
|
59
59
|
* @param {string} attr Attribute name to retrieve
|
|
60
60
|
* @param {SourceCode} source Source code from ESLint
|
|
61
61
|
* @return {?ESTree.Expression|string}
|
|
62
62
|
*/
|
|
63
|
-
getAttributeValue(element:
|
|
63
|
+
getAttributeValue(element: Parse5Element, attr: string, source: SourceCode): ESTree.Expression | string | null;
|
|
64
64
|
/**
|
|
65
65
|
* Returns the raw attribute source of a given attribute
|
|
66
66
|
*
|
|
67
|
-
* @param {
|
|
67
|
+
* @param {Parse5Element} element Element which owns this attribute
|
|
68
68
|
* @param {string} attr Attribute name to retrieve
|
|
69
69
|
* @return {string}
|
|
70
70
|
*/
|
|
71
|
-
getRawAttributeValue(element:
|
|
71
|
+
getRawAttributeValue(element: Parse5Element, attr: string): RawAttribute | null;
|
|
72
72
|
/**
|
|
73
73
|
* Resolves a Parse5 location into an ESTree range
|
|
74
74
|
*
|
|
@@ -76,7 +76,7 @@ export declare class TemplateAnalyzer {
|
|
|
76
76
|
* @param {SourceCode} source ESLint source code object
|
|
77
77
|
* @return {ESTree.SourceLocation}
|
|
78
78
|
*/
|
|
79
|
-
resolveLocation(loc: parse5.Location, source: SourceCode): ESTree.SourceLocation | null;
|
|
79
|
+
resolveLocation(loc: parse5.Token.Location, source: SourceCode): ESTree.SourceLocation | null;
|
|
80
80
|
/**
|
|
81
81
|
* Traverse the inner HTML tree with a given visitor
|
|
82
82
|
*
|
package/lib/template-analyzer.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as parse5 from 'parse5';
|
|
2
|
-
import treeAdapter from 'parse5-htmlparser2-tree-adapter';
|
|
2
|
+
import { adapter as treeAdapter } from 'parse5-htmlparser2-tree-adapter';
|
|
3
3
|
import { templateExpressionToHtml, getExpressionPlaceholder } from './util.js';
|
|
4
4
|
const isRootNode = (node) => node.type === 'root';
|
|
5
5
|
const analyzerCache = new WeakMap();
|
|
@@ -49,13 +49,14 @@ export class TemplateAnalyzer {
|
|
|
49
49
|
/**
|
|
50
50
|
* Returns the ESTree location equivalent of a given attribute
|
|
51
51
|
*
|
|
52
|
-
* @param {
|
|
52
|
+
* @param {Parse5Element} element Element which owns this attribute
|
|
53
53
|
* @param {string} attr Attribute name to retrieve
|
|
54
54
|
* @param {SourceCode} source Source code from ESLint
|
|
55
55
|
* @return {?ESTree.SourceLocation}
|
|
56
56
|
*/
|
|
57
57
|
getLocationForAttribute(element, attr, source) {
|
|
58
|
-
if (!element.sourceCodeLocation ||
|
|
58
|
+
if (!element.sourceCodeLocation ||
|
|
59
|
+
!element.sourceCodeLocation.attrs) {
|
|
59
60
|
return null;
|
|
60
61
|
}
|
|
61
62
|
const loc = element.sourceCodeLocation.attrs[attr.toLowerCase()];
|
|
@@ -67,7 +68,7 @@ export class TemplateAnalyzer {
|
|
|
67
68
|
* the raw value will be returned.
|
|
68
69
|
* NOTE: if an attribute has multiple expressions in its value, this will
|
|
69
70
|
* return the *first* expression.
|
|
70
|
-
* @param {
|
|
71
|
+
* @param {Parse5Element} element Element which owns this attribute
|
|
71
72
|
* @param {string} attr Attribute name to retrieve
|
|
72
73
|
* @param {SourceCode} source Source code from ESLint
|
|
73
74
|
* @return {?ESTree.Expression|string}
|
|
@@ -101,11 +102,12 @@ export class TemplateAnalyzer {
|
|
|
101
102
|
/**
|
|
102
103
|
* Returns the raw attribute source of a given attribute
|
|
103
104
|
*
|
|
104
|
-
* @param {
|
|
105
|
+
* @param {Parse5Element} element Element which owns this attribute
|
|
105
106
|
* @param {string} attr Attribute name to retrieve
|
|
106
107
|
* @return {string}
|
|
107
108
|
*/
|
|
108
109
|
getRawAttributeValue(element, attr) {
|
|
110
|
+
var _a, _b;
|
|
109
111
|
if (!element.sourceCodeLocation) {
|
|
110
112
|
return null;
|
|
111
113
|
}
|
|
@@ -115,7 +117,7 @@ export class TemplateAnalyzer {
|
|
|
115
117
|
originalAttr = `${xAttribs[attr]}:${attr}`;
|
|
116
118
|
}
|
|
117
119
|
const loc = element.sourceCodeLocation.attrs[originalAttr];
|
|
118
|
-
const source = this.source.substring(loc.startOffset, loc.endOffset);
|
|
120
|
+
const source = this.source.substring((_a = loc === null || loc === void 0 ? void 0 : loc.startOffset) !== null && _a !== void 0 ? _a : 0, (_b = loc === null || loc === void 0 ? void 0 : loc.endOffset) !== null && _b !== void 0 ? _b : 0);
|
|
119
121
|
const firstEq = source.indexOf('=');
|
|
120
122
|
const left = firstEq === -1 ? source : source.substr(0, firstEq);
|
|
121
123
|
const right = firstEq === -1 ? undefined : source.substr(firstEq + 1);
|
package/lib/util.d.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
import * as ESTree from 'estree';
|
|
2
2
|
import { Rule } from 'eslint';
|
|
3
|
+
import { type Htmlparser2TreeAdapterMap } from 'parse5-htmlparser2-tree-adapter';
|
|
4
|
+
export type Parse5Node = Htmlparser2TreeAdapterMap['node'];
|
|
5
|
+
export type Parse5Element = Htmlparser2TreeAdapterMap['element'];
|
|
6
|
+
export type Parse5Document = Htmlparser2TreeAdapterMap['document'];
|
|
7
|
+
export type Parse5DocumentFragment = Htmlparser2TreeAdapterMap['documentFragment'];
|
|
8
|
+
export type Parse5CommentNode = Htmlparser2TreeAdapterMap['commentNode'];
|
|
9
|
+
export type Parse5TextNode = Htmlparser2TreeAdapterMap['textNode'];
|
|
10
|
+
export type AttributeLocation = NonNullable<Parse5Element['sourceCodeLocation']> & {
|
|
11
|
+
attrs: Record<string, Parse5Node['sourceCodeLocation']>;
|
|
12
|
+
};
|
|
3
13
|
export interface BabelDecorator extends ESTree.BaseNode {
|
|
4
14
|
type: 'Decorator';
|
|
5
15
|
expression: ESTree.Expression;
|
package/lib/util.js
CHANGED
|
@@ -241,7 +241,7 @@ export function hasLitPropertyDecorator(node) {
|
|
|
241
241
|
export function getExpressionPlaceholder(node, quasi) {
|
|
242
242
|
const i = node.quasi.quasis.indexOf(quasi);
|
|
243
243
|
// Just a rough guess at if this might be an attribute binding or not
|
|
244
|
-
const possibleAttr = /\s[^\s
|
|
244
|
+
const possibleAttr = /\s[^\s/>"'=]+=$/;
|
|
245
245
|
if (possibleAttr.test(quasi.value.raw)) {
|
|
246
246
|
return `"{{__Q:${i}__}}"`;
|
|
247
247
|
}
|
|
@@ -299,10 +299,10 @@ export function toKebabCase(camelCaseStr) {
|
|
|
299
299
|
* @return {string[]}
|
|
300
300
|
*/
|
|
301
301
|
export function getElementBaseClasses(context) {
|
|
302
|
-
var _a;
|
|
303
302
|
const bases = new Set(['LitElement']);
|
|
304
|
-
|
|
305
|
-
|
|
303
|
+
const settings = context.settings.lit;
|
|
304
|
+
if (Array.isArray(settings === null || settings === void 0 ? void 0 : settings.elementBaseClasses)) {
|
|
305
|
+
const configuredBases = settings.elementBaseClasses;
|
|
306
306
|
for (const base of configuredBases) {
|
|
307
307
|
bases.add(base);
|
|
308
308
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-lit",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "lit-html support for ESLint",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -41,8 +41,8 @@
|
|
|
41
41
|
"node": ">= 18"
|
|
42
42
|
},
|
|
43
43
|
"dependencies": {
|
|
44
|
-
"parse5": "^
|
|
45
|
-
"parse5-htmlparser2-tree-adapter": "^
|
|
44
|
+
"parse5": "^8.0.1",
|
|
45
|
+
"parse5-htmlparser2-tree-adapter": "^8.0.1"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
48
|
"eslint": ">= 8"
|
|
@@ -50,24 +50,21 @@
|
|
|
50
50
|
"devDependencies": {
|
|
51
51
|
"@babel/eslint-parser": "^7.24.5",
|
|
52
52
|
"@babel/plugin-proposal-decorators": "^7.24.1",
|
|
53
|
+
"@eslint/js": "^9.39.4",
|
|
53
54
|
"@types/chai": "^4.2.16",
|
|
54
55
|
"@types/eslint": "^8.4.6",
|
|
55
56
|
"@types/estree": "^1.0.0",
|
|
56
57
|
"@types/mocha": "^10.0.0",
|
|
57
58
|
"@types/node": "^20.8.8",
|
|
58
|
-
"@types/parse5": "^6.0.0",
|
|
59
|
-
"@types/parse5-htmlparser2-tree-adapter": "^6.0.0",
|
|
60
|
-
"@typescript-eslint/eslint-plugin": "^6.9.0",
|
|
61
|
-
"@typescript-eslint/parser": "^6.9.0",
|
|
62
59
|
"c8": "^10.1.3",
|
|
63
60
|
"chai": "^4.2.0",
|
|
64
|
-
"eslint": "^
|
|
65
|
-
"eslint-
|
|
66
|
-
"eslint-plugin-eslint-plugin": "^5.0.6",
|
|
61
|
+
"eslint": "^9.39.4",
|
|
62
|
+
"eslint-plugin-eslint-plugin": "^7.3.2",
|
|
67
63
|
"espree": "^9.0.0",
|
|
68
64
|
"mocha": "^10.0.0",
|
|
69
65
|
"premove": "^4.0.0",
|
|
70
66
|
"prettier": "^3.0.3",
|
|
71
|
-
"typescript": "^
|
|
67
|
+
"typescript": "^6.0.3",
|
|
68
|
+
"typescript-eslint": "^8.59.1"
|
|
72
69
|
}
|
|
73
70
|
}
|