Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add control for executing rules based on Svelte/SvelteKit context #980

Merged
merged 19 commits into from
Jan 11, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/blue-swans-give.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'eslint-plugin-svelte': minor
---

feat: Implement util to conditionally run lint based on Svelte version and SvelteKit routes etc
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import type { TSESTree } from '@typescript-eslint/types';
import { createRule } from '../utils/index.js';
import { isKitPageComponent } from '../utils/svelte-kit.js';

export default createRule('no-export-load-in-svelte-module-in-kit-pages', {
meta: {
Expand All @@ -8,7 +7,7 @@
description:
'disallow exporting load functions in `*.svelte` module in SvelteKit page components.',
category: 'Possible Errors',
// TODO Switch to recommended in the major version.

Check warning on line 10 in packages/eslint-plugin-svelte/src/rules/no-export-load-in-svelte-module-in-kit-pages.ts

View workflow job for this annotation

GitHub Actions / lint

Unexpected 'todo' comment: 'TODO Switch to recommended in the major...'
recommended: false
},
schema: [],
Expand All @@ -16,12 +15,14 @@
unexpected:
'disallow exporting load functions in `*.svelte` module in SvelteKit page components.'
},
type: 'problem'
type: 'problem',
conditions: [
{
svelteKitFileTypes: ['+page.svelte', '+error.svelte', '+layout.svelte']
}
]
},
create(context) {
if (!isKitPageComponent(context)) {
return {};
}
let isModule = false;
return {
// <script context="module">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type { AST } from 'svelte-eslint-parser';
import type { TSESTree } from '@typescript-eslint/types';
import { createRule } from '../utils/index.js';
import { isKitPageComponent } from '../utils/svelte-kit.js';
import type { RuleContext } from '../types.js';

const EXPECTED_PROP_NAMES = ['data', 'errors', 'form', 'snapshot'];
Expand Down Expand Up @@ -35,10 +34,14 @@ export default createRule('valid-prop-names-in-kit-pages', {
messages: {
unexpected: 'disallow props other than data or errors in SvelteKit page components.'
},
type: 'problem'
type: 'problem',
conditions: [
{
svelteKitFileTypes: ['+page.svelte', '+error.svelte', '+layout.svelte']
}
]
},
create(context) {
if (!isKitPageComponent(context)) return {};
let isScript = false;
return {
// <script>
Expand Down
13 changes: 13 additions & 0 deletions packages/eslint-plugin-svelte/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { Root as SelectorRoot, Node as SelectorNode } from 'postcss-selecto
import type { ASTNode, ASTNodeWithParent, ASTNodeListener } from './types-for-node.js';
import type * as TS from 'typescript';
import type { SourceLocation } from 'svelte-eslint-parser/lib/ast/common.js';
import type { SvelteContext } from './utils/svelte-context.js';

export type { ASTNode, ASTNodeWithParent, ASTNodeListener };
export interface RuleListener extends ASTNodeListener {
Expand Down Expand Up @@ -108,6 +109,18 @@ export interface PartialRuleMetaData {
deprecated?: boolean;
replacedBy?: string[] | { note: string };
type: 'problem' | 'suggestion' | 'layout';
/**
* Conditions to determine whether this rule should be applied.
* Multiple conditions can be specified as array, and the rule will be applied if any one of them matches (logical OR).
* If not specified, the rule will be applied to all files.
*/
conditions?: {
svelteVersions?: SvelteContext['svelteVersion'][];
svelteFileTypes?: SvelteContext['svelteFileType'][];
runes?: SvelteContext['runes'][];
svelteKitVersions?: SvelteContext['svelteKitVersion'][];
svelteKitFileTypes?: SvelteContext['svelteKitFileType'][];
}[];
}

export type RuleContext = {
Expand Down
30 changes: 17 additions & 13 deletions packages/eslint-plugin-svelte/src/utils/get-package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ type PackageJson = {
};

const isRunOnBrowser = !fs.readFileSync;
const cache = createCache<PackageJson | null>();
const packageJsonCache = createCache<PackageJson | null>();
const packageJsonsCache = createCache<PackageJson[]>();

/**
* Reads the `package.json` data in a given path.
Expand Down Expand Up @@ -49,34 +50,37 @@ function readPackageJson(dir: string): PackageJson | null {
* @returns A found `package.json` data or `null`.
* This object have additional property `filePath`.
*/
export function getPackageJson(startPath = 'a.js'): PackageJson | null {
if (isRunOnBrowser) return null;
export function getPackageJsons(startPath = 'a.js'): PackageJson[] {
if (isRunOnBrowser) return [];

const cached = packageJsonsCache.get(startPath);
if (cached) {
return cached;
}

const packageJsons: PackageJson[] = [];
const startDir = path.dirname(path.resolve(startPath));
let dir = startDir;
let prevDir = '';
let data = null;

do {
data = cache.get(dir);
data = packageJsonCache.get(dir);
if (data) {
if (dir !== startDir) {
cache.set(startDir, data);
}
return data;
packageJsons.push(data);
}

data = readPackageJson(dir);
if (data) {
cache.set(dir, data);
cache.set(startDir, data);
return data;
packageJsonCache.set(dir, data);
packageJsons.push(data);
}

// Go to next.
prevDir = dir;
dir = path.resolve(dir, '..');
} while (dir !== prevDir);

cache.set(startDir, null);
return null;
packageJsonsCache.set(startDir, packageJsons);
return packageJsons;
}
58 changes: 56 additions & 2 deletions packages/eslint-plugin-svelte/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,50 @@
import type { RuleModule, PartialRuleModule } from '../types.js';
import type { RuleModule, PartialRuleModule, PartialRuleMetaData, RuleContext } from '../types.js';
import { getSvelteContext, type SvelteContext } from '../utils/svelte-context.js';

function doesNotSatisfy<T>(actual: T, expected?: T[]): boolean {
if (expected == null || expected.length === 0) {
return false;
}

return !expected.includes(actual);
}

function satisfiesCondition(
condition: NonNullable<PartialRuleMetaData['conditions']>[number],
svelteContext: SvelteContext
): boolean {
if (
doesNotSatisfy(svelteContext.svelteVersion, condition.svelteVersions) ||
doesNotSatisfy(svelteContext.svelteFileType, condition.svelteFileTypes) ||
doesNotSatisfy(svelteContext.runes, condition.runes) ||
doesNotSatisfy(svelteContext.svelteKitVersion, condition.svelteKitVersions) ||
doesNotSatisfy(svelteContext.svelteKitFileType, condition.svelteKitFileTypes)
) {
return false;
}

return true;
}

// export for testing
export function shouldRun(
svelteContext: SvelteContext | null,
conditions: PartialRuleMetaData['conditions']
): boolean {
// If svelteContext is null, it means the rule might be executed based on the analysis result of a different parser.
// In this case, always execute the rule.
if (svelteContext == null || conditions == null || conditions.length === 0) {
return true;
}

for (const condition of conditions) {
if (satisfiesCondition(condition, svelteContext)) {
return true;
}
}

return false;
}

/**
* Define the rule.
Expand All @@ -16,6 +62,14 @@ export function createRule(ruleName: string, rule: PartialRuleModule): RuleModul
ruleName
}
},
create: rule.create as never
create(context: RuleContext) {
const { conditions } = rule.meta;
const svelteContext = getSvelteContext(context);
if (!shouldRun(svelteContext, conditions)) {
return {};
}

return rule.create(context);
}
};
}
Loading
Loading