├── test
├── samples
│ ├── ignore-styles
│ │ ├── expected.json
│ │ ├── Input.svelte
│ │ └── .eslintrc.js
│ ├── line-endings
│ │ ├── preserve_line_endings
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── script-reference
│ │ ├── expected.json
│ │ ├── .eslintrc.js
│ │ └── Input.svelte
│ ├── compiler-error
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── unused-write-only-store
│ │ ├── expected.json
│ │ ├── .eslintrc.js
│ │ └── Input.svelte
│ ├── typescript-bind-reference
│ │ ├── expected.json
│ │ ├── Input.svelte
│ │ └── .eslintrc.js
│ ├── indentation
│ │ ├── Input.svelte
│ │ ├── .eslintrc.js
│ │ └── expected.json
│ ├── labels
│ │ ├── Input.svelte
│ │ ├── .eslintrc.js
│ │ └── expected.json
│ ├── self-assignment
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── template-quotes
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── html
│ │ ├── package.json
│ │ ├── .eslintrc.js
│ │ ├── expected.json
│ │ ├── index.js
│ │ └── Input.svelte
│ ├── typescript-indentation
│ │ ├── Input.svelte
│ │ ├── .eslintrc.js
│ │ └── expected.json
│ ├── typescript-template-quotes
│ │ ├── Input.svelte
│ │ ├── .eslintrc.js
│ │ └── expected.json
│ ├── typescript-unsafe-member-access
│ │ ├── external-file.ts
│ │ ├── tsconfig.json
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── typescript-type-aware-rules
│ │ ├── tsconfig.json
│ │ ├── Input.svelte
│ │ ├── .eslintrc.js
│ │ └── expected.json
│ ├── module-context
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── scope
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── block-filenames
│ │ ├── Input.svelte
│ │ ├── .eslintrc.js
│ │ └── expected.json
│ ├── typescript-block-filenames
│ │ ├── Input.svelte
│ │ ├── .eslintrc.js
│ │ └── expected.json
│ ├── typescript-imports
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── typescript-peer-dependency
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── typescript
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ ├── typescript-lazy
│ │ ├── .eslintrc.js
│ │ ├── Input.svelte
│ │ └── expected.json
│ └── .eslintrc.js
├── node_modules
│ └── eslint-plugin-svelte3
│ │ └── index.js
└── index.js
├── src
├── state.js
├── index.js
├── block.js
├── processor_options.js
├── utils.js
├── postprocess.js
├── mapping.js
└── preprocess.js
├── rollup.config.js
├── .gitignore
├── .github
└── workflows
│ └── ci.yml
├── LICENSE
├── OTHER_PLUGINS.md
├── package.json
├── INTEGRATIONS.md
├── CHANGELOG.md
├── README.md
└── index.js
/test/samples/ignore-styles/expected.json:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/test/samples/line-endings/preserve_line_endings:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/test/samples/script-reference/expected.json:
--------------------------------------------------------------------------------
1 | []
--------------------------------------------------------------------------------
/test/samples/compiler-error/Input.svelte:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/test/node_modules/eslint-plugin-svelte3/index.js:
--------------------------------------------------------------------------------
1 | module.exports = require('../../..');
2 |
--------------------------------------------------------------------------------
/test/samples/labels/Input.svelte:
--------------------------------------------------------------------------------
1 |
6 |
--------------------------------------------------------------------------------
/test/samples/script-reference/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'no-unused-vars': 'error',
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/test/samples/self-assignment/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | 'no-self-assign': 'error',
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/test/samples/template-quotes/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | quotes: ['error', 'single'],
4 | },
5 | };
6 |
--------------------------------------------------------------------------------
/test/samples/template-quotes/Input.svelte:
--------------------------------------------------------------------------------
1 | {'foo'}
2 | {"bar"}
3 |
$imported = 'clicked' }/>
13 |
--------------------------------------------------------------------------------
/test/samples/typescript-peer-dependency/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4 | plugins: ['@typescript-eslint'],
5 | settings: {
6 | 'svelte3/typescript': true,
7 | },
8 | rules: {
9 | indent: ['error', 'tab'],
10 | semi: 'error',
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/test/samples/typescript/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4 | plugins: ['@typescript-eslint'],
5 | settings: {
6 | 'svelte3/typescript': require('typescript'),
7 | },
8 | rules: {
9 | indent: ['error', 'tab'],
10 | semi: 'error',
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/test/samples/typescript-lazy/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
4 | plugins: ['@typescript-eslint'],
5 | settings: {
6 | 'svelte3/typescript': () => require('typescript'),
7 | },
8 | rules: {
9 | indent: ['error', 'tab'],
10 | semi: 'error',
11 | },
12 | };
13 |
--------------------------------------------------------------------------------
/test/samples/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | "extends": ["plugin:svelte3/defaultWithJsx"],
3 | root: true,
4 | parserOptions: {
5 | ecmaVersion: 2019,
6 | sourceType: 'module',
7 | },
8 | env: {
9 | es6: true,
10 | browser: true,
11 | },
12 | plugins: ['svelte3'],
13 | overrides: [
14 | {
15 | files: ['**/*.svelte'],
16 | processor: 'svelte3/svelte3',
17 | },
18 | ]
19 | };
--------------------------------------------------------------------------------
/test/samples/typescript/Input.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
21 |
22 |
{b}
23 |
{x}
24 |
--------------------------------------------------------------------------------
/test/samples/typescript-lazy/Input.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
21 |
22 |
{b}
23 |
{x}
24 |
--------------------------------------------------------------------------------
/test/samples/typescript-imports/Input.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
new Thing2()}>
11 |
12 |
--------------------------------------------------------------------------------
/test/samples/typescript-peer-dependency/Input.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
21 |
22 |
{b}
23 |
{x}
24 |
--------------------------------------------------------------------------------
/test/samples/block-filenames/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | curly: 'error',
4 | 'no-undef': 'error',
5 | },
6 | settings: {
7 | 'svelte3/named-blocks': true
8 | },
9 | overrides: [
10 | {
11 | files: ['**/*.svelte/*_template.js'],
12 | rules: {
13 | curly: 'off',
14 | },
15 | },
16 | {
17 | files: ['**/*.svelte/*_module.js'],
18 | rules: {
19 | 'no-undef': 'off',
20 | },
21 | },
22 | ],
23 | };
24 |
--------------------------------------------------------------------------------
/test/samples/typescript-unsafe-member-access/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: "@typescript-eslint/parser",
3 | plugins: [
4 | "@typescript-eslint",
5 | ],
6 | parserOptions: {
7 | project: ["./tsconfig.json"],
8 | tsconfigRootDir: __dirname,
9 | extraFileExtensions: [".svelte"],
10 | },
11 | settings: {
12 | "svelte3/typescript": require("typescript"),
13 | },
14 | rules: {
15 | "@typescript-eslint/no-unsafe-member-access": "error",
16 | },
17 | };
18 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import { preprocess } from "./preprocess.js";
2 | import { postprocess } from "./postprocess.js";
3 |
4 | export default {
5 | processors: { svelte3: { preprocess, postprocess, supportsAutofix: true } },
6 | configs: {
7 | defaultWithJsx: {
8 | parserOptions: {
9 | ecmaFeatures: {
10 | jsx: true,
11 | },
12 | },
13 | overrides: [
14 | {
15 | files: ["**/*.{tsx,jsx}"],
16 | },
17 | ],
18 | },
19 | },
20 | };
21 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on: [push, pull_request]
3 | jobs:
4 | Tests:
5 | runs-on: ${{ matrix.os }}
6 | strategy:
7 | matrix:
8 | node-version: [10, 12, 14]
9 | os: [ubuntu-latest, windows-latest, macOS-latest]
10 | steps:
11 | - run: git config --global core.autocrlf false
12 | - uses: actions/checkout@v1
13 | - uses: actions/setup-node@v1
14 | with:
15 | node-version: ${{ matrix.node-version }}
16 | - run: npm install
17 | - run: npm test
18 | env:
19 | CI: true
20 |
--------------------------------------------------------------------------------
/test/samples/typescript-type-aware-rules/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: [
4 | 'eslint:recommended',
5 | 'plugin:@typescript-eslint/recommended',
6 | 'plugin:@typescript-eslint/recommended-requiring-type-checking',],
7 | plugins: ['@typescript-eslint'],
8 | parserOptions: {
9 | tsconfigRootDir: __dirname,
10 | project: ['./tsconfig.json'],
11 | extraFileExtensions: ['.svelte'],
12 | },
13 | ignorePatterns: ['.eslintrc.js'],
14 | settings: {
15 | 'svelte3/typescript': require('typescript'),
16 | }
17 | };
18 |
--------------------------------------------------------------------------------
/test/samples/html/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | rules: {
3 | "no-undef": "error",
4 | "custom-rules/html-example": "error",
5 | },
6 | parser: '@typescript-eslint/parser',
7 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended'],
8 | plugins: ['@typescript-eslint', "custom-rules"],
9 | settings: {
10 | 'svelte3/typescript': require('typescript'),
11 | "svelte3/ignore-warnings": ({ code }) => code === "missing-declaration",
12 | "svelte3/named-blocks": true,
13 | "svelte3/ignore-styles": () => true,
14 | },
15 | parserOptions: {
16 | ecmaVersion: 2020,
17 | },
18 | };
19 |
--------------------------------------------------------------------------------
/test/samples/typescript-block-filenames/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | extends: ['plugin:@typescript-eslint/recommended'],
4 | plugins: ['@typescript-eslint'],
5 | overrides: [
6 | {
7 | files: ['**/*.svelte/*_template.ts'],
8 | rules: {
9 | curly: 'off',
10 | },
11 | },
12 | {
13 | files: ['**/*.svelte/*_module.ts'],
14 | rules: {
15 | 'no-undef': 'off',
16 | },
17 | },
18 | ],
19 | settings: {
20 | 'svelte3/typescript': require('typescript'),
21 | 'svelte3/named-blocks': true,
22 | },
23 | rules: {
24 | curly: 'error',
25 | 'no-undef': 'error',
26 | },
27 | };
28 |
--------------------------------------------------------------------------------
/test/samples/scope/Input.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 | {#each foo as bar}
6 | {bar}
7 | {/each}
8 |
9 | {bar}
10 |
11 | {#each foo as [bar1, { bar2 }]}
12 | {bar1}
13 | {bar2}
14 | {bar3}
15 | {/each}
16 |
17 |
18 | {baz}
19 |
20 |
21 |
22 | {baz1}
23 | {baz2}
24 | {baz3}
25 |
26 |
27 |
28 |
29 | {blah1}
30 | {blah2}
31 |
32 |
33 |
34 | {#await foo}
35 | xxx
36 | {:then blah1}
37 | {blah1}
38 | {blah2}
39 | {:catch blah2}
40 | {blah1}
41 | {blah2}
42 | {/await}
43 |
44 | {#await foo then bar}
45 | {bar}
46 | {/await}
47 |
--------------------------------------------------------------------------------
/test/samples/block-filenames/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "curly",
4 | "severity": 2,
5 | "message": "Expected { after 'if' condition.",
6 | "line": 2,
7 | "column": 2,
8 | "nodeType": "IfStatement",
9 | "messageId": "missingCurlyAfterCondition",
10 | "fix": {
11 | "range": [
12 | 37,
13 | 42
14 | ],
15 | "text": "{blah;}"
16 | }
17 | },
18 | {
19 | "ruleId": "curly",
20 | "severity": 2,
21 | "message": "Expected { after 'if' condition.",
22 | "line": 7,
23 | "column": 2,
24 | "nodeType": "IfStatement",
25 | "messageId": "missingCurlyAfterCondition",
26 | "fix": {
27 | "range": [
28 | 88,
29 | 92
30 | ],
31 | "text": "{bar;}"
32 | }
33 | }
34 | ]
--------------------------------------------------------------------------------
/test/samples/typescript-block-filenames/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "curly",
4 | "severity": 2,
5 | "message": "Expected { after 'if' condition.",
6 | "line": 2,
7 | "column": 2,
8 | "nodeType": "IfStatement",
9 | "messageId": "missingCurlyAfterCondition",
10 | "fix": {
11 | "range": [
12 | 37,
13 | 42
14 | ],
15 | "text": "{blah;}"
16 | }
17 | },
18 | {
19 | "ruleId": "curly",
20 | "severity": 2,
21 | "message": "Expected { after 'if' condition.",
22 | "line": 7,
23 | "column": 2,
24 | "nodeType": "IfStatement",
25 | "messageId": "missingCurlyAfterCondition",
26 | "fix": {
27 | "range": [
28 | 88,
29 | 92
30 | ],
31 | "text": "{bar;}"
32 | }
33 | }
34 | ]
--------------------------------------------------------------------------------
/test/samples/template-quotes/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "quotes",
4 | "severity": 2,
5 | "message": "Strings must use singlequote.",
6 | "line": 2,
7 | "column": 2,
8 | "nodeType": "Literal",
9 | "messageId": "wrongQuotes",
10 | "endLine": 2,
11 | "endColumn": 7,
12 | "fix": {
13 | "range": [
14 | 9,
15 | 14
16 | ],
17 | "text": "'bar'"
18 | }
19 | },
20 | {
21 | "ruleId": "quotes",
22 | "severity": 2,
23 | "message": "Strings must use singlequote.",
24 | "line": 3,
25 | "column": 13,
26 | "nodeType": "Literal",
27 | "messageId": "wrongQuotes",
28 | "endLine": 3,
29 | "endColumn": 19,
30 | "fix": {
31 | "range": [
32 | 28,
33 | 34
34 | ],
35 | "text": "'baz1'"
36 | }
37 | }
38 | ]
--------------------------------------------------------------------------------
/test/samples/module-context/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "no-unused-vars",
4 | "severity": 2,
5 | "message": "'bar' is assigned a value but never used.",
6 | "line": 2,
7 | "column": 18,
8 | "nodeType": "Identifier",
9 | "messageId": "unusedVar",
10 | "endLine": 2,
11 | "endColumn": 21
12 | },
13 | {
14 | "ruleId": "prefer-const",
15 | "severity": 2,
16 | "message": "'baz1' is never reassigned. Use 'const' instead.",
17 | "line": 3,
18 | "column": 6,
19 | "nodeType": "Identifier",
20 | "messageId": "useConst",
21 | "endLine": 3,
22 | "endColumn": 10
23 | },
24 | {
25 | "ruleId": "missing-declaration",
26 | "severity": 1,
27 | "message": "'blah' is not defined",
28 | "line": 9,
29 | "column": 14,
30 | "endLine": 9,
31 | "endColumn": 19
32 | }
33 | ]
--------------------------------------------------------------------------------
/test/samples/typescript-template-quotes/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "@typescript-eslint/quotes",
4 | "severity": 2,
5 | "message": "Strings must use singlequote.",
6 | "line": 2,
7 | "column": 2,
8 | "nodeType": "Literal",
9 | "messageId": "wrongQuotes",
10 | "endLine": 2,
11 | "endColumn": 7,
12 | "fix": {
13 | "range": [
14 | 9,
15 | 14
16 | ],
17 | "text": "'bar'"
18 | }
19 | },
20 | {
21 | "ruleId": "@typescript-eslint/quotes",
22 | "severity": 2,
23 | "message": "Strings must use singlequote.",
24 | "line": 3,
25 | "column": 13,
26 | "nodeType": "Literal",
27 | "messageId": "wrongQuotes",
28 | "endLine": 3,
29 | "endColumn": 19,
30 | "fix": {
31 | "range": [
32 | 28,
33 | 34
34 | ],
35 | "text": "'baz1'"
36 | }
37 | }
38 | ]
--------------------------------------------------------------------------------
/test/samples/typescript-imports/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "no-unused-vars",
4 | "severity": 2,
5 | "message": "'UnusedType' is defined but never used.",
6 | "line": 2,
7 | "column": 22,
8 | "nodeType": "Identifier",
9 | "messageId": "unusedVar",
10 | "endLine": 2,
11 | "endColumn": 32
12 | },
13 | {
14 | "ruleId": "no-unused-vars",
15 | "severity": 2,
16 | "message": "'UnusedComponent' is defined but never used.",
17 | "line": 4,
18 | "column": 9,
19 | "nodeType": "Identifier",
20 | "messageId": "unusedVar",
21 | "endLine": 4,
22 | "endColumn": 24
23 | },
24 | {
25 | "ruleId": "no-unused-vars",
26 | "severity": 2,
27 | "message": "'UnusedThing' is defined but never used.",
28 | "line": 5,
29 | "column": 27,
30 | "nodeType": "Identifier",
31 | "messageId": "unusedVar",
32 | "endLine": 5,
33 | "endColumn": 38
34 | }
35 | ]
--------------------------------------------------------------------------------
/test/samples/html/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "a11y-missing-content",
4 | "severity": 1,
5 | "message": "A11y:
element should have child content",
6 | "line": 10,
7 | "column": 1,
8 | "endLine": 10,
9 | "endColumn": 59
10 | },
11 | {
12 | "ruleId": "custom-rules/html-example",
13 | "severity": 2,
14 | "message": "Headings must have content and the content must be accessible by a screen reader.",
15 | "line": 10,
16 | "column": 1,
17 | "nodeType": "JSXElement",
18 | "endLine": 10,
19 | "endColumn": 74
20 | },
21 | {
22 | "ruleId": "custom-rules/html-example",
23 | "severity": 2,
24 | "message": "I am asked to error out...",
25 | "line": 10,
26 | "column": 1,
27 | "nodeType": "JSXElement",
28 | "endLine": 10,
29 | "endColumn": 74
30 | },
31 | {
32 | "ruleId": "no-irregular-whitespace",
33 | "severity": 2,
34 | "message": "Irregular whitespace not allowed.",
35 | "line": 75,
36 | "column": 9,
37 | "nodeType": "Program",
38 | "messageId": "noIrregularWhitespace",
39 | "endLine": 75,
40 | "endColumn": 11
41 | }
42 | ]
--------------------------------------------------------------------------------
/test/samples/indentation/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "semi",
4 | "severity": 2,
5 | "message": "Missing semicolon.",
6 | "line": 2,
7 | "column": 5,
8 | "nodeType": "ExpressionStatement",
9 | "messageId": "missingSemi",
10 | "endLine": 3,
11 | "endColumn": 2,
12 | "fix": {
13 | "range": [
14 | 13,
15 | 13
16 | ],
17 | "text": ";"
18 | }
19 | },
20 | {
21 | "ruleId": "indent",
22 | "severity": 2,
23 | "message": "Expected indentation of 0 tabs but found 1.",
24 | "line": 3,
25 | "column": 2,
26 | "nodeType": "Identifier",
27 | "messageId": "wrongIndentation",
28 | "endLine": 3,
29 | "endColumn": 3,
30 | "fix": {
31 | "range": [
32 | 15,
33 | 16
34 | ],
35 | "text": ""
36 | }
37 | },
38 | {
39 | "ruleId": "semi",
40 | "severity": 2,
41 | "message": "Missing semicolon.",
42 | "line": 3,
43 | "column": 6,
44 | "nodeType": "ExpressionStatement",
45 | "messageId": "missingSemi",
46 | "endLine": 4,
47 | "endColumn": 2,
48 | "fix": {
49 | "range": [
50 | 19,
51 | 19
52 | ],
53 | "text": ";"
54 | }
55 | }
56 | ]
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2018 Conduitry
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/test/samples/typescript-indentation/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "semi",
4 | "severity": 2,
5 | "message": "Missing semicolon.",
6 | "line": 2,
7 | "column": 9,
8 | "nodeType": "VariableDeclaration",
9 | "messageId": "missingSemi",
10 | "endLine": 3,
11 | "endColumn": 2,
12 | "fix": {
13 | "range": [
14 | 27,
15 | 27
16 | ],
17 | "text": ";"
18 | }
19 | },
20 | {
21 | "ruleId": "@typescript-eslint/indent",
22 | "severity": 2,
23 | "message": "Expected indentation of 0 tabs but found 1.",
24 | "line": 3,
25 | "column": 2,
26 | "nodeType": "Keyword",
27 | "messageId": "wrongIndentation",
28 | "endLine": 3,
29 | "endColumn": 3,
30 | "fix": {
31 | "range": [
32 | 29,
33 | 30
34 | ],
35 | "text": ""
36 | }
37 | },
38 | {
39 | "ruleId": "semi",
40 | "severity": 2,
41 | "message": "Missing semicolon.",
42 | "line": 3,
43 | "column": 10,
44 | "nodeType": "VariableDeclaration",
45 | "messageId": "missingSemi",
46 | "endLine": 4,
47 | "endColumn": 2,
48 | "fix": {
49 | "range": [
50 | 37,
51 | 37
52 | ],
53 | "text": ";"
54 | }
55 | }
56 | ]
--------------------------------------------------------------------------------
/test/samples/html/index.js:
--------------------------------------------------------------------------------
1 | const headings = ["h1", "h2", "h3", "h4", "h5", "h6"];
2 | const errorMessage =
3 | "Headings must have content and the content must be accessible by a screen reader.";
4 |
5 | module.exports = {
6 | rules: {
7 | "html-example": (context, _) => ({
8 | "*": (node) => {
9 | if (!node.openingElement) {
10 | return;
11 | }
12 | if (!node.openingElement.name) {
13 | return;
14 | }
15 | // Check 'h*' elements
16 | if (!headings.includes(node.openingElement.name.name)) {
17 | return;
18 | }
19 | // Check 'h*' elements
20 | if (!node.children.length) {
21 | context.report({
22 | node,
23 | message: errorMessage,
24 | });
25 | }
26 |
27 | if (
28 | node.openingElement.attributes.find(
29 | (a) => a.name.name === "data-error-out"
30 | )
31 | ) {
32 | context.report({
33 | node,
34 | message: "I am asked to error out...",
35 | });
36 | }
37 | },
38 | }),
39 | },
40 | };
41 |
--------------------------------------------------------------------------------
/OTHER_PLUGINS.md:
--------------------------------------------------------------------------------
1 | # Interactions with other plugins
2 |
3 | ## `eslint-plugin-html`
4 |
5 | Don't enable this at all on the files you're running `eslint-plugin-svelte3` on. Everything will almost certainly break.
6 |
7 | ## `eslint-plugin-prettier`
8 |
9 | Don't enable this either on Svelte components. If you want to use Prettier, just use it directly, along with appropriate plugins.
10 |
11 | ## `eslint-plugin-import`
12 |
13 | These rules are known to not work correctly together with this plugin:
14 |
15 | - `import/first`
16 | - `import/no-duplicates`
17 | - `import/no-mutable-exports`
18 | - `import/no-unresolved` when using `svelte3/named-blocks`, pending [this issue](https://github.com/benmosher/eslint-plugin-import/issues/1415)
19 |
20 | If you're using them on other linted files, consider [adding `overrides` for them for Svelte components](https://eslint.org/docs/user-guide/configuring#disabling-rules-only-for-a-group-of-files).
21 |
22 | ## `eslint-config-standard`
23 |
24 | This uses `eslint-plugin-import` by default, so the above applies.
25 |
26 | ## Others?
27 |
28 | If you've found another mainstream ESLint plugin that doesn't play nicely with this one, or has certain rules that don't work properly, please let us know!
29 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "eslint-plugin-svelte3",
3 | "version": "3.2.0",
4 | "description": "An ESLint plugin for Svelte v3 components.",
5 | "keywords": [
6 | "eslint",
7 | "eslintplugin",
8 | "svelte",
9 | "sveltejs"
10 | ],
11 | "files": [
12 | "index.js"
13 | ],
14 | "main": "index.js",
15 | "engines": {
16 | "node": ">=10"
17 | },
18 | "repository": {
19 | "type": "git",
20 | "url": "https://github.com/sveltejs/eslint-plugin-svelte3.git"
21 | },
22 | "author": "Conduitry",
23 | "license": "MIT",
24 | "bugs": {
25 | "url": "https://github.com/sveltejs/eslint-plugin-svelte3/issues"
26 | },
27 | "peerDependencies": {
28 | "eslint": ">=6.0.0",
29 | "svelte": "^3.2.0"
30 | },
31 | "scripts": {
32 | "build": "rollup -c",
33 | "dev": "rollup -cw",
34 | "test": "npm run build && node test",
35 | "dev-test": "rollup -cm && node test",
36 | "lint": "eslint --ext .svelte .",
37 | "lint:prettier": "prettier --write ."
38 | },
39 | "devDependencies": {
40 | "eslint-plugin-custom-rules": "file://./test/samples/html",
41 | "@rollup/plugin-node-resolve": "^11.2.0",
42 | "@typescript-eslint/eslint-plugin": "^4.14.2",
43 | "@typescript-eslint/parser": "^4.14.2",
44 | "eslint": ">=6.0.0",
45 | "rollup": "^2",
46 | "sourcemap-codec": "1.4.8",
47 | "prettier": "2.3.2",
48 | "svelte": "^3.2.0",
49 | "typescript": "^4.0.0"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/test/samples/typescript-unsafe-member-access/Input.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
30 |
31 | {context_safe.length}
32 | {context_unsafe.length}
33 | {external_safe.length}
34 | {external_unsafe.length}
35 | {instance_safe.length}
36 | {instance_unsafe.length}
37 |
38 | {reactive_unsafe.length}
39 |
40 | {$writable_unsafe.length}
41 |
--------------------------------------------------------------------------------
/test/samples/scope/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "no-undef",
4 | "severity": 2,
5 | "message": "'bar' is not defined.",
6 | "line": 9,
7 | "column": 2,
8 | "nodeType": "Identifier",
9 | "messageId": "undef",
10 | "endLine": 9,
11 | "endColumn": 5
12 | },
13 | {
14 | "ruleId": "no-undef",
15 | "severity": 2,
16 | "message": "'bar3' is not defined.",
17 | "line": 14,
18 | "column": 3,
19 | "nodeType": "Identifier",
20 | "messageId": "undef",
21 | "endLine": 14,
22 | "endColumn": 7
23 | },
24 | {
25 | "ruleId": "no-undef",
26 | "severity": 2,
27 | "message": "'baz3' is not defined.",
28 | "line": 24,
29 | "column": 3,
30 | "nodeType": "Identifier",
31 | "messageId": "undef",
32 | "endLine": 24,
33 | "endColumn": 7
34 | },
35 | {
36 | "ruleId": "no-undef",
37 | "severity": 2,
38 | "message": "'blah1' is not defined.",
39 | "line": 29,
40 | "column": 4,
41 | "nodeType": "Identifier",
42 | "messageId": "undef",
43 | "endLine": 29,
44 | "endColumn": 9
45 | },
46 | {
47 | "ruleId": "no-undef",
48 | "severity": 2,
49 | "message": "'blah2' is not defined.",
50 | "line": 38,
51 | "column": 3,
52 | "nodeType": "Identifier",
53 | "messageId": "undef",
54 | "endLine": 38,
55 | "endColumn": 8
56 | },
57 | {
58 | "ruleId": "no-undef",
59 | "severity": 2,
60 | "message": "'blah1' is not defined.",
61 | "line": 40,
62 | "column": 3,
63 | "nodeType": "Identifier",
64 | "messageId": "undef",
65 | "endLine": 40,
66 | "endColumn": 8
67 | }
68 | ]
--------------------------------------------------------------------------------
/test/samples/html/Input.svelte:
--------------------------------------------------------------------------------
1 |
5 | 2
6 |
7 | head
8 |
9 | 3
10 |
11 | {#if 0}
12 |
0
13 | {/if}
14 | {#each [] as a, i (a.id)}
15 | each
16 | {/each}
17 | 4
18 |
21 | 5
22 |
23 | {#await 0}
24 | pending
25 | {:then a}
26 | then
27 | {:catch e}
28 | catch
29 | {/await}
30 |
31 | component
32 |
33 | {#if 0}
34 |
35 | self
36 |
37 | {:else}else
38 | {/if}
39 | {a}
40 |
45 |
46 | 6
47 |
50 | 7
51 |
52 | options
53 |
54 |
55 | 8
56 |
61 | {#if 1}
62 |
63 | {#if 1 === 'individual'}
64 |
65 | {#if 1 && 1}
66 |
67 | {:else}
68 | {#if 1}
69 |
You haven't connected any accounts yet.
70 | {/if}
71 | {/if}
72 |
73 | {:else if '' === 'shared'}
74 |
75 | If
76 |
77 | {/if}
78 |
79 | {/if}
--------------------------------------------------------------------------------
/src/block.js:
--------------------------------------------------------------------------------
1 | import { get_offsets, dedent_code } from "./utils.js";
2 |
3 | // return a new block
4 | export const new_block = () => ({
5 | transformed_code: "",
6 | line_offsets: null,
7 | translations: new Map(),
8 | });
9 |
10 | // get translation info and include the processed scripts in this block's transformed_code
11 | export const get_translation = (text, block, node, options = {}) => {
12 | block.transformed_code += "\n";
13 | const translation = {
14 | options,
15 | unoffsets: get_offsets(block.transformed_code),
16 | };
17 | translation.range = [node.start, node.end];
18 | const { dedented, offsets } = dedent_code(text.slice(node.start, node.end));
19 | block.transformed_code += dedented;
20 | translation.offsets = get_offsets(text.slice(0, node.start));
21 | translation.dedent = offsets;
22 | translation.end = get_offsets(block.transformed_code).lines;
23 | for (let i = translation.unoffsets.lines; i <= translation.end; i++) {
24 | block.translations.set(i, translation);
25 | }
26 | block.transformed_code += "\n";
27 | };
28 |
29 | const nullProxy = new Proxy(
30 | {},
31 | {
32 | get(target, p, receiver) {
33 | return 0;
34 | },
35 | }
36 | )
37 |
38 | export const get_template_translation = (text, block, ast) => {
39 | const codeOffsets = get_offsets(text);
40 |
41 | const translation = {
42 | options: {},
43 | start: 0,
44 | end: codeOffsets.lines,
45 | unoffsets: { length: 0, lines: 1, last: 0 },
46 | dedent: {
47 | offsets: nullProxy,
48 | total_offsets: nullProxy,
49 | },
50 | offsets: { length: 0, lines: 1, last: 0 },
51 | range: [0, text.length - 1]
52 | };
53 |
54 | for (let i = translation.start; i <= translation.end; i++) {
55 | translation.options.template = i > 0;
56 | block.translations.set(i, translation);
57 | }
58 | };
59 |
--------------------------------------------------------------------------------
/test/samples/labels/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "no-restricted-syntax",
4 | "severity": 2,
5 | "message": "Using 'LabeledStatement' is not allowed.",
6 | "line": 2,
7 | "column": 2,
8 | "nodeType": "LabeledStatement",
9 | "messageId": "restrictedSyntax",
10 | "endLine": 2,
11 | "endColumn": 11
12 | },
13 | {
14 | "ruleId": "no-labels",
15 | "severity": 2,
16 | "message": "Unexpected labeled statement.",
17 | "line": 2,
18 | "column": 2,
19 | "nodeType": "LabeledStatement",
20 | "messageId": "unexpectedLabel",
21 | "endLine": 2,
22 | "endColumn": 11
23 | },
24 | {
25 | "ruleId": "no-unused-labels",
26 | "severity": 2,
27 | "message": "'foo:' is defined but never used.",
28 | "line": 2,
29 | "column": 2,
30 | "nodeType": "Identifier",
31 | "messageId": "unused",
32 | "endLine": 2,
33 | "endColumn": 5,
34 | "fix": {
35 | "range": [
36 | 10,
37 | 15
38 | ],
39 | "text": ""
40 | }
41 | },
42 | {
43 | "ruleId": "no-restricted-syntax",
44 | "severity": 2,
45 | "message": "Using 'LabeledStatement' is not allowed.",
46 | "line": 4,
47 | "column": 2,
48 | "nodeType": "LabeledStatement",
49 | "messageId": "restrictedSyntax",
50 | "endLine": 4,
51 | "endColumn": 13
52 | },
53 | {
54 | "ruleId": "no-labels",
55 | "severity": 2,
56 | "message": "Unexpected labeled statement.",
57 | "line": 4,
58 | "column": 2,
59 | "nodeType": "LabeledStatement",
60 | "messageId": "unexpectedLabel",
61 | "endLine": 4,
62 | "endColumn": 13
63 | },
64 | {
65 | "ruleId": "no-unused-labels",
66 | "severity": 2,
67 | "message": "'$baz:' is defined but never used.",
68 | "line": 4,
69 | "column": 2,
70 | "nodeType": "Identifier",
71 | "messageId": "unused",
72 | "endLine": 4,
73 | "endColumn": 6,
74 | "fix": {
75 | "range": [
76 | 30,
77 | 36
78 | ],
79 | "text": ""
80 | }
81 | }
82 | ]
--------------------------------------------------------------------------------
/src/processor_options.js:
--------------------------------------------------------------------------------
1 | export const processor_options = {};
2 |
3 | // find Linter instance
4 | const linter_paths = Object.keys(require.cache).filter(path => path.endsWith('/eslint/lib/linter/linter.js') || path.endsWith('\\eslint\\lib\\linter\\linter.js'));
5 | if (!linter_paths.length) {
6 | throw new Error('Could not find ESLint Linter in require cache');
7 | }
8 | // There may be more than one instance of the linter when we're in a workspace with multiple directories.
9 | // We first try to find the one that's inside the same node_modules directory as this plugin.
10 | // If that can't be found for some reason, we assume the one we want is the last one in the array.
11 | const current_node_modules_path = __dirname.replace(/(?<=[/\\]node_modules[/\\]).*$/, '')
12 | const linter_path = linter_paths.find(path => path.startsWith(current_node_modules_path)) || linter_paths.pop();
13 | const { Linter } = require(linter_path);
14 |
15 | // patch Linter#verify
16 | const { verify } = Linter.prototype;
17 | Linter.prototype.verify = function(code, config, options) {
18 | // fetch settings
19 | const settings = config && (typeof config.extractConfig === 'function' ? config.extractConfig(options.filename) : config).settings || {};
20 | processor_options.custom_compiler = settings['svelte3/compiler'];
21 | processor_options.ignore_warnings = settings['svelte3/ignore-warnings'];
22 | processor_options.ignore_styles = settings['svelte3/ignore-styles'];
23 | processor_options.compiler_options = settings['svelte3/compiler-options'];
24 | processor_options.named_blocks = settings['svelte3/named-blocks'];
25 | processor_options.typescript =
26 | settings['svelte3/typescript'] === true
27 | ? require('typescript')
28 | : typeof settings['svelte3/typescript'] === 'function'
29 | ? settings['svelte3/typescript']()
30 | : settings['svelte3/typescript'];
31 | // call original Linter#verify
32 | return verify.call(this, code, config, options);
33 | };
34 |
--------------------------------------------------------------------------------
/test/samples/typescript-type-aware-rules/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "@typescript-eslint/no-for-in-array",
4 | "severity": 2,
5 | "message": "For-in loops over arrays are forbidden. Use for-of or array.forEach instead.",
6 | "line": 2,
7 | "column": 2,
8 | "nodeType": "ForInStatement",
9 | "messageId": "forInViolation",
10 | "endLine": 4,
11 | "endColumn": 3
12 | },
13 | {
14 | "ruleId": "@typescript-eslint/no-unsafe-return",
15 | "severity": 2,
16 | "message": "Unsafe return of an `any` typed value.",
17 | "line": 7,
18 | "column": 3,
19 | "nodeType": "ReturnStatement",
20 | "messageId": "unsafeReturn",
21 | "endLine": 7,
22 | "endColumn": 19
23 | },
24 | {
25 | "ruleId": "@typescript-eslint/no-explicit-any",
26 | "severity": 1,
27 | "message": "Unexpected any. Specify a different type.",
28 | "line": 7,
29 | "column": 15,
30 | "nodeType": "TSAnyKeyword",
31 | "messageId": "unexpectedAny",
32 | "endLine": 7,
33 | "endColumn": 18,
34 | "suggestions": [
35 | {
36 | "messageId": "suggestUnknown",
37 | "fix": {
38 | "range": [
39 | 61,
40 | 64
41 | ],
42 | "text": "unknown"
43 | },
44 | "desc": "Use `unknown` instead, this will force you to explicitly, and safely assert the type is correct."
45 | },
46 | {
47 | "messageId": "suggestNever",
48 | "fix": {
49 | "range": [
50 | 61,
51 | 64
52 | ],
53 | "text": "never"
54 | },
55 | "desc": "Use `never` instead, this is useful when instantiating generic type parameters that you don't need to know the type of."
56 | }
57 | ]
58 | },
59 | {
60 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
61 | "severity": 2,
62 | "message": "Unsafe member access .hello on an `any` value.",
63 | "line": 10,
64 | "column": 2,
65 | "nodeType": "MemberExpression",
66 | "messageId": "unsafeMemberExpression",
67 | "endLine": 10,
68 | "endColumn": 13
69 | },
70 | {
71 | "ruleId": "@typescript-eslint/no-unsafe-call",
72 | "severity": 2,
73 | "message": "Unsafe call of an `any` typed value.",
74 | "line": 10,
75 | "column": 2,
76 | "nodeType": "MemberExpression",
77 | "messageId": "unsafeCall",
78 | "endLine": 10,
79 | "endColumn": 13
80 | }
81 | ]
--------------------------------------------------------------------------------
/test/index.js:
--------------------------------------------------------------------------------
1 | "use strict";
2 |
3 | require("eslint/lib/linter");
4 | const svelte3 = require("eslint-plugin-svelte3").processors.svelte3;
5 |
6 | process.chdir(__dirname);
7 |
8 | const { CLIEngine } = require("eslint");
9 | const assert = require("assert");
10 | const fs = require("fs");
11 |
12 | const cli = new CLIEngine({ reportUnusedDisableDirectives: true });
13 |
14 | function checkPreprocessOutput(name, text) {
15 | const preprocessed = svelte3.preprocess(text);
16 | let expectedPreprocessorOutput;
17 |
18 | preprocessed.forEach((codeText, i) => {
19 | const filename = preprocessed[i].filename || `svelte${i}.tsx`;
20 | const filepath = `samples/${name}/${filename}`;
21 |
22 | if (!fs.existsSync(filepath) || process.env.OVERWRITE_SNAPSHOTS) {
23 | console.log(`Overwriting ${filepath} snapshot`);
24 | fs.writeFileSync(filepath, preprocessed[i].text || preprocessed[i]);
25 | }
26 |
27 | expectedPreprocessorOutput = fs.readFileSync(filepath).toString();
28 |
29 | console.log(`Checking ${filepath}...`);
30 |
31 | assert.strictEqual(
32 | preprocessed[i].text || preprocessed[i],
33 | expectedPreprocessorOutput,
34 | `${name}: ${filename}`
35 | );
36 | });
37 |
38 | svelte3.postprocess([]);
39 | }
40 |
41 | function jsonify(val) {
42 | return JSON.parse(JSON.stringify(val));
43 | }
44 |
45 | for (const name of fs.readdirSync("samples")) {
46 | if (name[0] !== ".") {
47 | console.log(name);
48 | if (
49 | process.platform === "win32" &&
50 | !fs.existsSync(`samples/${name}/preserve_line_endings`)
51 | ) {
52 | fs.writeFileSync(
53 | `samples/${name}/Input.svelte`,
54 | fs
55 | .readFileSync(`samples/${name}/Input.svelte`)
56 | .toString()
57 | .replace(/\r/g, "")
58 | );
59 | }
60 | const result = cli.executeOnFiles([`samples/${name}/Input.svelte`]);
61 | const actual_messages = Object.values(
62 | result.results[0].messages.reduce(
63 | (mem, m) => Object.assign(mem, { [JSON.stringify(m)]: m }),
64 | {}
65 | )
66 | );
67 | fs.writeFileSync(
68 | `samples/${name}/actual.json`,
69 | JSON.stringify(actual_messages, null, "\t")
70 | );
71 | if (result.results[0].source) {
72 | checkPreprocessOutput(name, result.results[0].source);
73 | }
74 | const filepath = `samples/${name}/expected.json`;
75 | if (!fs.existsSync(filepath) || process.env.OVERWRITE_SNAPSHOTS) {
76 | console.log(`Overwriting ${filepath} snapshot`);
77 | fs.writeFileSync(filepath, JSON.stringify(actual_messages, null, "\t"));
78 | }
79 | const expected_messages = JSON.parse(fs.readFileSync(filepath).toString());
80 | assert.deepStrictEqual(
81 | jsonify(actual_messages),
82 | jsonify(expected_messages),
83 | name
84 | );
85 | console.log("passed!\n");
86 | }
87 | }
88 |
--------------------------------------------------------------------------------
/test/samples/typescript-unsafe-member-access/expected.json:
--------------------------------------------------------------------------------
1 | [
2 | {
3 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
4 | "severity": 2,
5 | "message": "Unsafe member access .length on an `any` value.",
6 | "line": 6,
7 | "column": 14,
8 | "nodeType": "MemberExpression",
9 | "messageId": "unsafeMemberExpression",
10 | "endLine": 6,
11 | "endColumn": 35
12 | },
13 | {
14 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
15 | "severity": 2,
16 | "message": "Unsafe member access .length on an `any` value.",
17 | "line": 20,
18 | "column": 14,
19 | "nodeType": "MemberExpression",
20 | "messageId": "unsafeMemberExpression",
21 | "endLine": 20,
22 | "endColumn": 35
23 | },
24 | {
25 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
26 | "severity": 2,
27 | "message": "Unsafe member access .length on an `any` value.",
28 | "line": 22,
29 | "column": 14,
30 | "nodeType": "MemberExpression",
31 | "messageId": "unsafeMemberExpression",
32 | "endLine": 22,
33 | "endColumn": 36
34 | },
35 | {
36 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
37 | "severity": 2,
38 | "message": "Unsafe member access .length on an `any` value.",
39 | "line": 24,
40 | "column": 14,
41 | "nodeType": "MemberExpression",
42 | "messageId": "unsafeMemberExpression",
43 | "endLine": 24,
44 | "endColumn": 36
45 | },
46 | {
47 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
48 | "severity": 2,
49 | "message": "Unsafe member access .length on an `any` value.",
50 | "line": 26,
51 | "column": 14,
52 | "nodeType": "MemberExpression",
53 | "messageId": "unsafeMemberExpression",
54 | "endLine": 26,
55 | "endColumn": 36
56 | },
57 | {
58 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
59 | "severity": 2,
60 | "message": "Unsafe member access .length on an `any` value.",
61 | "line": 28,
62 | "column": 14,
63 | "nodeType": "MemberExpression",
64 | "messageId": "unsafeMemberExpression",
65 | "endLine": 28,
66 | "endColumn": 37
67 | },
68 | {
69 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
70 | "severity": 2,
71 | "message": "Unsafe member access .length on an `any` value.",
72 | "line": 32,
73 | "column": 2,
74 | "nodeType": "MemberExpression",
75 | "messageId": "unsafeMemberExpression",
76 | "endLine": 32,
77 | "endColumn": 23
78 | },
79 | {
80 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
81 | "severity": 2,
82 | "message": "Unsafe member access .length on an `any` value.",
83 | "line": 34,
84 | "column": 2,
85 | "nodeType": "MemberExpression",
86 | "messageId": "unsafeMemberExpression",
87 | "endLine": 34,
88 | "endColumn": 24
89 | },
90 | {
91 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
92 | "severity": 2,
93 | "message": "Unsafe member access .length on an `any` value.",
94 | "line": 36,
95 | "column": 2,
96 | "nodeType": "MemberExpression",
97 | "messageId": "unsafeMemberExpression",
98 | "endLine": 36,
99 | "endColumn": 24
100 | },
101 | {
102 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
103 | "severity": 2,
104 | "message": "Unsafe member access .length on an `any` value.",
105 | "line": 38,
106 | "column": 2,
107 | "nodeType": "MemberExpression",
108 | "messageId": "unsafeMemberExpression",
109 | "endLine": 38,
110 | "endColumn": 24
111 | },
112 | {
113 | "ruleId": "@typescript-eslint/no-unsafe-member-access",
114 | "severity": 2,
115 | "message": "Unsafe member access .length on an `any` value.",
116 | "line": 40,
117 | "column": 2,
118 | "nodeType": "MemberExpression",
119 | "messageId": "unsafeMemberExpression",
120 | "endLine": 40,
121 | "endColumn": 25
122 | }
123 | ]
--------------------------------------------------------------------------------
/INTEGRATIONS.md:
--------------------------------------------------------------------------------
1 | # Visual Studio Code
2 |
3 | You'll need the [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) extension installed.
4 |
5 | Unless you're using `.html` for your Svelte components, you'll need to configure `files.associations` to associate the appropriate file extension with the `html` language. For example, to associate `.svelte`, put this in your `settings.json`:
6 |
7 | ```json
8 | {
9 | "files.associations": {
10 | "*.svelte": "html"
11 | }
12 | }
13 | ```
14 |
15 | Then, you'll need to tell the ESLint extension to also lint files with language `html`. If you haven't adjusted the `eslint.validate` setting, it defaults to `[ "javascript", "javascriptreact" ]`, so put this in your `settings.json`:
16 |
17 | ```json
18 | {
19 | "eslint.validate": [
20 | "javascript",
21 | "javascriptreact",
22 | "html"
23 | ]
24 | }
25 | ```
26 |
27 | If you are using an extension that provides Svelte syntax highlighting, don't associate `*.svelte` files with the `html` language, and instead enable the ESLint extension on `"svelte"`.
28 |
29 | Reload VS Code and give it a go!
30 |
31 | # Atom
32 |
33 | You'll need the [linter](https://atom.io/packages/linter) and [linter-eslint](https://atom.io/packages/linter-eslint) packages installed.
34 |
35 | Unless you're using `.html` for your Svelte components, you'll need to configure `*`.`core`.`customFileTypes` to associate the appropriate file extension with the `text.html.basic` language. For example, to associate `.svelte`, put this in your `config.cson`:
36 |
37 | ```cson
38 | "*":
39 | core:
40 | customFileTypes:
41 | "text.html.basic": [
42 | "svelte"
43 | ]
44 | ```
45 |
46 | Then, you'll need to tell linter-eslint to also lint HTML files: add `source.html` to the list of scopes to run ESLint on in the linter-eslint settings.
47 |
48 | Reload Atom and give it a go!
49 |
50 | # Sublime Text
51 |
52 | You'll need the [SublimeLinter](https://github.com/SublimeLinter/SublimeLinter) and [SublimeLinter-eslint](https://github.com/SublimeLinter/SublimeLinter-eslint) packages installed.
53 |
54 | Unless you're using `.html` for your Svelte components, you'll need to configure Sublime to associate the appropriate file extension with the `text.html` syntax. Open any Svelte component, and go to **View > Syntax > Open all with current extension as... > HTML**.
55 |
56 | Then, you'll need to tell SublimeLinter-eslint to lint entire files with the `text.html` syntax, and not just the contents of their `
123 | ```
124 |
125 | ## Interactions with other plugins
126 |
127 | Care needs to be taken when using this plugin alongside others. Take a look at [this list of things you need to watch out for](OTHER_PLUGINS.md).
128 |
129 | ## Configuration
130 |
131 | There are a few settings you can use to adjust this plugin's behavior. These go in the `settings` object in your ESLint configuration.
132 |
133 | Passing a function as a value for a setting (which some of the settings below require) is only possible when using a CommonJS `.eslintrc.js` file, and not a JSON or YAML configuration file.
134 |
135 | ### `svelte3/ignore-warnings`
136 |
137 | This setting can be given a function that indicates whether to ignore a warning in the linting. The function will be passed a warning object and should return a boolean.
138 |
139 | The default is to not ignore any warnings.
140 |
141 | ### `svelte3/compiler-options`
142 |
143 | Most compiler options do not affect the validity of compiled components, but a couple of them can. If you are compiling to custom elements, or for some other reason need to control how the plugin compiles the components it's linting, you can use this setting.
144 |
145 | This setting can be given an object of compiler options.
146 |
147 | The default is to compile with `{ generate: false }`.
148 |
149 | ### `svelte3/ignore-styles`
150 |
151 | If you're using some sort of preprocessor on the component styles, then it's likely that when this plugin calls the Svelte compiler on your component, it will throw an exception. In a perfect world, this plugin would be able to apply the preprocessor to the component and then use source maps to translate any warnings back to the original source. In the current reality, however, you can instead simply disregard styles written in anything other than standard CSS. You won't get warnings about the styles from the linter, but your application will still use them (of course) and compiler warnings will still appear in your build logs.
152 |
153 | This setting can be given a function that accepts an object of attributes on a ``
62 | : match;
63 | }
64 | );
65 | }
66 |
67 | // get information about the component
68 | let result;
69 | try {
70 | result = compile_code(text, compiler, processor_options);
71 | } catch ({ name, message, start, end }) {
72 | // convert the error to a linting message, store it, and return
73 | state.messages = [
74 | {
75 | ruleId: name,
76 | severity: 2,
77 | message,
78 | line: start && start.line,
79 | column: start && start.column + 1,
80 | endLine: end && end.line,
81 | endColumn: end && end.column + 1,
82 | },
83 | ];
84 | return [];
85 | }
86 | const { ast, warnings, vars, mapper } = result;
87 |
88 | injectMissingAstNodes(ast, text);
89 |
90 | const references_and_reassignments = `{${vars
91 | .filter((v) => v.referenced || v.name[0] === "$")
92 | .map((v) => v.name)};${vars
93 | .filter((v) => v.reassigned || v.export_name)
94 | .map((v) => v.name + "=0")}}`;
95 | state.var_names = new Set(vars.map((v) => v.name));
96 |
97 | // convert warnings to linting messages
98 | const filtered_warnings = processor_options.ignore_warnings
99 | ? warnings.filter((warning) => !processor_options.ignore_warnings(warning))
100 | : warnings;
101 | state.messages = filtered_warnings.map(({ code, message, start, end }) => {
102 | const start_pos =
103 | processor_options.typescript && start
104 | ? mapper.get_original_position(start)
105 | : start && { line: start.line, column: start.column + 1 };
106 | const end_pos =
107 | processor_options.typescript && end
108 | ? mapper.get_original_position(end)
109 | : end && { line: end.line, column: end.column + 1 };
110 | return {
111 | ruleId: code,
112 | severity: 1,
113 | message,
114 | line: start_pos && start_pos.line,
115 | column: start_pos && start_pos.column,
116 | endLine: end_pos && end_pos.line,
117 | endColumn: end_pos && end_pos.column,
118 | };
119 | });
120 |
121 | // build strings that we can send along to ESLint to get the remaining messages
122 |
123 | // Things to think about:
124 | // - not all Svelte files may be typescript -> do we need a distinction on a file basis by analyzing the attribute + a config option to tell "treat all as TS"?
125 | const with_file_ending = (filename) =>
126 | `${filename}${processor_options.typescript ? ".ts" : ".js"}`;
127 |
128 | if (ast.module) {
129 | // block for `;
564 | }
565 | );
566 | const mapper = new DocumentMapper(text, transpiled, diffs);
567 |
568 | let ts_result;
569 | try {
570 | ts_result = compiler.compile(transpiled, {
571 | generate: false,
572 | ...processor_options.compiler_options,
573 | });
574 | } catch (err) {
575 | // remap the error to be in the correct spot and rethrow it
576 | err.start = mapper.get_original_position(err.start);
577 | err.end = mapper.get_original_position(err.end);
578 | throw err;
579 | }
580 |
581 | text = text.replace(
582 | /`;
589 | }
590 | );
591 | // if we do a full recompile Svelte can fail due to the blank script tag not declaring anything
592 | // so instead we just parse for the AST (which is likely faster, anyways)
593 | const ast = compiler.parse(text, { ...processor_options.compiler_options });
594 | const { warnings, vars } = ts_result;
595 | return { ast, warnings, vars, mapper };
596 | }
597 | }
598 |
--------------------------------------------------------------------------------
/index.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | // get the total length, number of lines, and length of the last line of a string
4 | const get_offsets = (str) => {
5 | const { length } = str;
6 | let lines = 1;
7 | let last = 0;
8 | for (let i = 0; i < length; i++) {
9 | if (str[i] === "\n") {
10 | lines++;
11 | last = 0;
12 | } else {
13 | last++;
14 | }
15 | }
16 | return { length, lines, last };
17 | };
18 |
19 | // dedent a script block, and get offsets necessary to later adjust linting messages about the block
20 | const dedent_code = (str) => {
21 | let indentation = "";
22 | for (let i = 0; i < str.length; i++) {
23 | const char = str[i];
24 | if (char === "\n" || char === "\r") {
25 | indentation = "";
26 | } else if (char === " " || char === "\t") {
27 | indentation += str[i];
28 | } else {
29 | break;
30 | }
31 | }
32 | const { length } = indentation;
33 | let dedented = "";
34 | const offsets = [];
35 | const total_offsets = [0];
36 | for (let i = 0; i < str.length; i++) {
37 | if (i === 0 || str[i - 1] === "\n") {
38 | if (str.slice(i, i + length) === indentation) {
39 | i += length;
40 | offsets.push(length);
41 | } else {
42 | offsets.push(0);
43 | }
44 | total_offsets.push(
45 | total_offsets[total_offsets.length - 1] + offsets[offsets.length - 1]
46 | );
47 | if (i >= str.length) {
48 | break;
49 | }
50 | }
51 | dedented += str[i];
52 | }
53 | return { dedented, offsets: { offsets, total_offsets } };
54 | };
55 |
56 | // get character offsets of each line in a string
57 | const get_line_offsets$1 = (str) => {
58 | const offsets = [-1];
59 | for (let i = 0; i < str.length; i++) {
60 | if (str[i] === "\n") {
61 | offsets.push(i);
62 | }
63 | }
64 | return offsets;
65 | };
66 |
67 | const pad = (times) => {
68 | return Array.from({ length: times }, () => "\n").join("");
69 | };
70 |
71 | const closingTagLength = new Proxy(
72 | {
73 | Head: 14,
74 | Options: 17
75 | },
76 | {
77 | get(source, name) {
78 | return source[name] || name.length - 2;
79 | },
80 | }
81 | );
82 |
83 | function getInjectOrder(asts) {
84 | return asts.sort((a, b) => a.start - b.start);
85 | }
86 |
87 | function findGaps(nodes, text) {
88 | return nodes.reduce((mem, c, i, a) => {
89 | if (a[i - 1]) {
90 | if (a[i - 1].end !== c.start) {
91 | c.inject = "before";
92 | }
93 | } else {
94 | if (c.start) {
95 | c.inject = "before";
96 | }
97 | }
98 | if (a[i + 1]) {
99 | if (a[i + 1].start !== c.end) {
100 | c.inject = "after";
101 | }
102 | } else {
103 | if (c.end !== text.length - 1) {
104 | c.inject = "before";
105 | }
106 | }
107 | if (c.inject && !mem.includes(a[i - 1]) && !mem.includes(a[i + 1])) {
108 | mem.push(c);
109 | }
110 | return mem;
111 | }, []);
112 | }
113 |
114 | function padCodeWithMissingNodesLines(ast, text) {
115 | if (!ast.html || !ast.html.children || !ast.html.children.length) {
116 | return;
117 | }
118 | if (!ast.instance && !ast.module && !ast.css) {
119 | return;
120 | }
121 | const injectOrder = getInjectOrder([ast.instance, ast.module, ast.css].filter(_ => _));
122 | // pad html block so we map 1<->1
123 |
124 | const textNodes = findGaps(ast.html.children, text);
125 | injectOrder.forEach((node, i) => {
126 | let textNode = textNodes[i] || textNodes[textNodes.length - 1];
127 |
128 | if (textNode.inject === "after") {
129 | textNode.raw += pad(
130 | get_offsets(text.slice(node.start, node.end)).lines - 1
131 | );
132 | }
133 |
134 | if (textNode.inject === "before") {
135 | textNode.raw =
136 | pad(get_offsets(text.slice(node.start, node.end)).lines - 1) +
137 | textNode.raw;
138 | }
139 | });
140 | }
141 |
142 | function replaceWithWhitespaces(text, node) {
143 | if (!text || !node) {
144 | return '';
145 | }
146 | return text.slice(
147 | node.start,
148 | node.end
149 | ).replace(/\S/g, ' ')
150 | }
151 |
152 | // return a new block
153 | const new_block = () => ({
154 | transformed_code: "",
155 | line_offsets: null,
156 | translations: new Map(),
157 | });
158 |
159 | // get translation info and include the processed scripts in this block's transformed_code
160 | const get_translation = (text, block, node, options = {}) => {
161 | block.transformed_code += "\n";
162 | const translation = {
163 | options,
164 | unoffsets: get_offsets(block.transformed_code),
165 | };
166 | translation.range = [node.start, node.end];
167 | const { dedented, offsets } = dedent_code(text.slice(node.start, node.end));
168 | block.transformed_code += dedented;
169 | translation.offsets = get_offsets(text.slice(0, node.start));
170 | translation.dedent = offsets;
171 | translation.end = get_offsets(block.transformed_code).lines;
172 | for (let i = translation.unoffsets.lines; i <= translation.end; i++) {
173 | block.translations.set(i, translation);
174 | }
175 | block.transformed_code += "\n";
176 | };
177 |
178 | const nullProxy = new Proxy(
179 | {},
180 | {
181 | get(target, p, receiver) {
182 | return 0;
183 | },
184 | }
185 | );
186 |
187 | const get_template_translation = (text, block, ast) => {
188 | const codeOffsets = get_offsets(text);
189 |
190 | const translation = {
191 | options: {},
192 | start: 0,
193 | end: codeOffsets.lines,
194 | unoffsets: { length: 0, lines: 1, last: 0 },
195 | dedent: {
196 | offsets: nullProxy,
197 | total_offsets: nullProxy,
198 | },
199 | offsets: { length: 0, lines: 1, last: 0 },
200 | range: [0, text.length - 1]
201 | };
202 |
203 | for (let i = translation.start; i <= translation.end; i++) {
204 | translation.options.template = i > 0;
205 | block.translations.set(i, translation);
206 | }
207 | };
208 |
209 | const processor_options = {};
210 |
211 | // find Linter instance
212 | const linter_paths = Object.keys(require.cache).filter(path => path.endsWith('/eslint/lib/linter/linter.js') || path.endsWith('\\eslint\\lib\\linter\\linter.js'));
213 | if (!linter_paths.length) {
214 | throw new Error('Could not find ESLint Linter in require cache');
215 | }
216 | // There may be more than one instance of the linter when we're in a workspace with multiple directories.
217 | // We first try to find the one that's inside the same node_modules directory as this plugin.
218 | // If that can't be found for some reason, we assume the one we want is the last one in the array.
219 | const current_node_modules_path = __dirname.replace(/(?<=[/\\]node_modules[/\\]).*$/, '');
220 | const linter_path = linter_paths.find(path => path.startsWith(current_node_modules_path)) || linter_paths.pop();
221 | const { Linter } = require(linter_path);
222 |
223 | // patch Linter#verify
224 | const { verify } = Linter.prototype;
225 | Linter.prototype.verify = function(code, config, options) {
226 | // fetch settings
227 | const settings = config && (typeof config.extractConfig === 'function' ? config.extractConfig(options.filename) : config).settings || {};
228 | processor_options.custom_compiler = settings['svelte3/compiler'];
229 | processor_options.ignore_warnings = settings['svelte3/ignore-warnings'];
230 | processor_options.ignore_styles = settings['svelte3/ignore-styles'];
231 | processor_options.compiler_options = settings['svelte3/compiler-options'];
232 | processor_options.named_blocks = settings['svelte3/named-blocks'];
233 | processor_options.typescript =
234 | settings['svelte3/typescript'] === true
235 | ? require('typescript')
236 | : typeof settings['svelte3/typescript'] === 'function'
237 | ? settings['svelte3/typescript']()
238 | : settings['svelte3/typescript'];
239 | // call original Linter#verify
240 | return verify.call(this, code, config, options);
241 | };
242 |
243 | let state;
244 | const reset = () => {
245 | state = {
246 | messages: null,
247 | var_names: null,
248 | blocks: new Map(),
249 | };
250 | };
251 | reset();
252 |
253 | var charToInteger = {};
254 | var chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';
255 | for (var i = 0; i < chars.length; i++) {
256 | charToInteger[chars.charCodeAt(i)] = i;
257 | }
258 | function decode(mappings) {
259 | var decoded = [];
260 | var line = [];
261 | var segment = [
262 | 0,
263 | 0,
264 | 0,
265 | 0,
266 | 0,
267 | ];
268 | var j = 0;
269 | for (var i = 0, shift = 0, value = 0; i < mappings.length; i++) {
270 | var c = mappings.charCodeAt(i);
271 | if (c === 44) { // ","
272 | segmentify(line, segment, j);
273 | j = 0;
274 | }
275 | else if (c === 59) { // ";"
276 | segmentify(line, segment, j);
277 | j = 0;
278 | decoded.push(line);
279 | line = [];
280 | segment[0] = 0;
281 | }
282 | else {
283 | var integer = charToInteger[c];
284 | if (integer === undefined) {
285 | throw new Error('Invalid character (' + String.fromCharCode(c) + ')');
286 | }
287 | var hasContinuationBit = integer & 32;
288 | integer &= 31;
289 | value += integer << shift;
290 | if (hasContinuationBit) {
291 | shift += 5;
292 | }
293 | else {
294 | var shouldNegate = value & 1;
295 | value >>>= 1;
296 | if (shouldNegate) {
297 | value = value === 0 ? -0x80000000 : -value;
298 | }
299 | segment[j] += value;
300 | j++;
301 | value = shift = 0; // reset
302 | }
303 | }
304 | }
305 | segmentify(line, segment, j);
306 | decoded.push(line);
307 | return decoded;
308 | }
309 | function segmentify(line, segment, j) {
310 | // This looks ugly, but we're creating specialized arrays with a specific
311 | // length. This is much faster than creating a new array (which v8 expands to
312 | // a capacity of 17 after pushing the first item), or slicing out a subarray
313 | // (which is slow). Length 4 is assumed to be the most frequent, followed by
314 | // length 5 (since not everything will have an associated name), followed by
315 | // length 1 (it's probably rare for a source substring to not have an
316 | // associated segment data).
317 | if (j === 4)
318 | line.push([segment[0], segment[1], segment[2], segment[3]]);
319 | else if (j === 5)
320 | line.push([segment[0], segment[1], segment[2], segment[3], segment[4]]);
321 | else if (j === 1)
322 | line.push([segment[0]]);
323 | }
324 |
325 | class GeneratedFragmentMapper {
326 | constructor(generated_code, diff) {
327 | this.generated_code = generated_code;
328 | this.diff = diff;
329 | }
330 |
331 | get_position_relative_to_fragment(position_relative_to_file) {
332 | const fragment_offset = this.offset_in_fragment(offset_at(position_relative_to_file, this.generated_code));
333 | return position_at(fragment_offset, this.diff.generated_content);
334 | }
335 |
336 | offset_in_fragment(offset) {
337 | return offset - this.diff.generated_start
338 | }
339 | }
340 |
341 | class OriginalFragmentMapper {
342 | constructor(original_code, diff) {
343 | this.original_code = original_code;
344 | this.diff = diff;
345 | }
346 |
347 | get_position_relative_to_file(position_relative_to_fragment) {
348 | const parent_offset = this.offset_in_parent(offset_at(position_relative_to_fragment, this.diff.original_content));
349 | return position_at(parent_offset, this.original_code);
350 | }
351 |
352 | offset_in_parent(offset) {
353 | return this.diff.original_start + offset;
354 | }
355 | }
356 |
357 | class SourceMapper {
358 | constructor(raw_source_map) {
359 | this.raw_source_map = raw_source_map;
360 | }
361 |
362 | get_original_position(generated_position) {
363 | if (generated_position.line < 0) {
364 | return { line: -1, column: -1 };
365 | }
366 |
367 | // Lazy-load
368 | if (!this.decoded) {
369 | this.decoded = decode(JSON.parse(this.raw_source_map).mappings);
370 | }
371 |
372 | let line = generated_position.line;
373 | let column = generated_position.column;
374 |
375 | let line_match = this.decoded[line];
376 | while (line >= 0 && (!line_match || !line_match.length)) {
377 | line -= 1;
378 | line_match = this.decoded[line];
379 | if (line_match && line_match.length) {
380 | return {
381 | line: line_match[line_match.length - 1][2],
382 | column: line_match[line_match.length - 1][3]
383 | };
384 | }
385 | }
386 |
387 | if (line < 0) {
388 | return { line: -1, column: -1 };
389 | }
390 |
391 | const column_match = line_match.find((col, idx) =>
392 | idx + 1 === line_match.length ||
393 | (col[0] <= column && line_match[idx + 1][0] > column)
394 | );
395 |
396 | return {
397 | line: column_match[2],
398 | column: column_match[3],
399 | };
400 | }
401 | }
402 |
403 | class DocumentMapper {
404 | constructor(original_code, generated_code, diffs) {
405 | this.original_code = original_code;
406 | this.generated_code = generated_code;
407 | this.diffs = diffs;
408 | this.mappers = diffs.map(diff => {
409 | return {
410 | start: diff.generated_start,
411 | end: diff.generated_end,
412 | diff: diff.diff,
413 | generated_fragment_mapper: new GeneratedFragmentMapper(generated_code, diff),
414 | source_mapper: new SourceMapper(diff.map),
415 | original_fragment_mapper: new OriginalFragmentMapper(original_code, diff)
416 | }
417 | });
418 | }
419 |
420 | get_original_position(generated_position) {
421 | generated_position = { line: generated_position.line - 1, column: generated_position.column };
422 | const offset = offset_at(generated_position, this.generated_code);
423 | let original_offset = offset;
424 | for (const mapper of this.mappers) {
425 | if (offset >= mapper.start && offset <= mapper.end) {
426 | return this.map(mapper, generated_position);
427 | }
428 | if (offset > mapper.end) {
429 | original_offset -= mapper.diff;
430 | }
431 | }
432 | const original_position = position_at(original_offset, this.original_code);
433 | return this.to_ESLint_position(original_position);
434 | }
435 |
436 | map(mapper, generated_position) {
437 | // Map the position to be relative to the transpiled fragment
438 | const position_in_transpiled_fragment = mapper.generated_fragment_mapper.get_position_relative_to_fragment(
439 | generated_position
440 | );
441 | // Map the position, using the sourcemap, to the original position in the source fragment
442 | const position_in_original_fragment = mapper.source_mapper.get_original_position(
443 | position_in_transpiled_fragment
444 | );
445 | // Map the position to be in the original fragment's parent
446 | const original_position = mapper.original_fragment_mapper.get_position_relative_to_file(position_in_original_fragment);
447 | return this.to_ESLint_position(original_position);
448 | }
449 |
450 | to_ESLint_position(position) {
451 | // ESLint line/column is 1-based
452 | return { line: position.line + 1, column: position.column + 1 };
453 | }
454 |
455 | }
456 |
457 | /**
458 | * Get the offset of the line and character position
459 | * @param position Line and character position
460 | * @param text The text for which the offset should be retrieved
461 | */
462 | function offset_at(position, text) {
463 | const line_offsets = get_line_offsets(text);
464 |
465 | if (position.line >= line_offsets.length) {
466 | return text.length;
467 | } else if (position.line < 0) {
468 | return 0;
469 | }
470 |
471 | const line_offset = line_offsets[position.line];
472 | const next_line_offset =
473 | position.line + 1 < line_offsets.length ? line_offsets[position.line + 1] : text.length;
474 |
475 | return clamp(next_line_offset, line_offset, line_offset + position.column);
476 | }
477 |
478 | function position_at(offset, text) {
479 | offset = clamp(offset, 0, text.length);
480 |
481 | const line_offsets = get_line_offsets(text);
482 | let low = 0;
483 | let high = line_offsets.length;
484 | if (high === 0) {
485 | return { line: 0, column: offset };
486 | }
487 |
488 | while (low < high) {
489 | const mid = Math.floor((low + high) / 2);
490 | if (line_offsets[mid] > offset) {
491 | high = mid;
492 | } else {
493 | low = mid + 1;
494 | }
495 | }
496 |
497 | // low is the least x for which the line offset is larger than the current offset
498 | // or array.length if no line offset is larger than the current offset
499 | const line = low - 1;
500 | return { line, column: offset - line_offsets[line] };
501 | }
502 |
503 | function get_line_offsets(text) {
504 | const line_offsets = [];
505 | let is_line_start = true;
506 |
507 | for (let i = 0; i < text.length; i++) {
508 | if (is_line_start) {
509 | line_offsets.push(i);
510 | is_line_start = false;
511 | }
512 | const ch = text.charAt(i);
513 | is_line_start = ch === '\r' || ch === '\n';
514 | if (ch === '\r' && i + 1 < text.length && text.charAt(i + 1) === '\n') {
515 | i++;
516 | }
517 | }
518 |
519 | if (is_line_start && text.length > 0) {
520 | line_offsets.push(text.length);
521 | }
522 |
523 | return line_offsets;
524 | }
525 |
526 | function clamp(num, min, max) {
527 | return Math.max(min, Math.min(max, num));
528 | }
529 |
530 | let default_compiler;
531 |
532 | // find the contextual name or names described by a particular node in the AST
533 | const contextual_names = [];
534 | const find_contextual_names = (compiler, node) => {
535 | if (node) {
536 | if (typeof node === "string") {
537 | contextual_names.push(node);
538 | } else if (typeof node === "object") {
539 | compiler.walk(node, {
540 | enter(node, parent, prop) {
541 | if (node.name && prop !== "key") {
542 | contextual_names.push(node.name);
543 | }
544 | },
545 | });
546 | }
547 | }
548 | };
549 |
550 | // extract scripts to lint from component definition
551 | const preprocess = (text) => {
552 | const compiler =
553 | processor_options.custom_compiler ||
554 | default_compiler ||
555 | (default_compiler = require("svelte/compiler"));
556 | if (processor_options.ignore_styles) {
557 | // wipe the appropriate `
577 | : match;
578 | }
579 | );
580 | }
581 |
582 | // get information about the component
583 | let result;
584 | try {
585 | result = compile_code(text, compiler, processor_options);
586 | } catch ({ name, message, start, end }) {
587 | // convert the error to a linting message, store it, and return
588 | state.messages = [
589 | {
590 | ruleId: name,
591 | severity: 2,
592 | message,
593 | line: start && start.line,
594 | column: start && start.column + 1,
595 | endLine: end && end.line,
596 | endColumn: end && end.column + 1,
597 | },
598 | ];
599 | return [];
600 | }
601 | const { ast, warnings, vars, mapper } = result;
602 |
603 | padCodeWithMissingNodesLines(ast, text);
604 |
605 | const references_and_reassignments = `{${vars
606 | .filter((v) => v.referenced || v.name[0] === "$")
607 | .map((v) => v.name)};${vars
608 | .filter((v) => v.reassigned || v.export_name)
609 | .map((v) => v.name + "=0")}}`;
610 | state.var_names = new Set(vars.map((v) => v.name));
611 |
612 | // convert warnings to linting messages
613 | const filtered_warnings = processor_options.ignore_warnings
614 | ? warnings.filter((warning) => !processor_options.ignore_warnings(warning))
615 | : warnings;
616 | state.messages = filtered_warnings.map(({ code, message, start, end }) => {
617 | const start_pos =
618 | processor_options.typescript && start
619 | ? mapper.get_original_position(start)
620 | : start && { line: start.line, column: start.column + 1 };
621 | const end_pos =
622 | processor_options.typescript && end
623 | ? mapper.get_original_position(end)
624 | : end && { line: end.line, column: end.column + 1 };
625 | return {
626 | ruleId: code,
627 | severity: 1,
628 | message,
629 | line: start_pos && start_pos.line,
630 | column: start_pos && start_pos.column,
631 | endLine: end_pos && end_pos.line,
632 | endColumn: end_pos && end_pos.column,
633 | };
634 | });
635 |
636 | // build strings that we can send along to ESLint to get the remaining messages
637 |
638 | // Things to think about:
639 | // - not all Svelte files may be typescript -> do we need a distinction on a file basis by analyzing the attribute + a config option to tell "treat all as TS"?
640 | const with_file_ending = (filename) =>
641 | `${filename}${processor_options.typescript ? ".ts" : ".js"}`;
642 |
643 | if (ast.module) {
644 | // block for `;
1067 | }
1068 | );
1069 | const mapper = new DocumentMapper(text, transpiled, diffs);
1070 |
1071 | let ts_result;
1072 | try {
1073 | ts_result = compiler.compile(transpiled, {
1074 | generate: false,
1075 | ...processor_options.compiler_options,
1076 | });
1077 | } catch (err) {
1078 | // remap the error to be in the correct spot and rethrow it
1079 | err.start = mapper.get_original_position(err.start);
1080 | err.end = mapper.get_original_position(err.end);
1081 | throw err;
1082 | }
1083 |
1084 | text = text.replace(
1085 | /`;
1092 | }
1093 | );
1094 | // if we do a full recompile Svelte can fail due to the blank script tag not declaring anything
1095 | // so instead we just parse for the AST (which is likely faster, anyways)
1096 | const ast = compiler.parse(text, { ...processor_options.compiler_options });
1097 | const { warnings, vars } = ts_result;
1098 | return { ast, warnings, vars, mapper };
1099 | }
1100 | }
1101 |
1102 | // transform a linting message according to the module/instance script info we've gathered
1103 | const transform_message = ({ transformed_code }, { unoffsets, dedent, offsets, range }, message) => {
1104 | // strip out the start and end of the fix if they are not actually changes
1105 | if (message.fix) {
1106 | while (message.fix.range[0] < message.fix.range[1] && transformed_code[message.fix.range[0]] === message.fix.text[0]) {
1107 | message.fix.range[0]++;
1108 | message.fix.text = message.fix.text.slice(1);
1109 | }
1110 | while (message.fix.range[0] < message.fix.range[1] && transformed_code[message.fix.range[1] - 1] === message.fix.text[message.fix.text.length - 1]) {
1111 | message.fix.range[1]--;
1112 | message.fix.text = message.fix.text.slice(0, -1);
1113 | }
1114 | }
1115 | // shift position reference backward according to unoffsets
1116 | {
1117 | const { length, lines, last } = unoffsets;
1118 | if (message.line === lines) {
1119 | message.column -= last;
1120 | }
1121 | if (message.endColumn && message.endLine === lines) {
1122 | message.endColumn -= last;
1123 | }
1124 | message.line -= lines - 1;
1125 | if (message.endLine) {
1126 | message.endLine -= lines - 1;
1127 | }
1128 | if (message.fix) {
1129 | message.fix.range[0] -= length;
1130 | message.fix.range[1] -= length;
1131 | }
1132 | }
1133 | // adjust position reference according to the previous dedenting
1134 | {
1135 | const { offsets, total_offsets } = dedent;
1136 | message.column += offsets[message.line - 1];
1137 | if (message.endColumn) {
1138 | message.endColumn += offsets[message.endLine - 1];
1139 | }
1140 | if (message.fix) {
1141 | message.fix.range[0] += total_offsets[message.line];
1142 | message.fix.range[1] += total_offsets[message.line];
1143 | }
1144 | }
1145 | // shift position reference forward according to offsets
1146 | {
1147 | const { length, lines, last } = offsets;
1148 | if (message.line === 1) {
1149 | message.column += last;
1150 | }
1151 | if (message.endColumn && message.endLine === 1) {
1152 | message.endColumn += last;
1153 | }
1154 | message.line += lines - 1;
1155 | if (message.endLine) {
1156 | message.endLine += lines - 1;
1157 | }
1158 | if (message.fix) {
1159 | message.fix.range[0] += length;
1160 | message.fix.range[1] += length;
1161 | }
1162 | }
1163 | // make sure the fix doesn't include anything outside the range of the script
1164 | if (message.fix) {
1165 | if (message.fix.range[0] < range[0]) {
1166 | message.fix.text = message.fix.text.slice(range[0] - message.fix.range[0]);
1167 | message.fix.range[0] = range[0];
1168 | }
1169 | if (message.fix.range[1] > range[1]) {
1170 | message.fix.text = message.fix.text.slice(0, range[1] - message.fix.range[1]);
1171 | message.fix.range[1] = range[1];
1172 | }
1173 | }
1174 | };
1175 |
1176 | // extract the string referenced by a message
1177 | const get_referenced_string = (block, message) => {
1178 | if (message.line && message.column && message.endLine && message.endColumn) {
1179 | if (!block.line_offsets) {
1180 | block.line_offsets = get_line_offsets$1(block.transformed_code);
1181 | }
1182 | return block.transformed_code.slice(block.line_offsets[message.line - 1] + message.column, block.line_offsets[message.endLine - 1] + message.endColumn);
1183 | }
1184 | };
1185 |
1186 | // extract something that looks like an identifier (not supporting unicode escape stuff) from the beginning of a string
1187 | const get_identifier = str => (str && str.match(/^[^\s!"#%&\\'()*+,\-./:;<=>?@[\\\]^`{|}~]+/) || [])[0];
1188 |
1189 | // determine whether this message from ESLint is something we care about
1190 | const is_valid_message = (block, message, translation) => {
1191 | switch (message.ruleId) {
1192 | case 'eol-last': return false;
1193 | case '@typescript-eslint/indent':
1194 | case 'indent': return !translation.options.template;
1195 | case 'linebreak-style': return message.line !== translation.end;
1196 | case 'no-labels': return get_identifier(get_referenced_string(block, message)) !== '$';
1197 | case 'no-restricted-syntax': return message.nodeType !== 'LabeledStatement' || get_identifier(get_referenced_string(block, message)) !== '$';
1198 | case 'no-self-assign': return !state.var_names.has(get_identifier(get_referenced_string(block, message)));
1199 | case 'no-unused-labels': return get_referenced_string(block, message) !== '$';
1200 | case '@typescript-eslint/quotes':
1201 | case 'quotes': return !translation.options.in_quoted_attribute;
1202 | }
1203 | return true;
1204 | };
1205 |
1206 | // transform linting messages and combine with compiler warnings
1207 | const postprocess = blocks_messages => {
1208 | // filter messages and fix their offsets
1209 | const blocks_array = [...state.blocks.values()];
1210 | for (let i = 0; i < blocks_messages.length; i++) {
1211 | const block = blocks_array[i];
1212 | for (let j = 0; j < blocks_messages[i].length; j++) {
1213 | const message = blocks_messages[i][j];
1214 | const translation = block.translations.get(message.line);
1215 | if (translation && is_valid_message(block, message, translation)) {
1216 | transform_message(block, translation, message);
1217 | state.messages.push(message);
1218 | }
1219 | }
1220 | }
1221 |
1222 | // sort messages and return
1223 | const sorted_messages = state.messages.sort((a, b) => a.line - b.line || a.column - b.column);
1224 | reset();
1225 | return sorted_messages;
1226 | };
1227 |
1228 | var index = {
1229 | processors: { svelte3: { preprocess, postprocess, supportsAutofix: true } },
1230 | configs: {
1231 | defaultWithJsx: {
1232 | parserOptions: {
1233 | ecmaFeatures: {
1234 | jsx: true,
1235 | },
1236 | },
1237 | overrides: [
1238 | {
1239 | files: ["**/*.{tsx,jsx}"],
1240 | },
1241 | ],
1242 | },
1243 | },
1244 | };
1245 |
1246 | module.exports = index;
1247 | //# sourceMappingURL=index.js.map
1248 |
--------------------------------------------------------------------------------