Skip to content

Commit b6f2ee6

Browse files
committed
feat(consistent-selector-style): matching dynamic suffixes
1 parent 639b265 commit b6f2ee6

File tree

4 files changed

+148
-16
lines changed

4 files changed

+148
-16
lines changed

Diff for: packages/eslint-plugin-svelte/src/rules/consistent-selector-style.ts

+21-15
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,16 @@ import type {
99
import type { SvelteHTMLElement } from 'svelte-eslint-parser/lib/ast';
1010
import { findClassesInAttribute } from '../utils/ast-utils.js';
1111
import { getSourceCode } from '../utils/compat.js';
12-
import { extractExpressionPrefixLiteral } from '../utils/expression-affixes.js';
12+
import {
13+
extractExpressionPrefixLiteral,
14+
extractExpressionSuffixLiteral
15+
} from '../utils/expression-affixes.js';
1316
import { createRule } from '../utils/index.js';
1417

1518
interface Selections {
1619
exact: Map<string, AST.SvelteHTMLElement[]>;
17-
prefixes: Map<string, AST.SvelteHTMLElement[]>;
20+
// [prefix, suffix]
21+
affixes: Map<[string | null, string | null], AST.SvelteHTMLElement[]>;
1822
universalSelector: boolean;
1923
}
2024

@@ -79,12 +83,12 @@ export default createRule('consistent-selector-style', {
7983
} = {
8084
class: {
8185
exact: new Map(),
82-
prefixes: new Map(),
86+
affixes: new Map(),
8387
universalSelector: false
8488
},
8589
id: {
8690
exact: new Map(),
87-
prefixes: new Map(),
91+
affixes: new Map(),
8892
universalSelector: false
8993
},
9094
type: new Map()
@@ -232,21 +236,23 @@ export default createRule('consistent-selector-style', {
232236
for (const value of attribute.value) {
233237
if (attribute.key.name === 'class' && value.type === 'SvelteMustacheTag') {
234238
const prefix = extractExpressionPrefixLiteral(context, value.expression);
235-
if (prefix !== null) {
236-
addToArrayMap(selections.class.prefixes, prefix, node);
237-
} else {
239+
const suffix = extractExpressionSuffixLiteral(context, value.expression);
240+
if (prefix === null && suffix === null) {
238241
selections.class.universalSelector = true;
242+
} else {
243+
addToArrayMap(selections.class.affixes, [prefix, suffix], node);
239244
}
240245
}
241246
if (attribute.key.name === 'id') {
242247
if (value.type === 'SvelteLiteral') {
243248
addToArrayMap(selections.id.exact, value.value, node);
244249
} else if (value.type === 'SvelteMustacheTag') {
245250
const prefix = extractExpressionPrefixLiteral(context, value.expression);
246-
if (prefix !== null) {
247-
addToArrayMap(selections.id.prefixes, prefix, node);
248-
} else {
251+
const suffix = extractExpressionSuffixLiteral(context, value.expression);
252+
if (prefix === null && suffix === null) {
249253
selections.id.universalSelector = true;
254+
} else {
255+
addToArrayMap(selections.id.affixes, [prefix, suffix], node);
250256
}
251257
}
252258
}
@@ -270,9 +276,9 @@ export default createRule('consistent-selector-style', {
270276
/**
271277
* Helper function to add a value to a Map of arrays
272278
*/
273-
function addToArrayMap(
274-
map: Map<string, AST.SvelteHTMLElement[]>,
275-
key: string,
279+
function addToArrayMap<T>(
280+
map: Map<T, AST.SvelteHTMLElement[]>,
281+
key: T,
276282
value: AST.SvelteHTMLElement
277283
): void {
278284
map.set(key, (map.get(key) ?? []).concat(value));
@@ -283,8 +289,8 @@ function addToArrayMap(
283289
*/
284290
function matchSelection(selections: Selections, key: string): SvelteHTMLElement[] {
285291
const selection = selections.exact.get(key) ?? [];
286-
selections.prefixes.forEach((nodes, prefix) => {
287-
if (key.startsWith(prefix)) {
292+
selections.affixes.forEach((nodes, [prefix, suffix]) => {
293+
if ((prefix === null || key.startsWith(prefix)) && (suffix === null || key.endsWith(suffix))) {
288294
selection.push(...nodes);
289295
}
290296
});

Diff for: packages/eslint-plugin-svelte/src/utils/expression-affixes.ts

+66-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ function extractTemplateLiteralPrefixLiteral(
133133
for (const part of literalParts) {
134134
if (part.type === 'TemplateElement') {
135135
if (part.value.raw === '') {
136-
// Skip empty quasi in the begining
136+
// Skip empty quasi
137137
continue;
138138
}
139139
return part.value.raw;
@@ -142,3 +142,68 @@ function extractTemplateLiteralPrefixLiteral(
142142
}
143143
return null;
144144
}
145+
146+
// Literal suffix extraction
147+
148+
export function extractExpressionSuffixLiteral(
149+
context: RuleContext,
150+
expression: SvelteLiteral | TSESTree.Node
151+
): string | null {
152+
switch (expression.type) {
153+
case 'BinaryExpression':
154+
return extractBinaryExpressionSuffixLiteral(context, expression);
155+
case 'Identifier':
156+
return extractVariableSuffixLiteral(context, expression);
157+
case 'Literal':
158+
return typeof expression.value === 'string' ? expression.value : null;
159+
case 'SvelteLiteral':
160+
return expression.value;
161+
case 'TemplateLiteral':
162+
return extractTemplateLiteralSuffixLiteral(context, expression);
163+
default:
164+
return null;
165+
}
166+
}
167+
168+
function extractBinaryExpressionSuffixLiteral(
169+
context: RuleContext,
170+
expression: TSESTree.BinaryExpression
171+
): string | null {
172+
return extractExpressionSuffixLiteral(context, expression.right);
173+
}
174+
175+
function extractVariableSuffixLiteral(
176+
context: RuleContext,
177+
expression: TSESTree.Identifier
178+
): string | null {
179+
const variable = findVariable(context, expression);
180+
if (
181+
variable === null ||
182+
variable.identifiers.length !== 1 ||
183+
variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
184+
variable.identifiers[0].parent.init === null
185+
) {
186+
return null;
187+
}
188+
return extractExpressionSuffixLiteral(context, variable.identifiers[0].parent.init);
189+
}
190+
191+
function extractTemplateLiteralSuffixLiteral(
192+
context: RuleContext,
193+
expression: TSESTree.TemplateLiteral
194+
): string | null {
195+
const literalParts = [...expression.expressions, ...expression.quasis].sort((a, b) =>
196+
a.range[0] < b.range[0] ? -1 : 1
197+
);
198+
for (const part of literalParts.reverse()) {
199+
if (part.type === 'TemplateElement') {
200+
if (part.value.raw === '') {
201+
// Skip empty quasi
202+
continue;
203+
}
204+
return part.value.raw;
205+
}
206+
return extractExpressionSuffixLiteral(context, part);
207+
}
208+
return null;
209+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<script>
2+
import { value } from "package";
3+
4+
const derived = value + "-link-three";
5+
</script>
6+
7+
<a>Click me!</a>
8+
9+
<a class={value + "-link-one"}>Click me two!</a>
10+
11+
<a class={value + "-link-one"}>Click me two!</a>
12+
13+
<a class={`${value}-link-two`}>Click me three!</a>
14+
15+
<a class={`${value}-link-two`}>Click me three!</a>
16+
17+
18+
<a class={derived}>Click me four!</a>
19+
20+
<a class={derived}>Click me four!</a>
21+
22+
<style>
23+
.foo-link-one {
24+
color: red;
25+
}
26+
27+
.foo-link-two {
28+
color: red;
29+
}
30+
31+
.foo-link-three {
32+
color: red;
33+
}
34+
</style>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<script>
2+
import { value } from "package";
3+
4+
const derived = value + "-link-three";
5+
</script>
6+
7+
<a>Click me!</a>
8+
9+
<a id={value + "-link-one"}>Click me two!</a>
10+
11+
<a id={`${value}-link-two`}>Click me three!</a>
12+
13+
<a id={derived}>Click me four!</a>
14+
15+
<style>
16+
#foo-link-one {
17+
color: red;
18+
}
19+
20+
#foo-link-two {
21+
color: red;
22+
}
23+
24+
#foo-link-three {
25+
color: red;
26+
}
27+
</style>

0 commit comments

Comments
 (0)