Skip to content

Commit 858fc22

Browse files
committed
feat: adding sort-scripts-elements rule
1 parent 386efc8 commit 858fc22

File tree

8 files changed

+165
-6
lines changed

8 files changed

+165
-6
lines changed

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

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -382,6 +382,11 @@ export interface RuleOptions {
382382
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-attributes/
383383
*/
384384
'svelte/sort-attributes'?: Linter.RuleEntry<SvelteSortAttributes>
385+
/**
386+
* enforce order of elements in Svelte scripts section
387+
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-scripts-elements/
388+
*/
389+
'svelte/sort-scripts-sections'?: Linter.RuleEntry<SvelteSortScriptsElements>
385390
/**
386391
* enforce consistent spacing after the `<!--` and before the `-->` in a HTML comment
387392
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/spaced-html-comment/
@@ -417,9 +422,9 @@ export interface RuleOptions {
417422
/* ======= Declarations ======= */
418423
// ----- svelte/@typescript-eslint/no-unnecessary-condition -----
419424
type SvelteTypescriptEslintNoUnnecessaryCondition = []|[{
420-
425+
421426
allowConstantLoopConditions?: boolean
422-
427+
423428
allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean
424429
}]
425430
// ----- svelte/block-lang -----
@@ -442,7 +447,7 @@ type SvelteCommentDirective = []|[{
442447
// ----- svelte/consistent-selector-style -----
443448
type SvelteConsistentSelectorStyle = []|[{
444449
checkGlobal?: boolean
445-
450+
446451
style?: []|[("class" | "id" | "type")]|[("class" | "id" | "type"), ("class" | "id" | "type")]|[("class" | "id" | "type"), ("class" | "id" | "type"), ("class" | "id" | "type")]
447452
}]
448453
// ----- svelte/first-attribute-linebreak -----
@@ -537,11 +542,11 @@ type SvelteNoReactiveReassign = []|[{
537542
}]
538543
// ----- svelte/no-restricted-html-elements -----
539544
type SvelteNoRestrictedHtmlElements = [(string | {
540-
545+
541546
elements?: [string, ...(string)[]]
542547
message?: string
543548
}), ...((string | {
544-
549+
545550
elements?: [string, ...(string)[]]
546551
message?: string
547552
}))[]]
@@ -557,7 +562,7 @@ type SvelteNoTrailingSpaces = []|[{
557562
}]
558563
// ----- svelte/no-unknown-style-directive-property -----
559564
type SvelteNoUnknownStyleDirectiveProperty = []|[{
560-
565+
561566
ignoreProperties?: [string, ...(string)[]]
562567
ignorePrefixed?: boolean
563568
}]
@@ -613,6 +618,12 @@ type SvelteSortAttributes = []|[{
613618
})[]
614619
alphabetical?: boolean
615620
}]
621+
// ----- svelte/sort-scripts-elements -----
622+
type SvelteSortScriptsElements = []|[{
623+
order?: (string | [string, ...(string)[]] | {
624+
match: (string | [string, ...(string)[]])
625+
})[]
626+
}]
616627
// ----- svelte/spaced-html-comment -----
617628
type SvelteSpacedHtmlComment = []|[("always" | "never")]
618629
// ----- svelte/valid-compile -----
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { createRule } from '../utils/index.js';
2+
import type { RuleContext } from '../types.js';
3+
import type { AST } from 'svelte-eslint-parser';
4+
5+
const DEFAULT_ORDER: string[] = [
6+
'ImportDeclaration',
7+
'TSTypeAliasDeclaration',
8+
'VariableDeclaration',
9+
'FunctionDeclaration'
10+
];
11+
12+
// creating and exporting the rule
13+
export default createRule('sort-scripts-elements', {
14+
meta: {
15+
docs: {
16+
description: 'enforce order of elements in the Svelte scripts sections',
17+
category: 'Stylistic Issues',
18+
recommended: false,
19+
conflictWithPrettier: false
20+
},
21+
schema: [],
22+
messages: {
23+
scriptsIsNotSorted: 'Scripts is not sorted.'
24+
},
25+
type: 'layout',
26+
fixable: 'code'
27+
},
28+
create(context: RuleContext) {
29+
const sourceCode = context.sourceCode;
30+
const MAPPING = new Map<string, number>(DEFAULT_ORDER.map((value, index) => [value, index]));
31+
32+
return {
33+
SvelteScriptElement(node: AST.SvelteScriptElement) {
34+
// do not accept scripts without closing tags
35+
if (node.endTag === null) return;
36+
const svelteEndTag: AST.SvelteEndTag = node.endTag;
37+
38+
// collect scripts statement
39+
const statements = node.body;
40+
// do not sort when we only have one elements
41+
if (statements.length <= 1) return;
42+
43+
const seens = new Set<string>();
44+
let current: string = statements[0].type;
45+
46+
let sortingRequired = false;
47+
for (let i = 0; i < statements.length; i++) {
48+
// if we are the same as previous
49+
if (seens.has(statements[i].type) && statements[i].type === current) {
50+
continue;
51+
}
52+
53+
if (i > 0) {
54+
const previousOrderIndex = MAPPING.get(current);
55+
const currentOrderIndex = MAPPING.get(statements[i].type);
56+
if (previousOrderIndex !== undefined && currentOrderIndex !== undefined) {
57+
if (previousOrderIndex > currentOrderIndex) {
58+
sortingRequired = true;
59+
break;
60+
}
61+
}
62+
}
63+
64+
// mark the node type as seen
65+
seens.add(statements[i].type);
66+
current = statements[i].type;
67+
}
68+
69+
if (!sortingRequired) return;
70+
71+
context.report({
72+
node,
73+
messageId: 'scriptsIsNotSorted',
74+
fix: (fixer) => {
75+
const foo: { order: number; text: string }[] = [];
76+
77+
for (let i = 0; i < statements.length; i++) {
78+
// Getting the comments between previous and current statement
79+
const comments = sourceCode
80+
.getCommentsInside({
81+
type: 'Null',
82+
range: [
83+
// Get comment between the last statement and current
84+
i === 0 ? node.startTag.range[1] : statements[i - 1].range[1],
85+
i === statements.length - 1 ? svelteEndTag.range[0] : statements[i].range[0]
86+
]
87+
})
88+
.map((comment) => sourceCode.getText(comment));
89+
90+
/**
91+
* We need to handle the missing \t\n
92+
* Example when i === 0 we should take the startTag.range[1] up to the first statement if no comments
93+
* Same between statements etc.
94+
* sourceCode.getText(statements[i], (statements[i].range[0]) - (node.startTag.range[1] + 1))
95+
*/
96+
foo.push({
97+
order: MAPPING.get(statements[i].type) ?? Number.MAX_SAFE_INTEGER,
98+
text: comments.join('') + sourceCode.getText(statements[i])
99+
});
100+
}
101+
102+
const text = foo
103+
.sort((a, b) => a.order - b.order)
104+
.map(({ text }) => text)
105+
.join('\n');
106+
107+
return [
108+
fixer.replaceTextRange([node.startTag.range[1] + 1, svelteEndTag.range[0] - 1], text)
109+
];
110+
}
111+
});
112+
}
113+
};
114+
}
115+
});

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ import requireStoresInit from '../rules/require-stores-init.js';
7575
import shorthandAttribute from '../rules/shorthand-attribute.js';
7676
import shorthandDirective from '../rules/shorthand-directive.js';
7777
import sortAttributes from '../rules/sort-attributes.js';
78+
import sortScriptsElements from '../rules/sort-scripts-elements.js';
7879
import spacedHtmlComment from '../rules/spaced-html-comment.js';
7980
import system from '../rules/system.js';
8081
import validCompile from '../rules/valid-compile.js';
@@ -156,6 +157,7 @@ export const rules = [
156157
shorthandAttribute,
157158
shorthandDirective,
158159
sortAttributes,
160+
sortScriptsElements,
159161
spacedHtmlComment,
160162
system,
161163
validCompile,
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
- message: Scripts is not sorted.
2+
line: 1
3+
column: 1
4+
suggestions: null
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
<script lang="ts">
2+
type Foo = string;
3+
import Bar from './bar.svelte';
4+
5+
function hello(): string {
6+
return 'world';
7+
}
8+
</script>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<script lang="ts">
2+
import Bar from './bar.svelte';
3+
type Foo = string;
4+
</script>
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
<script>
2+
import component from './foo';
3+
</script>
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { RuleTester } from '../../utils/eslint-compat.js';
2+
import rule from '../../../src/rules/sort-scripts-elements.js';
3+
import { loadTestCases } from '../../utils/utils.js';
4+
5+
const tester = new RuleTester({
6+
languageOptions: {
7+
ecmaVersion:"latest",
8+
sourceType: 'module'
9+
}
10+
});
11+
12+
tester.run('sort-scripts-elements', rule as any, loadTestCases('sort-scripts-elements'));

0 commit comments

Comments
 (0)