Skip to content

Commit 838a515

Browse files
committed
feat(no-navigation-without-resolve): recognizing resolved values with queries and fragments
1 parent 1c756ba commit 838a515

14 files changed

+1754
-20
lines changed

.changeset/six-mugs-pick.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat(no-navigation-without-resolve): recognizing resolved values with queries and fragments

packages/eslint-plugin-svelte/src/rules/no-navigation-without-resolve.ts

Lines changed: 99 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,13 @@ function checkGotoCall(
189189
): void {
190190
if (
191191
call.arguments.length > 0 &&
192-
!isValueAllowed(new FindVariableContext(context), call.arguments[0], resolveReferences, {})
192+
!isValueAllowed(
193+
context,
194+
new FindVariableContext(context),
195+
call.arguments[0],
196+
resolveReferences,
197+
{}
198+
)
193199
) {
194200
context.report({ loc: call.arguments[0].loc, messageId: 'gotoWithoutResolve' });
195201
}
@@ -203,9 +209,15 @@ function checkShallowNavigationCall(
203209
): void {
204210
if (
205211
call.arguments.length > 0 &&
206-
!isValueAllowed(new FindVariableContext(context), call.arguments[0], resolveReferences, {
207-
allowEmpty: true
208-
})
212+
!isValueAllowed(
213+
context,
214+
new FindVariableContext(context),
215+
call.arguments[0],
216+
resolveReferences,
217+
{
218+
allowEmpty: true
219+
}
220+
)
209221
) {
210222
context.report({ loc: call.arguments[0].loc, messageId });
211223
}
@@ -223,7 +235,7 @@ function checkLinkAttribute(
223235
attribute.parent.parent.name.type === 'SvelteName' &&
224236
attribute.parent.parent.name.name === 'a' &&
225237
attribute.key.name === 'href' &&
226-
!isValueAllowed(new FindVariableContext(context), value, resolveReferences, {
238+
!isValueAllowed(context, new FindVariableContext(context), value, resolveReferences, {
227239
allowAbsolute: true,
228240
allowFragment: true,
229241
allowNullish: true
@@ -234,7 +246,8 @@ function checkLinkAttribute(
234246
}
235247

236248
function isValueAllowed(
237-
ctx: FindVariableContext,
249+
context: RuleContext,
250+
findContext: FindVariableContext,
238251
value: TSESTree.CallExpressionArgument | TSESTree.Expression | AST.SvelteLiteral,
239252
resolveReferences: Set<TSESTree.Identifier>,
240253
config: {
@@ -245,34 +258,73 @@ function isValueAllowed(
245258
}
246259
): boolean {
247260
if (value.type === 'Identifier') {
248-
const variable = ctx.findVariable(value);
261+
const variable = findContext.findVariable(value);
249262
if (
250263
variable !== null &&
251264
variable.identifiers.length > 0 &&
252265
variable.identifiers[0].parent.type === 'VariableDeclarator' &&
253266
variable.identifiers[0].parent.init !== null
254267
) {
255-
return isValueAllowed(ctx, variable.identifiers[0].parent.init, resolveReferences, config);
268+
return isValueAllowed(
269+
context,
270+
findContext,
271+
variable.identifiers[0].parent.init,
272+
resolveReferences,
273+
config
274+
);
256275
}
257276
}
258277
if (
259-
(config.allowAbsolute && expressionIsAbsoluteUrl(ctx, value)) ||
260-
(config.allowEmpty && expressionIsEmpty(value)) ||
261-
(config.allowFragment && expressionStartsWith(ctx, value, '#')) ||
278+
(config.allowAbsolute && expressionIsAbsoluteUrl(new FindVariableContext(context), value)) ||
279+
(config.allowEmpty && expressionIsEmpty(new FindVariableContext(context), value)) ||
280+
(config.allowFragment && expressionStartsWith(new FindVariableContext(context), value, '#')) ||
262281
(config.allowNullish && expressionIsNullish(value)) ||
263-
expressionStartsWith(ctx, value, '?') ||
264-
expressionIsResolveCall(ctx, value, resolveReferences)
282+
expressionStartsWith(new FindVariableContext(context), value, '?') ||
283+
expressionIsResolveCall(new FindVariableContext(context), value, resolveReferences)
265284
) {
266285
return true;
267286
}
287+
if (value.type === 'BinaryExpression' && value.left.type !== 'PrivateIdentifier') {
288+
if (['BinaryExpression', 'TemplateLiteral'].includes(value.left.type)) {
289+
return isValueAllowed(
290+
context,
291+
new FindVariableContext(context),
292+
value.left,
293+
resolveReferences,
294+
config
295+
);
296+
}
297+
if (
298+
expressionIsResolveCall(new FindVariableContext(context), value.left, resolveReferences) &&
299+
(expressionIsEmpty(new FindVariableContext(context), value.right) ||
300+
expressionStartsWith(new FindVariableContext(context), value.right, '?') ||
301+
expressionStartsWith(new FindVariableContext(context), value.right, '#'))
302+
) {
303+
return true;
304+
}
305+
}
306+
if (value.type === 'TemplateLiteral') {
307+
const parts = [
308+
...value.expressions,
309+
...value.quasis.filter((quasi) => quasi.value.raw !== '')
310+
].sort((a, b) => a.range[0] - b.range[0]);
311+
if (
312+
expressionIsResolveCall(new FindVariableContext(context), parts[0], resolveReferences) &&
313+
(expressionIsEmpty(new FindVariableContext(context), parts[1]) ||
314+
expressionStartsWith(new FindVariableContext(context), parts[1], '?') ||
315+
expressionStartsWith(new FindVariableContext(context), parts[1], '#'))
316+
) {
317+
return true;
318+
}
319+
}
268320
return false;
269321
}
270322

271323
// Helper functions
272324

273325
function expressionIsResolveCall(
274326
ctx: FindVariableContext,
275-
node: TSESTree.CallExpressionArgument | AST.SvelteLiteral,
327+
node: TSESTree.CallExpressionArgument | TSESTree.TemplateElement | AST.SvelteLiteral,
276328
resolveReferences: Set<TSESTree.Identifier>
277329
): boolean {
278330
if (
@@ -300,15 +352,36 @@ function expressionIsResolveCall(
300352
}
301353

302354
function expressionIsEmpty(
303-
node: TSESTree.CallExpressionArgument | TSESTree.Expression | AST.SvelteLiteral
355+
ctx: FindVariableContext,
356+
node:
357+
| TSESTree.CallExpressionArgument
358+
| TSESTree.Expression
359+
| TSESTree.TemplateElement
360+
| AST.SvelteLiteral
304361
): boolean {
305-
return (
362+
if (
306363
(node.type === 'Literal' && node.value === '') ||
364+
(node.type === 'TemplateElement' && node.value.raw === '') ||
307365
(node.type === 'TemplateLiteral' &&
308366
node.expressions.length === 0 &&
309367
node.quasis.length === 1 &&
310-
node.quasis[0].value.raw === '')
311-
);
368+
expressionIsEmpty(ctx, node.quasis[0]))
369+
) {
370+
return true;
371+
}
372+
if (node.type !== 'Identifier') {
373+
return false;
374+
}
375+
const variable = ctx.findVariable(node);
376+
if (
377+
variable === null ||
378+
variable.identifiers.length === 0 ||
379+
variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
380+
variable.identifiers[0].parent.init === null
381+
) {
382+
return false;
383+
}
384+
return expressionIsEmpty(ctx, variable.identifiers[0].parent.init);
312385
}
313386

314387
function expressionIsNullish(
@@ -368,7 +441,11 @@ function valueIsAbsoluteUrl(node: string): boolean {
368441

369442
function expressionStartsWith(
370443
ctx: FindVariableContext,
371-
node: TSESTree.CallExpressionArgument | TSESTree.Expression | AST.SvelteLiteral,
444+
node:
445+
| TSESTree.CallExpressionArgument
446+
| TSESTree.Expression
447+
| TSESTree.TemplateElement
448+
| AST.SvelteLiteral,
372449
prefix: string
373450
): boolean {
374451
switch (node.type) {
@@ -380,6 +457,8 @@ function expressionStartsWith(
380457
return typeof node.value === 'string' && node.value.startsWith(prefix);
381458
case 'SvelteLiteral':
382459
return node.value.startsWith(prefix);
460+
case 'TemplateElement':
461+
return node.value.raw.startsWith(prefix);
383462
case 'TemplateLiteral':
384463
return templateLiteralStartsWith(ctx, node, prefix);
385464
default:
@@ -419,6 +498,6 @@ function templateLiteralStartsWith(
419498
): boolean {
420499
return (
421500
(node.expressions.length >= 1 && expressionStartsWith(ctx, node.expressions[0], prefix)) ||
422-
(node.quasis.length >= 1 && node.quasis[0].value.raw.startsWith(prefix))
501+
(node.quasis.length >= 1 && expressionStartsWith(ctx, node.quasis[0], prefix))
423502
);
424503
}

0 commit comments

Comments
 (0)