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 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.
@@ -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").Linter.RulesRecord>;
6
- 'flat/all': import("eslint").Linter.FlatConfig;
7
- recommended: ESLint.ConfigData<import("eslint").Linter.RulesRecord>;
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) {
@@ -74,7 +74,7 @@ export const rule = {
74
74
  * @return {void}
75
75
  */
76
76
  function assignmentFound(node) {
77
- if (!inRender) {
77
+ if (!inRender || !node.parent) {
78
78
  return;
79
79
  }
80
80
  context.report({
@@ -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,6 @@
1
+ /**
2
+ * @fileoverview Requires use of query decorators instead of manual DOM queries
3
+ * @author Kirill Karpov <https://github.com/null0rUndefined>
4
+ */
5
+ import { Rule } from 'eslint';
6
+ export declare const rule: Rule.RuleModule;
@@ -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;
@@ -23,7 +23,8 @@ export const rule = {
23
23
  ' (e.g. `foo="${bar}"`)',
24
24
  neverQuote: 'Expressions must not be quoted inside templates ' +
25
25
  ' (e.g. `foo="${bar}"`)'
26
- }
26
+ },
27
+ defaultOptions: ['never']
27
28
  },
28
29
  create(context) {
29
30
  // variables should be defined here
@@ -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.attrs;
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 treeAdapter from 'parse5-htmlparser2-tree-adapter';
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: treeAdapter.Node, parent: treeAdapter.Node | null) => void;
12
- exit: (node: treeAdapter.Node, parent: treeAdapter.Node | null) => void;
13
- enterElement: (node: treeAdapter.Element, parent: treeAdapter.Node | null) => void;
14
- enterDocumentFragment: (node: treeAdapter.DocumentFragment, parent: treeAdapter.Node | null) => void;
15
- enterCommentNode: (node: treeAdapter.CommentNode, parent: treeAdapter.Node | null) => void;
16
- enterTextNode: (node: treeAdapter.TextNode, parent: treeAdapter.Node | null) => void;
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: treeAdapter.DocumentFragment;
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 {treeAdapter.Element} element Element which owns this attribute
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: treeAdapter.Element, attr: string, source: SourceCode): ESTree.SourceLocation | null | undefined;
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 {treeAdapter.Element} element Element which owns this attribute
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: treeAdapter.Element, attr: string, source: SourceCode): ESTree.Expression | string | null;
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 {treeAdapter.Element} element Element which owns this attribute
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: treeAdapter.Element, attr: string): RawAttribute | null;
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
  *
@@ -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 {treeAdapter.Element} element Element which owns this attribute
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 || !element.sourceCodeLocation.attrs) {
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 {treeAdapter.Element} element Element which owns this attribute
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 {treeAdapter.Element} element Element which owns this attribute
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
- if (Array.isArray((_a = context.settings.lit) === null || _a === void 0 ? void 0 : _a.elementBaseClasses)) {
305
- const configuredBases = context.settings.lit.elementBaseClasses;
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.2.1",
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": "^6.0.1",
45
- "parse5-htmlparser2-tree-adapter": "^6.0.1"
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": "^8.23.0",
65
- "eslint-config-google": "^0.14.0",
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": "^5.2.2"
67
+ "typescript": "^6.0.3",
68
+ "typescript-eslint": "^8.59.1"
72
69
  }
73
70
  }