Skip to content
Draft
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
23 changes: 17 additions & 6 deletions packages/eslint-plugin-svelte/src/rule-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,11 @@ export interface RuleOptions {
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-attributes/
*/
'svelte/sort-attributes'?: Linter.RuleEntry<SvelteSortAttributes>
/**
* enforce order of elements in Svelte scripts section
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/sort-scripts-elements/
*/
'svelte/sort-scripts-sections'?: Linter.RuleEntry<SvelteSortScriptsElements>
/**
* enforce consistent spacing after the `<!--` and before the `-->` in a HTML comment
* @see https://sveltejs.github.io/eslint-plugin-svelte/rules/spaced-html-comment/
Expand Down Expand Up @@ -417,9 +422,9 @@ export interface RuleOptions {
/* ======= Declarations ======= */
// ----- svelte/@typescript-eslint/no-unnecessary-condition -----
type SvelteTypescriptEslintNoUnnecessaryCondition = []|[{

allowConstantLoopConditions?: boolean

allowRuleToRunWithoutStrictNullChecksIKnowWhatIAmDoing?: boolean
}]
// ----- svelte/block-lang -----
Expand All @@ -442,7 +447,7 @@ type SvelteCommentDirective = []|[{
// ----- svelte/consistent-selector-style -----
type SvelteConsistentSelectorStyle = []|[{
checkGlobal?: boolean

style?: []|[("class" | "id" | "type")]|[("class" | "id" | "type"), ("class" | "id" | "type")]|[("class" | "id" | "type"), ("class" | "id" | "type"), ("class" | "id" | "type")]
}]
// ----- svelte/first-attribute-linebreak -----
Expand Down Expand Up @@ -537,11 +542,11 @@ type SvelteNoReactiveReassign = []|[{
}]
// ----- svelte/no-restricted-html-elements -----
type SvelteNoRestrictedHtmlElements = [(string | {

elements?: [string, ...(string)[]]
message?: string
}), ...((string | {

elements?: [string, ...(string)[]]
message?: string
}))[]]
Expand All @@ -557,7 +562,7 @@ type SvelteNoTrailingSpaces = []|[{
}]
// ----- svelte/no-unknown-style-directive-property -----
type SvelteNoUnknownStyleDirectiveProperty = []|[{

ignoreProperties?: [string, ...(string)[]]
ignorePrefixed?: boolean
}]
Expand Down Expand Up @@ -613,6 +618,12 @@ type SvelteSortAttributes = []|[{
})[]
alphabetical?: boolean
}]
// ----- svelte/sort-scripts-elements -----
type SvelteSortScriptsElements = []|[{
order?: (string | [string, ...(string)[]] | {
match: (string | [string, ...(string)[]])
})[]
}]
// ----- svelte/spaced-html-comment -----
type SvelteSpacedHtmlComment = []|[("always" | "never")]
// ----- svelte/valid-compile -----
Expand Down
127 changes: 127 additions & 0 deletions packages/eslint-plugin-svelte/src/rules/sort-scripts-elements.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import { createRule } from '../utils/index.js';
import type { RuleContext } from '../types.js';
import type { AST } from 'svelte-eslint-parser';
import type { Range } from 'svelte-eslint-parser/lib/ast/common.js';

const DEFAULT_ORDER: string[] = [
'ImportDeclaration',
'TSTypeAliasDeclaration',
'TSInterfaceDeclaration',
'VariableDeclaration',
'FunctionDeclaration'
];

// creating and exporting the rule
export default createRule('sort-scripts-elements', {
meta: {
docs: {
description: 'enforce order of elements in the Svelte scripts sections',
category: 'Stylistic Issues',
recommended: false,
conflictWithPrettier: false
},
schema: [],
messages: {
scriptsIsNotSorted: 'Scripts is not sorted.'
},
type: 'layout',
fixable: 'code'
},
create(context: RuleContext) {
const sourceCode = context.sourceCode;
const MAPPING = new Map<string, number>(DEFAULT_ORDER.map((value, index) => [value, index]));

return {
SvelteScriptElement(node: AST.SvelteScriptElement) {
// do not accept scripts without closing tags
if (node.endTag === null) return;
const svelteEndTag: AST.SvelteEndTag = node.endTag;

// collect scripts statement
const statements = node.body;
// do not sort when we only have one elements
if (statements.length <= 1) return;

const seens = new Set<string>();
let current: string = statements[0].type;

let sortingRequired = false;
for (let i = 0; i < statements.length; i++) {
// if we are the same as previous
if (seens.has(statements[i].type) && statements[i].type === current) {
continue;
}

if (i > 0) {
const previousOrderIndex = MAPPING.get(current);
const currentOrderIndex = MAPPING.get(statements[i].type);
if (previousOrderIndex !== undefined && currentOrderIndex !== undefined) {
if (previousOrderIndex > currentOrderIndex) {
sortingRequired = true;
break;
}
}
}

// mark the node type as seen
seens.add(statements[i].type);
current = statements[i].type;
}

if (!sortingRequired) return;

context.report({
node,
messageId: 'scriptsIsNotSorted',
fix: (fixer) => {
const foo: { order: number; text: string }[] = [];

const ranges: Range[] = [];
for (let i = 0; i < statements.length; i++) {
// the first token start after the previous statement
let start: number;
if (i === 0) {
// special case: first statement, we copy everything from <script> <--
start = node.startTag.range[1];
} else {
start = ranges[i - 1][1];
}

let end: number;
if (i === statements.length - 1) {
end = svelteEndTag.range[0] - 1;
} else {
end = statements[i].range[1];
}

ranges.push([start, end]);

/**
* We need to handle the missing \t\n
* Example when i === 0 we should take the startTag.range[1] up to the first statement if no comments
* Same between statements etc.
* sourceCode.getText(statements[i], (statements[i].range[0]) - (node.startTag.range[1] + 1))
*/
foo.push({
order: MAPPING.get(statements[i].type) ?? Number.MAX_SAFE_INTEGER,
text: sourceCode.getText({
range: [start, end],
type: 'Null'
})
});
}

const text = foo
.sort((a, b) => a.order - b.order)
.map(({ text }) => text)
.join('');

return [
fixer.replaceTextRange([node.startTag.range[1], svelteEndTag.range[0] - 1], text)
];
}
});
}
};
}
});
2 changes: 2 additions & 0 deletions packages/eslint-plugin-svelte/src/utils/rules.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ import requireStoresInit from '../rules/require-stores-init.js';
import shorthandAttribute from '../rules/shorthand-attribute.js';
import shorthandDirective from '../rules/shorthand-directive.js';
import sortAttributes from '../rules/sort-attributes.js';
import sortScriptsElements from '../rules/sort-scripts-elements.js';
import spacedHtmlComment from '../rules/spaced-html-comment.js';
import system from '../rules/system.js';
import validCompile from '../rules/valid-compile.js';
Expand Down Expand Up @@ -156,6 +157,7 @@ export const rules = [
shorthandAttribute,
shorthandDirective,
sortAttributes,
sortScriptsElements,
spacedHtmlComment,
system,
validCompile,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- message: Scripts is not sorted.
line: 1
column: 1
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script lang="ts">
let count: Foo = $state({ bar: 'dummy' });
interface Foo {
bar: string;
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script lang="ts">
interface Foo {
bar: string;
}
let count: Foo = $state({ bar: 'dummy' });
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
- message: Scripts is not sorted.
line: 1
column: 1
suggestions: null
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
type Foo = string;
import Bar from './bar.svelte';

function hello(): string {
return 'world';
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script lang="ts">
import Bar from './bar.svelte';
type Foo = string;

function hello(): string {
return 'world';
}
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<script>
// I am not a problem
import component from './foo';
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<script>
import component from './foo';
</script>
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { RuleTester } from '../../utils/eslint-compat.js';
import rule from '../../../src/rules/sort-scripts-elements.js';
import { loadTestCases } from '../../utils/utils.js';

const tester = new RuleTester({
languageOptions: {
ecmaVersion:"latest",
sourceType: 'module'
}
});

tester.run('sort-scripts-elements', rule as any, loadTestCases('sort-scripts-elements'));