├── 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 |
4 |
5 | -------------------------------------------------------------------------------- /test/samples/html/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-custom-rules", 3 | "version": "1.0.1", 4 | "main": "index.js" 5 | } 6 | -------------------------------------------------------------------------------- /test/samples/line-endings/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'linebreak-style': ['error', 'windows'], 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/samples/unused-write-only-store/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | "no-unused-vars": "error", 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/samples/indentation/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | indent: ['error', 'tab'], 4 | semi: 'error', 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /test/samples/typescript-indentation/Input.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 |
{foo}
7 | -------------------------------------------------------------------------------- /test/samples/typescript-template-quotes/Input.svelte: -------------------------------------------------------------------------------- 1 | {'foo'} 2 | {"bar"} 3 |
4 |
5 | -------------------------------------------------------------------------------- /test/samples/typescript-unsafe-member-access/external-file.ts: -------------------------------------------------------------------------------- 1 | export const external_safe = ['hi']; 2 | export const external_unsafe: any = null; 3 | -------------------------------------------------------------------------------- /test/samples/self-assignment/Input.svelte: -------------------------------------------------------------------------------- 1 | 9 | -------------------------------------------------------------------------------- /test/samples/typescript-unsafe-member-access/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*"], 3 | "compilerOptions": { 4 | "jsx": "preserve" 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /test/samples/typescript-type-aware-rules/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "include": ["**/*"], 3 | "compilerOptions": { 4 | "jsx": "preserve" 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /test/samples/module-context/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-undef': 'error', 4 | 'no-unused-vars': 'error', 5 | 'prefer-const': 'error', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /test/samples/typescript-bind-reference/Input.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/state.js: -------------------------------------------------------------------------------- 1 | export let state; 2 | export const reset = () => { 3 | state = { 4 | messages: null, 5 | var_names: null, 6 | blocks: new Map(), 7 | }; 8 | }; 9 | reset(); 10 | -------------------------------------------------------------------------------- /test/samples/ignore-styles/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | settings: { 3 | 'svelte3/ignore-styles': attributes => attributes.foo && attributes.foo.includes('bar'), 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /test/samples/line-endings/Input.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | {foo} 10 | -------------------------------------------------------------------------------- /test/samples/labels/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-labels': 'error', 4 | 'no-restricted-syntax': ['error', 'LabeledStatement'], 5 | 'no-unused-labels': 'error', 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /test/samples/script-reference/Input.svelte: -------------------------------------------------------------------------------- 1 | 7 | 8 |
foo = true}>foo
9 | -------------------------------------------------------------------------------- /test/samples/scope/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | rules: { 3 | 'no-undef': 'error', 4 | }, 5 | settings: { 6 | 'svelte3/ignore-warnings': ({ code }) => code === 'missing-declaration', 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import node_resolve from '@rollup/plugin-node-resolve'; 2 | 3 | export default { 4 | input: 'src/index.js', 5 | output: { file: 'index.js', format: 'cjs' }, 6 | plugins: [ node_resolve() ], 7 | }; 8 | -------------------------------------------------------------------------------- /test/samples/typescript-type-aware-rules/Input.svelte: -------------------------------------------------------------------------------- 1 | 12 | -------------------------------------------------------------------------------- /test/samples/block-filenames/Input.svelte: -------------------------------------------------------------------------------- 1 | 4 | 5 | 9 | 10 |
{ if (foo) bar; }}>blah
11 | -------------------------------------------------------------------------------- /test/samples/line-endings/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ruleId": "missing-declaration", 4 | "severity": 1, 5 | "message": "'foo' is not defined", 6 | "line": 9, 7 | "column": 2, 8 | "endLine": 9, 9 | "endColumn": 5 10 | } 11 | ] -------------------------------------------------------------------------------- /test/samples/compiler-error/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ruleId": "ParseError", 4 | "severity": 2, 5 | "message": " 4 | 5 | 9 | 10 |
{ if (foo) bar; }}>blah
11 | -------------------------------------------------------------------------------- /test/samples/typescript-template-quotes/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: [ 4 | '@typescript-eslint', 5 | ], 6 | rules: { 7 | quotes: 'off', 8 | '@typescript-eslint/quotes': ['error', 'single'] 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /test/samples/module-context/Input.svelte: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /test/samples/typescript-bind-reference/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: ['plugin:@typescript-eslint/recommended'], 4 | plugins: ['@typescript-eslint'], 5 | settings: { 6 | 'svelte3/typescript': require('typescript'), 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /test/samples/typescript-imports/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: ['@typescript-eslint'], 4 | settings: { 5 | 'svelte3/typescript': require('typescript'), 6 | }, 7 | rules: { 8 | 'no-unused-vars': 'error', 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /node_modules 3 | /package-lock.json 4 | /pnpm-lock.yaml 5 | /test/samples/*/actual.json 6 | /yarn.lock 7 | .idea 8 | /test/samples/*/instance.* 9 | /test/samples/*/module.* 10 | /test/samples/*/template.* 11 | /test/samples/*/svelte*.tsx 12 | /test/samples/*/svelte*.jsx 13 | -------------------------------------------------------------------------------- /test/samples/typescript-indentation/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | plugins: [ 4 | '@typescript-eslint', 5 | ], 6 | rules: { 7 | indent: 'off', 8 | '@typescript-eslint/indent': ['error', 'tab'], 9 | semi: 'error', 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /test/samples/self-assignment/expected.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "ruleId": "no-self-assign", 4 | "severity": 2, 5 | "message": "'baz' is assigned to itself.", 6 | "line": 6, 7 | "column": 9, 8 | "nodeType": "Identifier", 9 | "messageId": "selfAssignment", 10 | "endLine": 6, 11 | "endColumn": 12 12 | } 13 | ] -------------------------------------------------------------------------------- /test/samples/unused-write-only-store/Input.svelte: -------------------------------------------------------------------------------- 1 | 12 |
$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 | /([^]*?)<\/script>/gi, 583 | (match, attributes = "", content) => { 584 | return `${content 585 | // blank out the content 586 | .replace(/[^\n]/g, " ") 587 | // excess blank space can make the svelte parser very slow (sec->min). break it up with comments (works in style/script) 588 | .replace(/[^\n][^\n][^\n][^\n]\n/g, "/**/\n")}`; 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 | /([^]*?)<\/script>/gi, 1086 | (match, attributes = "", content) => { 1087 | return `${content 1088 | // blank out the content 1089 | .replace(/[^\n]/g, " ") 1090 | // excess blank space can make the svelte parser very slow (sec->min). break it up with comments (works in style/script) 1091 | .replace(/[^\n][^\n][^\n][^\n]\n/g, "/**/\n")}`; 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 | --------------------------------------------------------------------------------