UNPKG

eslint-plugin-svelte

Version:
233 lines (232 loc) 9.24 kB
import { createRule } from '../utils/index.js'; import { ReferenceTracker } from '@eslint-community/eslint-utils'; import { findVariable } from '../utils/ast-utils.js'; import { extractExpressionPrefixVariable } from '../utils/expression-affixes.js'; export default createRule('no-navigation-without-base', { meta: { deprecated: true, replacedBy: ['no-navigation-without-resolve'], docs: { description: 'disallow using navigation (links, goto, pushState, replaceState) without the base path', category: 'SvelteKit', recommended: false }, schema: [ { type: 'object', properties: { ignoreGoto: { type: 'boolean' }, ignoreLinks: { type: 'boolean' }, ignorePushState: { type: 'boolean' }, ignoreReplaceState: { type: 'boolean' } }, additionalProperties: false } ], messages: { gotoNotPrefixed: "Found a goto() call with a url that isn't prefixed with the base path.", linkNotPrefixed: "Found a link with a url that isn't prefixed with the base path.", pushStateNotPrefixed: "Found a pushState() call with a url that isn't prefixed with the base path.", replaceStateNotPrefixed: "Found a replaceState() call with a url that isn't prefixed with the base path." }, type: 'suggestion', conditions: [ { svelteKitVersions: ['1.0.0-next', '1', '2'] } ] }, create(context) { let basePathNames = new Set(); return { Program() { const referenceTracker = new ReferenceTracker(context.sourceCode.scopeManager.globalScope); basePathNames = extractBasePathReferences(referenceTracker, context); const { goto: gotoCalls, pushState: pushStateCalls, replaceState: replaceStateCalls } = extractFunctionCallReferences(referenceTracker); if (context.options[0]?.ignoreGoto !== true) { for (const gotoCall of gotoCalls) { checkGotoCall(context, gotoCall, basePathNames); } } if (context.options[0]?.ignorePushState !== true) { for (const pushStateCall of pushStateCalls) { checkShallowNavigationCall(context, pushStateCall, basePathNames, 'pushStateNotPrefixed'); } } if (context.options[0]?.ignoreReplaceState !== true) { for (const replaceStateCall of replaceStateCalls) { checkShallowNavigationCall(context, replaceStateCall, basePathNames, 'replaceStateNotPrefixed'); } } }, SvelteAttribute(node) { if (context.options[0]?.ignoreLinks === true || node.parent.parent.type !== 'SvelteElement' || node.parent.parent.kind !== 'html' || node.parent.parent.name.type !== 'SvelteName' || node.parent.parent.name.name !== 'a' || node.key.name !== 'href') { return; } const hrefValue = node.value[0]; if (hrefValue.type === 'SvelteLiteral') { if (!expressionIsAbsolute(hrefValue) && !expressionIsFragment(hrefValue)) { context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' }); } return; } if (!expressionStartsWithBase(context, hrefValue.expression, basePathNames) && !expressionIsAbsolute(hrefValue.expression) && !expressionIsFragment(hrefValue.expression)) { context.report({ loc: hrefValue.loc, messageId: 'linkNotPrefixed' }); } } }; } }); // Extract all imports of the base path function extractBasePathReferences(referenceTracker, context) { const set = new Set(); for (const { node } of referenceTracker.iterateEsmReferences({ '$app/paths': { [ReferenceTracker.ESM]: true, base: { [ReferenceTracker.READ]: true } } })) { if (node.type === 'ImportSpecifier') { const variable = findVariable(context, node.local); if (variable === null) { continue; } for (const reference of variable.references) { if (reference.identifier.type === 'Identifier') set.add(reference.identifier); } } else if (node.type === 'MemberExpression' && node.property.type === 'Identifier' && node.property.name === 'base') { set.add(node.property); } } return set; } // Extract all references to goto, pushState and replaceState function extractFunctionCallReferences(referenceTracker) { const rawReferences = Array.from(referenceTracker.iterateEsmReferences({ '$app/navigation': { [ReferenceTracker.ESM]: true, goto: { [ReferenceTracker.CALL]: true }, pushState: { [ReferenceTracker.CALL]: true }, replaceState: { [ReferenceTracker.CALL]: true } } })); return { goto: rawReferences .filter(({ path }) => path[path.length - 1] === 'goto') .map(({ node }) => node), pushState: rawReferences .filter(({ path }) => path[path.length - 1] === 'pushState') .map(({ node }) => node), replaceState: rawReferences .filter(({ path }) => path[path.length - 1] === 'replaceState') .map(({ node }) => node) }; } // Actual function checking function checkGotoCall(context, call, basePathNames) { if (call.arguments.length < 1) { return; } const url = call.arguments[0]; if (url.type === 'SpreadElement' || !expressionStartsWithBase(context, url, basePathNames)) { context.report({ loc: url.loc, messageId: 'gotoNotPrefixed' }); } } function checkShallowNavigationCall(context, call, basePathNames, messageId) { if (call.arguments.length < 1) { return; } const url = call.arguments[0]; if (url.type === 'SpreadElement' || (!expressionIsEmpty(url) && !expressionStartsWithBase(context, url, basePathNames))) { context.report({ loc: url.loc, messageId }); } } // Helper functions function expressionStartsWithBase(context, url, basePathNames) { const prefixVariable = extractExpressionPrefixVariable(context, url); return prefixVariable !== null && basePathNames.has(prefixVariable); } function expressionIsEmpty(url) { return ((url.type === 'Literal' && url.value === '') || (url.type === 'TemplateLiteral' && url.expressions.length === 0 && url.quasis.length === 1 && url.quasis[0].value.raw === '')); } function expressionIsAbsolute(url) { switch (url.type) { case 'BinaryExpression': return binaryExpressionIsAbsolute(url); case 'Literal': return typeof url.value === 'string' && urlValueIsAbsolute(url.value); case 'SvelteLiteral': return urlValueIsAbsolute(url.value); case 'TemplateLiteral': return templateLiteralIsAbsolute(url); default: return false; } } function binaryExpressionIsAbsolute(url) { return ((url.left.type !== 'PrivateIdentifier' && expressionIsAbsolute(url.left)) || expressionIsAbsolute(url.right)); } function templateLiteralIsAbsolute(url) { return (url.expressions.some(expressionIsAbsolute) || url.quasis.some((quasi) => urlValueIsAbsolute(quasi.value.raw))); } function urlValueIsAbsolute(url) { return /^[+a-z]*:/i.test(url); } function expressionIsFragment(url) { switch (url.type) { case 'BinaryExpression': return binaryExpressionIsFragment(url); case 'Literal': return typeof url.value === 'string' && urlValueIsFragment(url.value); case 'SvelteLiteral': return urlValueIsFragment(url.value); case 'TemplateLiteral': return templateLiteralIsFragment(url); default: return false; } } function binaryExpressionIsFragment(url) { return url.left.type !== 'PrivateIdentifier' && expressionIsFragment(url.left); } function templateLiteralIsFragment(url) { return ((url.expressions.length >= 1 && expressionIsFragment(url.expressions[0])) || (url.quasis.length >= 1 && urlValueIsFragment(url.quasis[0].value.raw))); } function urlValueIsFragment(url) { return url.startsWith('#'); }