Skip to content

Commit 0b28198

Browse files
authored
feat: add control for executing rules based on Svelte/SvelteKit context (#980)
1 parent 8a04ace commit 0b28198

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

63 files changed

+833
-108
lines changed

.changeset/blue-swans-give.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'eslint-plugin-svelte': minor
3+
---
4+
5+
feat: Implement util to conditionally run lint based on Svelte version and SvelteKit routes etc

packages/eslint-plugin-svelte/src/rules/no-export-load-in-svelte-module-in-kit-pages.ts

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import type { TSESTree } from '@typescript-eslint/types';
22
import { createRule } from '../utils/index.js';
3-
import { isKitPageComponent } from '../utils/svelte-kit.js';
43

54
export default createRule('no-export-load-in-svelte-module-in-kit-pages', {
65
meta: {
@@ -16,12 +15,14 @@ export default createRule('no-export-load-in-svelte-module-in-kit-pages', {
1615
unexpected:
1716
'disallow exporting load functions in `*.svelte` module in SvelteKit page components.'
1817
},
19-
type: 'problem'
18+
type: 'problem',
19+
conditions: [
20+
{
21+
svelteKitFileTypes: ['+page.svelte', '+error.svelte', '+layout.svelte']
22+
}
23+
]
2024
},
2125
create(context) {
22-
if (!isKitPageComponent(context)) {
23-
return {};
24-
}
2526
let isModule = false;
2627
return {
2728
// <script context="module">

packages/eslint-plugin-svelte/src/rules/valid-prop-names-in-kit-pages.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import type { AST } from 'svelte-eslint-parser';
22
import type { TSESTree } from '@typescript-eslint/types';
33
import { createRule } from '../utils/index.js';
4-
import { isKitPageComponent } from '../utils/svelte-kit.js';
54
import type { RuleContext } from '../types.js';
65

76
const EXPECTED_PROP_NAMES = ['data', 'errors', 'form', 'snapshot'];
@@ -35,10 +34,14 @@ export default createRule('valid-prop-names-in-kit-pages', {
3534
messages: {
3635
unexpected: 'disallow props other than data or errors in SvelteKit page components.'
3736
},
38-
type: 'problem'
37+
type: 'problem',
38+
conditions: [
39+
{
40+
svelteKitFileTypes: ['+page.svelte', '+error.svelte', '+layout.svelte']
41+
}
42+
]
3943
},
4044
create(context) {
41-
if (!isKitPageComponent(context)) return {};
4245
let isScript = false;
4346
return {
4447
// <script>

packages/eslint-plugin-svelte/src/types.ts

+13
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import type { Root as SelectorRoot, Node as SelectorNode } from 'postcss-selecto
88
import type { ASTNode, ASTNodeWithParent, ASTNodeListener } from './types-for-node.js';
99
import type * as TS from 'typescript';
1010
import type { SourceLocation } from 'svelte-eslint-parser/lib/ast/common.js';
11+
import type { SvelteContext } from './utils/svelte-context.js';
1112

1213
export type { ASTNode, ASTNodeWithParent, ASTNodeListener };
1314
export interface RuleListener extends ASTNodeListener {
@@ -108,6 +109,18 @@ export interface PartialRuleMetaData {
108109
deprecated?: boolean;
109110
replacedBy?: string[] | { note: string };
110111
type: 'problem' | 'suggestion' | 'layout';
112+
/**
113+
* Conditions to determine whether this rule should be applied.
114+
* Multiple conditions can be specified as array, and the rule will be applied if any one of them matches (logical OR).
115+
* If not specified, the rule will be applied to all files.
116+
*/
117+
conditions?: {
118+
svelteVersions?: SvelteContext['svelteVersion'][];
119+
svelteFileTypes?: SvelteContext['svelteFileType'][];
120+
runes?: SvelteContext['runes'][];
121+
svelteKitVersions?: SvelteContext['svelteKitVersion'][];
122+
svelteKitFileTypes?: SvelteContext['svelteKitFileType'][];
123+
}[];
111124
}
112125

113126
export type RuleContext = {

packages/eslint-plugin-svelte/src/utils/get-package-json.ts

+17-13
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@ type PackageJson = {
1414
};
1515

1616
const isRunOnBrowser = !fs.readFileSync;
17-
const cache = createCache<PackageJson | null>();
17+
const packageJsonCache = createCache<PackageJson | null>();
18+
const packageJsonsCache = createCache<PackageJson[]>();
1819

1920
/**
2021
* Reads the `package.json` data in a given path.
@@ -49,34 +50,37 @@ function readPackageJson(dir: string): PackageJson | null {
4950
* @returns A found `package.json` data or `null`.
5051
* This object have additional property `filePath`.
5152
*/
52-
export function getPackageJson(startPath = 'a.js'): PackageJson | null {
53-
if (isRunOnBrowser) return null;
53+
export function getPackageJsons(startPath = 'a.js'): PackageJson[] {
54+
if (isRunOnBrowser) return [];
55+
56+
const cached = packageJsonsCache.get(startPath);
57+
if (cached) {
58+
return cached;
59+
}
60+
61+
const packageJsons: PackageJson[] = [];
5462
const startDir = path.dirname(path.resolve(startPath));
5563
let dir = startDir;
5664
let prevDir = '';
5765
let data = null;
5866

5967
do {
60-
data = cache.get(dir);
68+
data = packageJsonCache.get(dir);
6169
if (data) {
62-
if (dir !== startDir) {
63-
cache.set(startDir, data);
64-
}
65-
return data;
70+
packageJsons.push(data);
6671
}
6772

6873
data = readPackageJson(dir);
6974
if (data) {
70-
cache.set(dir, data);
71-
cache.set(startDir, data);
72-
return data;
75+
packageJsonCache.set(dir, data);
76+
packageJsons.push(data);
7377
}
7478

7579
// Go to next.
7680
prevDir = dir;
7781
dir = path.resolve(dir, '..');
7882
} while (dir !== prevDir);
7983

80-
cache.set(startDir, null);
81-
return null;
84+
packageJsonsCache.set(startDir, packageJsons);
85+
return packageJsons;
8286
}

packages/eslint-plugin-svelte/src/utils/index.ts

+56-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,50 @@
1-
import type { RuleModule, PartialRuleModule } from '../types.js';
1+
import type { RuleModule, PartialRuleModule, PartialRuleMetaData, RuleContext } from '../types.js';
2+
import { getSvelteContext, type SvelteContext } from '../utils/svelte-context.js';
3+
4+
function doesNotSatisfy<T>(actual: T, expected?: T[]): boolean {
5+
if (expected == null || expected.length === 0) {
6+
return false;
7+
}
8+
9+
return !expected.includes(actual);
10+
}
11+
12+
function satisfiesCondition(
13+
condition: NonNullable<PartialRuleMetaData['conditions']>[number],
14+
svelteContext: SvelteContext
15+
): boolean {
16+
if (
17+
doesNotSatisfy(svelteContext.svelteVersion, condition.svelteVersions) ||
18+
doesNotSatisfy(svelteContext.svelteFileType, condition.svelteFileTypes) ||
19+
doesNotSatisfy(svelteContext.runes, condition.runes) ||
20+
doesNotSatisfy(svelteContext.svelteKitVersion, condition.svelteKitVersions) ||
21+
doesNotSatisfy(svelteContext.svelteKitFileType, condition.svelteKitFileTypes)
22+
) {
23+
return false;
24+
}
25+
26+
return true;
27+
}
28+
29+
// export for testing
30+
export function shouldRun(
31+
svelteContext: SvelteContext | null,
32+
conditions: PartialRuleMetaData['conditions']
33+
): boolean {
34+
// If svelteContext is null, it means the rule might be executed based on the analysis result of a different parser.
35+
// In this case, always execute the rule.
36+
if (svelteContext == null || conditions == null || conditions.length === 0) {
37+
return true;
38+
}
39+
40+
for (const condition of conditions) {
41+
if (satisfiesCondition(condition, svelteContext)) {
42+
return true;
43+
}
44+
}
45+
46+
return false;
47+
}
248

349
/**
450
* Define the rule.
@@ -16,6 +62,14 @@ export function createRule(ruleName: string, rule: PartialRuleModule): RuleModul
1662
ruleName
1763
}
1864
},
19-
create: rule.create as never
65+
create(context: RuleContext) {
66+
const { conditions } = rule.meta;
67+
const svelteContext = getSvelteContext(context);
68+
if (!shouldRun(svelteContext, conditions)) {
69+
return {};
70+
}
71+
72+
return rule.create(context);
73+
}
2074
};
2175
}

0 commit comments

Comments
 (0)