Skip to content

Commit

Permalink
feat(no-navigation-without-base): added support for urls defined in v…
Browse files Browse the repository at this point in the history
…ariables
  • Loading branch information
marekdedic committed Nov 3, 2024
1 parent 86898b2 commit 8a9a43d
Show file tree
Hide file tree
Showing 13 changed files with 110 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,14 @@ export default createRule('no-navigation-without-base', {
}
const hrefValue = node.value[0];
if (hrefValue.type === 'SvelteLiteral') {
if (!urlIsAbsolute(hrefValue)) {
if (!expressionIsAbsolute(hrefValue)) {
context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' });
}
return;
}
if (
!urlStartsWithBase(hrefValue.expression, basePathNames) &&
!urlIsAbsolute(hrefValue.expression)
!expressionStartsWithBase(context, hrefValue.expression, basePathNames) &&
!expressionIsAbsolute(hrefValue.expression)
) {
context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' });
}
Expand Down Expand Up @@ -183,7 +183,7 @@ function checkGotoCall(
return;
}
const url = call.arguments[0];
if (!urlStartsWithBase(url, basePathNames)) {
if (url.type === 'SpreadElement' || !expressionStartsWithBase(context, url, basePathNames)) {
context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' });
}
}
Expand All @@ -198,45 +198,79 @@ function checkShallowNavigationCall(
return;
}
const url = call.arguments[0];
if (!urlIsEmpty(url) && !urlStartsWithBase(url, basePathNames)) {
if (
url.type === 'SpreadElement' ||
(!expressionIsEmpty(url) && !expressionStartsWithBase(context, url, basePathNames))
) {
context.report({ loc: url.loc, messageId });
}
}

// Helper functions

function urlStartsWithBase(
url: TSESTree.CallExpressionArgument,
function expressionStartsWithBase(
context: RuleContext,
url: TSESTree.Expression,
basePathNames: Set<TSESTree.Identifier>
): boolean {
switch (url.type) {
case 'BinaryExpression':
return binaryExpressionStartsWithBase(url, basePathNames);
return binaryExpressionStartsWithBase(context, url, basePathNames);
case 'Identifier':
return variableStartsWithBase(context, url, basePathNames);
case 'TemplateLiteral':
return templateLiteralStartsWithBase(url, basePathNames);
return templateLiteralStartsWithBase(context, url, basePathNames);
default:
return false;
}
}

function binaryExpressionStartsWithBase(
context: RuleContext,
url: TSESTree.BinaryExpression,
basePathNames: Set<TSESTree.Identifier>
): boolean {
return url.left.type === 'Identifier' && basePathNames.has(url.left);
return (
url.left.type !== 'PrivateIdentifier' &&
expressionStartsWithBase(context, url.left, basePathNames)
);
}

function variableStartsWithBase(
context: RuleContext,
url: TSESTree.Identifier,
basePathNames: Set<TSESTree.Identifier>
): boolean {
if (basePathNames.has(url)) {
return true;
}
const variable = findVariable(context, url);
if (
variable === null ||
variable.identifiers.length !== 1 ||
variable.identifiers[0].parent.type !== 'VariableDeclarator' ||
variable.identifiers[0].parent.init === null
) {
return false;
}
return expressionStartsWithBase(context, variable.identifiers[0].parent.init, basePathNames);
}

function templateLiteralStartsWithBase(
context: RuleContext,
url: TSESTree.TemplateLiteral,
basePathNames: Set<TSESTree.Identifier>
): boolean {
const startingIdentifier = extractLiteralStartingIdentifier(url);
return startingIdentifier !== undefined && basePathNames.has(startingIdentifier);
const startingIdentifier = extractLiteralStartingExpression(url);
return (
startingIdentifier !== undefined &&
expressionStartsWithBase(context, startingIdentifier, basePathNames)
);
}

function extractLiteralStartingIdentifier(
function extractLiteralStartingExpression(
templateLiteral: TSESTree.TemplateLiteral
): TSESTree.Identifier | undefined {
): TSESTree.Expression | undefined {
const literalParts = [...templateLiteral.expressions, ...templateLiteral.quasis].sort((a, b) =>
a.range[0] < b.range[0] ? -1 : 1
);
Expand All @@ -245,15 +279,15 @@ function extractLiteralStartingIdentifier(
// Skip empty quasi in the begining
continue;
}
if (part.type === 'Identifier') {
if (part.type !== 'TemplateElement') {
return part;
}
return undefined;
}
return undefined;
}

function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
function expressionIsEmpty(url: TSESTree.Expression): boolean {
return (
(url.type === 'Literal' && url.value === '') ||
(url.type === 'TemplateLiteral' &&
Expand All @@ -263,7 +297,7 @@ function urlIsEmpty(url: TSESTree.CallExpressionArgument): boolean {
);
}

function urlIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean {
function expressionIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean {
switch (url.type) {
case 'BinaryExpression':
return binaryExpressionIsAbsolute(url);
Expand All @@ -280,13 +314,14 @@ function urlIsAbsolute(url: SvelteLiteral | TSESTree.Expression): boolean {

function binaryExpressionIsAbsolute(url: TSESTree.BinaryExpression): boolean {
return (
(url.left.type !== 'PrivateIdentifier' && urlIsAbsolute(url.left)) || urlIsAbsolute(url.right)
(url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(url.left)) ||
expressionIsAbsolute(url.right)
);
}

function templateLiteralIsAbsolute(url: TSESTree.TemplateLiteral): boolean {
return (
url.expressions.some(urlIsAbsolute) ||
url.expressions.some(expressionIsAbsolute) ||
url.quasis.some((quasi) => urlValueIsAbsolute(quasi.value.raw))
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
- message: Found a goto() call with a url that isn't prefixed with the base path.
line: 4
line: 6
column: 7
suggestions: null
- message: Found a goto() call with a url that isn't prefixed with the base path.
line: 7
column: 7
suggestions: null
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<script>
import { goto } from '$app/navigation';
const value = "/foo";
goto('/foo');
goto(value);
</script>
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
- message: Found a link with a url that isn't prefixed with the base path.
line: 1
line: 4
column: 10
suggestions: null
- message: Found a link with a url that isn't prefixed with the base path.
line: 2
line: 5
column: 9
suggestions: null
- message: Found a link with a url that isn't prefixed with the base path.
line: 3
line: 6
column: 9
suggestions: null
- message: Found a link with a url that isn't prefixed with the base path.
line: 7
column: 9
suggestions: null
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
<script>
const value = "/foo";
</script>
<a href="/foo">Click me!</a>
<a href={'/foo'}>Click me!</a>
<a href={'/' + 'foo'}>Click me!</a>
<a href={value}>Click me!</a>
Original file line number Diff line number Diff line change
@@ -1,4 +1,8 @@
- message: Found a pushState() call with a url that isn't prefixed with the base path.
line: 4
line: 6
column: 12
suggestions: null
- message: Found a pushState() call with a url that isn't prefixed with the base path.
line: 7
column: 12
suggestions: null
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<script>
import { pushState } from '$app/navigation';
const value = "/foo";
pushState('/foo');
pushState(value);
</script>
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
- message: Found a replaceState() call with a url that isn't prefixed with the
base path.
line: 4
line: 6
column: 15
suggestions: null
- message: Found a replaceState() call with a url that isn't prefixed with the
base path.
line: 7
column: 15
suggestions: null
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
<script>
import { replaceState } from '$app/navigation';
const value = "/foo";
replaceState('/foo');
replaceState(value);
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import { base } from '$app/paths';
import { goto } from '$app/navigation';
const value1 = base + '/foo/';
const value2 = `${base}/foo/`;
// eslint-disable-next-line prefer-template -- Testing both variants
goto(base + '/foo/');
goto(`${base}/foo/`);
goto(value1);
goto(value2);
</script>
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
<script>
import { base } from '$app/paths';
const value1 = base + '/foo/';
const value2 = `${base}/foo/`;
</script>

<a href={base + '/foo/'}>Click me!</a>
<a href={`${base}/foo/`}>Click me!</a>
<a href={value1}>Click me!</a>
<a href={value2}>Click me!</a>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import { base } from '$app/paths';
import { pushState } from '$app/navigation';
const value1 = base + '/foo/';
const value2 = `${base}/foo/`;
// eslint-disable-next-line prefer-template -- Testing both variants
pushState(base + '/foo/');
pushState(`${base}/foo/`);
pushState(value1);
pushState(value2);
</script>
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,12 @@
import { base } from '$app/paths';
import { replaceState } from '$app/navigation';
const value1 = base + '/foo/';
const value2 = `${base}/foo/`;
// eslint-disable-next-line prefer-template -- Testing both variants
replaceState(base + '/foo/');
replaceState(`${base}/foo/`);
replaceState(value1);
replaceState(value2);
</script>

0 comments on commit 8a9a43d

Please sign in to comment.