├── docs ├── .nojekyll ├── _404.md ├── _coverpage.md ├── _sidebar.md ├── rules │ ├── use-homepage-and-url.md │ ├── use-download-and-update-url.md │ ├── no-invalid-grant.md │ ├── filename-user.md │ ├── align-attributes.md │ ├── require-attribute-space-prefix.md │ ├── require-version.md │ ├── require-description.md │ ├── require-name.md │ ├── no-invalid-headers.md │ └── no-invalid-metadata.md ├── index.html └── README.md ├── .npmrc ├── .prettierrc.json ├── .markdownlint.json ├── .npmignore ├── .gitignore ├── .husky └── pre-commit ├── tests └── lib │ ├── index.js │ ├── rules │ ├── avoid-regexp-include.js │ ├── use-homepage-and-url.js │ ├── use-download-and-update-url.js │ ├── require-description.js │ ├── align-attributes.js │ ├── no-invalid-grant.js │ ├── require-version.js │ ├── require-attribute-space-prefix.js │ ├── no-invalid-headers.js │ ├── filename-user.js │ ├── no-invalid-metadata.js │ └── require-name.js │ └── utils │ └── createValidator.js ├── .github ├── dependabot.yml └── workflows │ ├── publish.yml │ ├── lint.yml │ ├── test.yml │ └── readme-in-sync.yml ├── lib ├── rules │ ├── avoid-regexp-include.js │ ├── require-description.js │ ├── require-version.js │ ├── use-download-and-update-url.js │ ├── use-homepage-and-url.js │ ├── filename-user.js │ ├── require-attribute-space-prefix.js │ ├── no-invalid-grant.js │ ├── no-invalid-headers.js │ ├── align-attributes.js │ ├── require-name.js │ └── no-invalid-metadata.js ├── index.js └── utils │ └── createValidator.js ├── .eslintrc.json ├── LICENSE ├── package.json └── README.md /docs/.nojekyll: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "none" 4 | } 5 | -------------------------------------------------------------------------------- /.markdownlint.json: -------------------------------------------------------------------------------- 1 | { 2 | "line-length": { 3 | "code_blocks": false, 4 | "tables": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | tests/ 2 | docs/ 3 | .github/ 4 | .husky/ 5 | .prettierrc.json 6 | .eslintrc.json 7 | .markdownlint.json 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules/ 3 | package-lock.json 4 | 5 | # Code Coverage 6 | .nyc_output/ 7 | coverage/ 8 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | cp README.md docs/README.md && git add docs/README.md 5 | -------------------------------------------------------------------------------- /tests/lib/index.js: -------------------------------------------------------------------------------- 1 | const requireindex = require('requireindex'); 2 | 3 | module.exports = requireindex(__dirname.replace(/\/tests(\/lib)$/, '$1/rules')); 4 | -------------------------------------------------------------------------------- /docs/_404.md: -------------------------------------------------------------------------------- 1 | 2 | 3 |

404: Not Found

4 | 5 |

Try going back to the homepage

6 | -------------------------------------------------------------------------------- /docs/_coverpage.md: -------------------------------------------------------------------------------- 1 | # `eslint-plugin-userscripts` 2 | 3 | > The ESLint plugin for userscripts 4 | 5 | [GitHub](https://github.com/Yash-Singh1/eslint-plugin-userscripts/) 6 | [Get Started](?id=main) 7 | 8 | 9 | 10 | ![color](#a11616) 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: '/' 5 | schedule: 6 | interval: daily 7 | time: '13:00' 8 | open-pull-requests-limit: 10 9 | 10 | - package-ecosystem: 'github-actions' 11 | directory: '/' 12 | schedule: 13 | interval: 'daily' 14 | -------------------------------------------------------------------------------- /tests/lib/rules/avoid-regexp-include.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['avoid-regexp-include']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('avoid-regexp-include', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // @description This is my description 9 | // @include https://* 10 | // ==/UserScript==`, 11 | ], 12 | invalid: [ 13 | { 14 | code: `// ==UserScript== 15 | // @include /https?:\\/\\/foo.bar\\/.*/ 16 | // ==/UserScript==`, 17 | errors: [{ messageId: 'avoidRegExpInclude' }] 18 | } 19 | ] 20 | }); -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [published, edited] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Clone repository 12 | uses: actions/checkout@v2.3.5 13 | 14 | - name: Set up Node.js 15 | uses: actions/setup-node@v2.4.1 16 | with: 17 | node-version: 14.x 18 | registry-url: https://registry.npmjs.org/ 19 | 20 | - name: Install npm dependencies 21 | run: npm install 22 | 23 | - name: Publish Package 24 | run: npm publish --access public 25 | env: 26 | NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}} 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint pull requests or commits 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | types: 10 | - opened 11 | - synchronize 12 | - ready_for_review 13 | jobs: 14 | test: 15 | name: Lint code 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Clone repository 19 | uses: actions/checkout@v2.3.5 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v2.4.1 23 | with: 24 | node-version: 14.x 25 | 26 | - name: Install npm dependencies 27 | run: npm install 28 | 29 | - name: Lint code 30 | run: npm run lint 31 | -------------------------------------------------------------------------------- /lib/rules/avoid-regexp-include.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../utils/createValidator'); 2 | 3 | module.exports = createValidator( 4 | 'include', 5 | false, 6 | ({ attrVal, context }) => { 7 | const argument = attrVal.val; 8 | if (argument.startsWith('/')) { 9 | context.report({ 10 | loc: { 11 | start: { 12 | line: attrVal.loc.start.line, 13 | column: 0 14 | }, 15 | end: attrVal.loc.end 16 | }, 17 | messageId: 'avoidRegExpInclude' 18 | }); 19 | } 20 | }, 21 | { 22 | avoidRegExpInclude: "Using a regular expression at '@include' can cause performance issues. Use a regular @include or @match instead." 23 | } 24 | ); -------------------------------------------------------------------------------- /lib/rules/require-description.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../utils/createValidator'); 2 | 3 | const descriptionReg = /^description(:\S+)?$/; 4 | 5 | module.exports = createValidator( 6 | 'description', 7 | true, 8 | ({ attrVal, context }) => { 9 | let iteratedKeyNames = []; 10 | for (let attrValue of attrVal) { 11 | if (iteratedKeyNames.includes(attrValue.key)) { 12 | context.report({ 13 | loc: attrValue.loc, 14 | messageId: 'multipleDescriptions' 15 | }); 16 | } else { 17 | iteratedKeyNames.push(attrValue.key); 18 | } 19 | } 20 | }, 21 | { 22 | multipleDescriptions: 'Include only one description for each language' 23 | }, 24 | false, 25 | descriptionReg, 26 | true 27 | ); 28 | -------------------------------------------------------------------------------- /docs/_sidebar.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | - Introduction 4 | 5 | - [Getting Started](README.md) 6 | 7 | - Rules 8 | 9 | - [`filename-user`](rules/filename-user.md) 10 | - [`no-invalid-grant`](rules/no-invalid-grant.md) 11 | - [`no-invalid-metadata`](rules/no-invalid-metadata.md) 12 | - [`require-name`](rules/require-name.md) 13 | - [`require-description`](rules/require-description.md) 14 | - [`require-version`](rules/require-version.md) 15 | - [`require-attribute-space-prefix`](rules/require-attribute-space-prefix.md) 16 | - [`use-homepage-and-url`](rules/use-homepage-and-url.md) 17 | - [`use-download-and-update-url`](rules/use-download-and-update-url.md) 18 | - [`align-attributes`](rules/align-attributes.md) 19 | - [`no-invalid-headers`](rules/no-invalid-headers.md) 20 | -------------------------------------------------------------------------------- /tests/lib/utils/createValidator.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../../../lib/utils/createValidator.js'); 2 | const assert = require('node:assert'); 3 | 4 | it('should properly generate description', () => { 5 | assert.strictEqual( 6 | createValidator('attributeName', false).meta.docs.description, 7 | 'validate attributeName in the metadata for userscripts' 8 | ); 9 | assert.strictEqual( 10 | createValidator('attributeName', true).meta.docs.description, 11 | 'require attributeName in the metadata for userscripts' 12 | ); 13 | assert.strictEqual( 14 | createValidator( 15 | 'attributeName2', 16 | true, 17 | ( 18 | /* eslint-disable-next-line no-unused-vars */ 19 | arg 20 | ) => {} 21 | ).meta.docs.description, 22 | 'require and validate attributeName2 in the metadata for userscripts' 23 | ); 24 | }); 25 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests on Pull Request or Push 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | types: 10 | - opened 11 | - synchronize 12 | - ready_for_review 13 | jobs: 14 | test: 15 | name: Run tests 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Clone repository 19 | uses: actions/checkout@v2.3.5 20 | 21 | - name: Set up Node.js 22 | uses: actions/setup-node@v2.4.1 23 | with: 24 | node-version: 14.x 25 | 26 | - name: Install npm dependencies 27 | run: npm install 28 | 29 | - name: Run test against code 30 | run: npm run test 31 | 32 | - name: Upload coverage to Codecov 33 | uses: codecov/codecov-action@v2 34 | with: 35 | token: ${{ secrets.CODECOV_TOKEN }} 36 | fail_ci_if_error: true 37 | -------------------------------------------------------------------------------- /lib/rules/require-version.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../utils/createValidator'); 2 | 3 | module.exports = createValidator( 4 | 'version', 5 | true, 6 | ({ attrVal, index, context }) => { 7 | if (index > 0) { 8 | context.report({ 9 | loc: attrVal.loc, 10 | messageId: 'multipleVersions' 11 | }); 12 | } 13 | if (!/^([^\s.]+)(\.[^\s.]+)*$/.test(attrVal.val)) { 14 | context.report({ 15 | loc: { 16 | start: { 17 | line: attrVal.loc.start.line, 18 | column: 19 | /^(\s*\/\/\s*)/.exec( 20 | context.getSourceCode().lines[attrVal.comment.loc.start.line] 21 | )[1].length - 1 22 | }, 23 | end: attrVal.loc.end 24 | }, 25 | messageId: 'invalidVersion' 26 | }); 27 | } 28 | }, 29 | { 30 | multipleVersions: 'Include only one version', 31 | invalidVersion: 'Invalid version' 32 | } 33 | ); 34 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const requireIndex = require('requireindex'); 4 | 5 | module.exports.rules = requireIndex(__dirname + '/rules'); 6 | 7 | module.exports.configs = { 8 | recommended: { 9 | plugins: ['userscripts'], 10 | rules: { 11 | 'userscripts/filename-user': ['error', 'always'], 12 | 'userscripts/no-invalid-metadata': ['error', { top: 'required' }], 13 | 'userscripts/require-name': ['error', 'required'], 14 | 'userscripts/require-description': ['error', 'required'], 15 | 'userscripts/require-version': ['error', 'required'], 16 | 'userscripts/require-attribute-space-prefix': 'error', 17 | 'userscripts/use-homepage-and-url': 'error', 18 | 'userscripts/use-download-and-update-url': 'error', 19 | 'userscripts/align-attributes': ['error', 2], 20 | 'userscripts/no-invalid-headers': 'error', 21 | 'userscripts/no-invalid-grant': 'error', 22 | 'userscripts/avoid-regexp-include': 'warning' 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@babel/eslint-parser", 3 | "plugins": ["eslint-plugin", "import", "unicorn"], 4 | "extends": [ 5 | "eslint:recommended", 6 | "plugin:eslint-plugin/recommended", 7 | "plugin:import/recommended", 8 | "plugin:unicorn/recommended" 9 | ], 10 | "parserOptions": { 11 | "requireConfigFile": false 12 | }, 13 | "env": { 14 | "node": true 15 | }, 16 | "rules": { 17 | "eslint-plugin/test-case-property-ordering": "error", 18 | "eslint-plugin/test-case-shorthand-strings": "error", 19 | "unicorn/prefer-module": "off", 20 | "unicorn/filename-case": "off", 21 | "unicorn/prefer-array-some": "off", 22 | "unicorn/prevent-abbreviations": "off", 23 | "unicorn/no-array-reduce": "off", 24 | "unicorn/no-nested-ternary": "off", 25 | "unicorn/prefer-array-find": "error", 26 | "unicorn/no-null": "off" 27 | }, 28 | "overrides": [ 29 | { 30 | "files": "tests/**/*.js", 31 | "env": { 32 | "mocha": true 33 | } 34 | } 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/readme-in-sync.yml: -------------------------------------------------------------------------------- 1 | name: Ensure both READMEs are synced 2 | on: 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | types: 10 | - opened 11 | - synchronize 12 | - ready_for_review 13 | 14 | jobs: 15 | test: 16 | name: Run tests 17 | runs-on: ubuntu-latest 18 | steps: 19 | - name: Clone repository 20 | uses: actions/checkout@v2.3.5 21 | 22 | - name: Set up Node.js 23 | uses: actions/setup-node@v2.4.1 24 | with: 25 | node-version: 14.x 26 | 27 | - name: Install npm dependencies 28 | run: npm install 29 | 30 | - name: Copy Documentation README 31 | run: ./.husky/pre-commit 32 | 33 | - name: Ensure there is no Git Changes 34 | run: | 35 | if [ -z "$(git status --short)" ] 36 | then 37 | echo "README.md and docs/README.md are in sync" 38 | else 39 | echo "Make sure that the README.md and docs/README.md are in sync" 40 | exit 1 41 | fi 42 | -------------------------------------------------------------------------------- /lib/rules/use-download-and-update-url.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../utils/createValidator'); 2 | 3 | const updateURLs = ['downloadURL', 'updateURL']; 4 | 5 | module.exports = createValidator( 6 | updateURLs, 7 | false, 8 | ({ attrVal, metadata, context, keyName }) => { 9 | const attribute = updateURLs.find((updateURL) => updateURL !== keyName); 10 | if (!metadata[attribute]) { 11 | context.report({ 12 | loc: attrVal.loc, 13 | messageId: 'missingAttribute', 14 | data: { 15 | attribute: attribute 16 | }, 17 | fix: function (fixer) { 18 | return fixer.insertTextAfterRange( 19 | attrVal.comment.range, 20 | `\n${context 21 | .getSourceCode() 22 | .lines[attrVal.comment.loc.start.line - 1].replace( 23 | /^(\s*\/\/\s*@)\S*/, 24 | '$1' + attribute 25 | )}` 26 | ); 27 | } 28 | }); 29 | } 30 | }, 31 | { 32 | missingAttribute: "Didn't find attribute '{{ attribute }}' in the metadata" 33 | }, 34 | true 35 | ); 36 | -------------------------------------------------------------------------------- /lib/rules/use-homepage-and-url.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../utils/createValidator'); 2 | 3 | const homepageAttrs = ['homepage', 'homepageURL']; 4 | 5 | module.exports = createValidator( 6 | homepageAttrs, 7 | false, 8 | ({ attrVal, metadata, context, keyName }) => { 9 | const attribute = homepageAttrs.find( 10 | (homepageAttr) => homepageAttr !== keyName 11 | ); 12 | if (!metadata[attribute]) { 13 | context.report({ 14 | loc: attrVal.loc, 15 | messageId: 'missingAttribute', 16 | data: { 17 | attribute: attribute 18 | }, 19 | fix: function (fixer) { 20 | return fixer.insertTextAfterRange( 21 | attrVal.comment.range, 22 | `\n${context 23 | .getSourceCode() 24 | .lines[attrVal.comment.loc.start.line - 1].replace( 25 | /^(\s*\/\/\s*@)\S*/, 26 | '$1' + attribute 27 | )}` 28 | ); 29 | } 30 | }); 31 | } 32 | }, 33 | { 34 | missingAttribute: "Didn't find attribute '{{ attribute }}' in the metadata" 35 | }, 36 | true 37 | ); 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2021 Yash Singh 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 13 | all 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/rules/use-homepage-and-url.md: -------------------------------------------------------------------------------- 1 | # `use-homepage-and-url` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | 7 | 8 | > 🔧 The `--fix` option on the command line can automatically fix some of the 9 | > problems reported by this rule. 10 | 11 | The `use-homepage-and-url` rule verifies that if the `homepage` attribute is 12 | present then the `homepageURL` attribute is too and vice versa. 13 | 14 | ## Why? 15 | 16 | For compatibility with different userscript runners. 17 | 18 | ## Examples 19 | 20 | 👍 Examples of **correct** code for this rule 21 | 22 | ```js 23 | /* eslint userscripts/use-homepage-and-url: "error" */ 24 | 25 | // ==UserScript== 26 | // @homepage example.com 27 | // @homepageURL example.com 28 | // ==/UserScript== 29 | ``` 30 | 31 | 👎︎ Examples of **incorrect** code for this rule 32 | 33 | ```js 34 | /* eslint userscripts/use-homepage-and-url: "error" */ 35 | 36 | // ==UserScript== 37 | // @homepage example.com 38 | // ==/UserScript== 39 | ``` 40 | 41 | ## When Not to Use It 42 | 43 | When you are sure that the userscript runner that you and your users use is 44 | compatible with your choice. 45 | -------------------------------------------------------------------------------- /tests/lib/rules/use-homepage-and-url.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['use-homepage-and-url']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('use-homepage-and-url', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // @homepage example.com 9 | // @homepageURL example.com 10 | // ==/UserScript==` 11 | ], 12 | invalid: [ 13 | { 14 | code: `// ==UserScript== 15 | // @homepage example.com 16 | // ==/UserScript==`, 17 | output: `// ==UserScript== 18 | // @homepage example.com 19 | // @homepageURL example.com 20 | // ==/UserScript==`, 21 | errors: [ 22 | { 23 | messageId: 'missingAttribute', 24 | data: { attribute: 'homepageURL' } 25 | } 26 | ] 27 | }, 28 | { 29 | code: `// ==UserScript== 30 | // @homepageURL example.com 31 | // ==/UserScript==`, 32 | output: `// ==UserScript== 33 | // @homepageURL example.com 34 | // @homepage example.com 35 | // ==/UserScript==`, 36 | errors: [ 37 | { 38 | messageId: 'missingAttribute', 39 | data: { attribute: 'homepage' } 40 | } 41 | ] 42 | } 43 | ] 44 | }); 45 | -------------------------------------------------------------------------------- /docs/rules/use-download-and-update-url.md: -------------------------------------------------------------------------------- 1 | # `use-download-and-update-url` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | 7 | 8 | > 🔧 The `--fix` option on the command line can automatically fix some of the 9 | > problems reported by this rule. 10 | 11 | The `use-download-and-update-url` rule verifies that if the `updateURL` attribute 12 | is present then the `downloadURL` attribute is too and vice versa. 13 | 14 | ## Why? 15 | 16 | For compatibility with different userscript runners. 17 | 18 | ## Examples 19 | 20 | 👍 Examples of **correct** code for this rule 21 | 22 | ```js 23 | /* eslint userscripts/use-homepage-and-url: "error" */ 24 | 25 | // ==UserScript== 26 | // @updateURL example.com 27 | // @downloadURL example.com 28 | // ==/UserScript== 29 | ``` 30 | 31 | 👎︎ Examples of **incorrect** code for this rule 32 | 33 | ```js 34 | /* eslint userscripts/use-homepage-and-url: "error" */ 35 | 36 | // ==UserScript== 37 | // @downloadURL example.com 38 | // ==/UserScript== 39 | ``` 40 | 41 | ## When Not to Use It 42 | 43 | When you are sure that the userscript runner that you and your users use is 44 | compatible with your choice. 45 | -------------------------------------------------------------------------------- /tests/lib/rules/use-download-and-update-url.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['use-download-and-update-url']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('use-download-and-update-url', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // @downloadURL example.com 9 | // @updateURL example.com 10 | // ==/UserScript==` 11 | ], 12 | invalid: [ 13 | { 14 | code: `// ==UserScript== 15 | // @downloadURL example.com 16 | // ==/UserScript==`, 17 | output: `// ==UserScript== 18 | // @downloadURL example.com 19 | // @updateURL example.com 20 | // ==/UserScript==`, 21 | errors: [ 22 | { 23 | messageId: 'missingAttribute', 24 | data: { attribute: 'updateURL' } 25 | } 26 | ] 27 | }, 28 | { 29 | code: `// ==UserScript== 30 | // @updateURL example.com 31 | // ==/UserScript==`, 32 | output: `// ==UserScript== 33 | // @updateURL example.com 34 | // @downloadURL example.com 35 | // ==/UserScript==`, 36 | errors: [ 37 | { 38 | messageId: 'missingAttribute', 39 | data: { attribute: 'downloadURL' } 40 | } 41 | ] 42 | } 43 | ] 44 | }); 45 | -------------------------------------------------------------------------------- /docs/rules/no-invalid-grant.md: -------------------------------------------------------------------------------- 1 | # `no-invalid-grant` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | The `no-invalid-grant` rule verifies that the argument passed to `@grant` is valid. 7 | 8 | ## Why? 9 | 10 | So as to avoid typos that might result in `GM_* is not defined` errors. 11 | 12 | ## Examples 13 | 14 | 👍 Examples of **correct** code for this rule 15 | 16 | ```js 17 | /* eslint userscripts/no-invalid-grant: "error" */ 18 | 19 | // ==UserScript== 20 | // @grant GM_info 21 | // @grant GM.info 22 | // @grant GM_getValue 23 | // @grant GM.getValue 24 | // @grant GM_getResourceURL 25 | // @grant GM.getResourceUrl 26 | // @grant GM_xmlhttpRequest 27 | // @grant GM.xmlHttpRequest 28 | // @grant unsafeWindow 29 | // @grant window.onurlchange 30 | // ==/UserScript== 31 | ``` 32 | 33 | 👎︎ Examples of **incorrect** code for this rule 34 | 35 | ```js 36 | /* eslint userscripts/no-invalid-grant: "error" */ 37 | 38 | // ==UserScript== 39 | // @grant GM_notificaiton 40 | // @grant GM_getResourceUrl 41 | // @grant GM_xmlHttpRequest 42 | // ==/UserScript== 43 | ``` 44 | 45 | ## When Not to Use It 46 | 47 | Turn off this rule if you don't want to check the validity of `@grant` arguments. 48 | -------------------------------------------------------------------------------- /docs/rules/filename-user.md: -------------------------------------------------------------------------------- 1 | # `filename-user` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | The `filename-user` rule verifies that the filename ends in `.user.js`. 7 | 8 | ## Why? 9 | 10 | It is a good practice to end userscripts in a `.user.js`. 11 | 12 | ## Options 13 | 14 | This rule has a string option: 15 | 16 | - `"always"` (default) requires the filename to end in `.user.js` 17 | - `"never"` ensures that the filename never ends in `.user.js` 18 | 19 | ## Examples 20 | 21 | ### `"always"` 22 | 23 | 👍 Examples of **correct** code for this rule 24 | 25 | ```js 26 | /* eslint userscripts/filename-user: "error" */ 27 | 28 | // hello.user.js 29 | ``` 30 | 31 | 👎︎ Examples of **incorrect** code for this rule 32 | 33 | ```js 34 | /* eslint userscripts/filename-user: "error" */ 35 | 36 | // hello.js 37 | ``` 38 | 39 | ### `"never"` 40 | 41 | 👍 Examples of **correct** code for this rule 42 | 43 | ```js 44 | /* eslint userscripts/filename-user: ["error", "never"] */ 45 | 46 | // hello.js 47 | ``` 48 | 49 | 👎︎ Examples of **incorrect** code for this rule 50 | 51 | ```js 52 | /* eslint userscripts/filename-user: ["error", "never"] */ 53 | 54 | // hello.user.js 55 | ``` 56 | 57 | ## When Not to Use It 58 | 59 | Turn off this rule when you are not linting userscripts. 60 | -------------------------------------------------------------------------------- /lib/rules/filename-user.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | meta: { 3 | type: 'suggestion', 4 | docs: { 5 | description: 'ensure userscripts end with .user.js', 6 | category: 'Best Practices' 7 | }, 8 | schema: [ 9 | { 10 | enum: ['always', 'never'] 11 | } 12 | ], 13 | messages: { 14 | filenameExtension: "Rename '{{ oldFilename }}' to '{{ newFilename }}'" 15 | } 16 | }, 17 | create: (context) => { 18 | const fileName = context.getFilename(); 19 | 20 | if (fileName === '' || fileName === '') { 21 | return {}; 22 | } 23 | 24 | return { 25 | Program() { 26 | if ( 27 | (!fileName.endsWith('.user.js') && 28 | (!context.options[0] || context.options[0] === 'always')) || 29 | (fileName.endsWith('.user.js') && context.options[0] === 'never') 30 | ) { 31 | context.report({ 32 | loc: { column: 0, line: 1 }, 33 | messageId: 'filenameExtension', 34 | data: { 35 | newFilename: fileName.replace( 36 | context.options[0] === 'always' ? /.js$/ : /.user.js$/, 37 | context.options[0] === 'always' ? '.user.js' : '.js' 38 | ), 39 | oldFilename: fileName 40 | } 41 | }); 42 | } 43 | } 44 | }; 45 | } 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-userscripts", 3 | "version": "0.2.0", 4 | "description": "Implements rules for userscripts metadata in eslint", 5 | "keywords": [ 6 | "eslint", 7 | "eslintplugin", 8 | "eslint-plugin" 9 | ], 10 | "author": "Yash Singh", 11 | "main": "./lib/index.js", 12 | "scripts": { 13 | "test": "nyc --reporter=lcov --reporter=text mocha tests --recursive", 14 | "lint": "eslint . --ignore-path .gitignore && prettier --check . --ignore-path .gitignore", 15 | "lint:fix": "eslint . --ignore-path .gitignore --fix && prettier --write . --ignore-path .gitignore", 16 | "prepare": "husky install" 17 | }, 18 | "dependencies": { 19 | "requireindex": "~1.2.0" 20 | }, 21 | "devDependencies": { 22 | "@babel/eslint-parser": "^7.15.8", 23 | "acorn": "^8.5.0", 24 | "eslint": "^8.0.0", 25 | "eslint-plugin-eslint-plugin": ">=4.0.0", 26 | "eslint-plugin-import": ">=2.24.2", 27 | "eslint-plugin-unicorn": ">=37.0.1", 28 | "husky": "^7.0.2", 29 | "mocha": "^9.0.3", 30 | "nyc": "^15.1.0", 31 | "prettier": "^2.4.0" 32 | }, 33 | "peerDependencies": { 34 | "eslint": ">=6.0.0 <=^8.0.0" 35 | }, 36 | "engines": { 37 | "node": ">=16.0.0" 38 | }, 39 | "license": "MIT", 40 | "homepage": "https://github.com/Yash-Singh1/eslint-plugin-userscripts#readme", 41 | "repository": { 42 | "type": "git", 43 | "url": "https://github.com/Yash-Singh1/eslint-plugin-userscripts.git" 44 | }, 45 | "bugs": { 46 | "url": "https://github.com/Yash-Singh1/eslint-plugin-userscripts/issues" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /lib/rules/require-attribute-space-prefix.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | meta: { 3 | type: 'suggestion', 4 | docs: { 5 | description: 'ensure atributes are prefixed by one space', 6 | category: 'Possible Errors' 7 | }, 8 | messages: { 9 | attributeNotPrefixedBySpace: 'Attributes should be prefixed by one space' 10 | }, 11 | schema: [] 12 | }, 13 | create: (context) => { 14 | const sourceCode = context.getSourceCode(); 15 | const lines = sourceCode.lines; 16 | let inMetadata = false; 17 | let done = false; 18 | 19 | for (const [index, line] of lines.entries()) { 20 | if (done) { 21 | continue; 22 | } 23 | 24 | const tl = line.trim(); 25 | if ( 26 | inMetadata && 27 | tl.startsWith('//') && 28 | tl.slice(2).trim() === '==/UserScript==' 29 | ) { 30 | done = true; 31 | } else if ( 32 | !inMetadata && 33 | tl.startsWith('//') && 34 | tl.slice(2).trim() === '==UserScript==' 35 | ) { 36 | inMetadata = true; 37 | } else if ( 38 | inMetadata && 39 | tl.slice(2).trim().startsWith('@') && 40 | tl.startsWith('//') && 41 | (!tl.startsWith('// ') || tl.startsWith('// ')) 42 | ) { 43 | context.report({ 44 | loc: { 45 | start: { 46 | line: index + 1, 47 | column: 0 48 | }, 49 | end: { 50 | line: index + 1, 51 | column: line.length 52 | } 53 | }, 54 | messageId: 'attributeNotPrefixedBySpace' 55 | }); 56 | } 57 | } 58 | 59 | return {}; 60 | } 61 | }; 62 | -------------------------------------------------------------------------------- /docs/rules/align-attributes.md: -------------------------------------------------------------------------------- 1 | # `align-attributes` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | 7 | 8 | > 🔧 The `--fix` option on the command line can automatically fix some of the 9 | > problems reported by this rule. 10 | 11 | The `align-attributes` rule verifies that the attributes are spaced out. 12 | 13 | ## Why? 14 | 15 | For readability when debugging and editing the userscript. 16 | 17 | ## Options 18 | 19 | This rule has a number option which represents the spaces after the longest 20 | attribute name. Defaults to 2. 21 | 22 | ## Examples 23 | 24 | 👍 Examples of **correct** code for this rule 25 | 26 | ```js 27 | /* eslint userscripts/align-attributes: "error" */ 28 | 29 | // ==UserScript== 30 | // @updateURL example.com 31 | // @downloadURL example.com 32 | // ==/UserScript== 33 | ``` 34 | 35 | ```js 36 | /* eslint userscripts/align-attributes: ["error", 3] */ 37 | 38 | // ==UserScript== 39 | // @updateURL example.com 40 | // @downloadURL example.com 41 | // ==/UserScript== 42 | ``` 43 | 44 | 👎︎ Examples of **incorrect** code for this rule 45 | 46 | ```js 47 | /* eslint userscripts/align-attributes: "error" */ 48 | 49 | // ==UserScript== 50 | // @updateURL example.com 51 | // @downloadURL example.com 52 | // ==/UserScript== 53 | ``` 54 | 55 | ```js 56 | /* eslint userscripts/align-attributes: ["error", 3] */ 57 | 58 | // ==UserScript== 59 | // @updateURL example.com 60 | // @downloadURL example.com 61 | // ==/UserScript== 62 | ``` 63 | 64 | ## When Not to Use It 65 | 66 | When you are not using userscripts or don't want to auto-align your attributes. 67 | -------------------------------------------------------------------------------- /docs/rules/require-attribute-space-prefix.md: -------------------------------------------------------------------------------- 1 | # `require-attribute-space-prefix` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | The `require-attribute-space-prefix` rule verifies that the header attribute are 7 | prefixed by exactly one space. 8 | 9 | ## Why? 10 | 11 | To ensure maximum compatibility. 12 | 13 | ## Options 14 | 15 | This rule has no options. 16 | 17 | ## Examples 18 | 19 | 👍 Examples of **correct** code for this rule 20 | 21 | ```js 22 | /* eslint userscripts/require-attribute-space-prefix: "error" */ 23 | 24 | // ==UserScript== 25 | // @name Deletes the X Button 26 | // @description Some info on my userscript 27 | // ==/UserScript== 28 | ``` 29 | 30 | ```js 31 | /* eslint userscripts/require-attribute-space-prefix: "error" */ 32 | 33 | // ==UserScript== 34 | // @name Deletes the X Button 35 | // @description Some info on my userscript 36 | // ==/UserScript== 37 | ``` 38 | 39 | ```js 40 | /* eslint userscripts/require-attribute-space-prefix: "error" */ 41 | 42 | // ==UserScript== 43 | // @name Deletes the X Button 44 | // 45 | // @description Some info on my userscript 46 | // ==/UserScript== 47 | ``` 48 | 49 | 👎︎ Examples of **incorrect** code for this rule 50 | 51 | ```js 52 | /* eslint userscripts/require-attribute-space-prefix: "error" */ 53 | 54 | // ==UserScript== 55 | // @name Deletes the X Button 56 | //@description Some info on my userscript 57 | // ==/UserScript== 58 | ``` 59 | 60 | ```js 61 | /* eslint userscripts/require-attribute-space-prefix: "error" */ 62 | 63 | // ==UserScript== 64 | //@description Some info on my userscript 65 | //@name Deletes the X Button 66 | // ==/UserScript== 67 | ``` 68 | 69 | ## When Not to Use It 70 | 71 | This rule should apply to all userscripts. 72 | -------------------------------------------------------------------------------- /tests/lib/rules/require-description.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['require-description']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('require-description', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // @description This is my description 9 | // ==/UserScript==`, 10 | `// ==UserScript== 11 | // @description This is my description 12 | // @description:en This is my second description 13 | // ==/UserScript== 14 | // more comments here`, 15 | `// ==UserScript== 16 | // @description This is my description 17 | // @description:en This is my second description 18 | // @description:es This is my third description in Spanish 19 | // ==/UserScript== 20 | // more comments here` 21 | ], 22 | invalid: [ 23 | { 24 | code: `// ==UserScript== 25 | // @name abc 26 | // ==/UserScript==`, 27 | errors: [{ messageId: 'missingAttribute' }] 28 | }, 29 | { 30 | code: `// ==UserScript== 31 | // @name abc 32 | // ==/UserScript==`, 33 | options: ['required'], 34 | errors: [{ messageId: 'missingAttribute' }] 35 | }, 36 | { 37 | code: `// ==UserScript== 38 | // @description This is my description 39 | // @description This is my second description 40 | // ==/UserScript== 41 | // more comments here`, 42 | errors: [{ messageId: 'multipleDescriptions' }] 43 | }, 44 | { 45 | code: `// ==UserScript== 46 | // @description This is my description 47 | // @description:en This is my second description in English 48 | // @description:en This is my third description in English 49 | // ==/UserScript== 50 | // more comments here`, 51 | errors: [{ messageId: 'multipleDescriptions' }] 52 | } 53 | ] 54 | }); 55 | -------------------------------------------------------------------------------- /docs/rules/require-version.md: -------------------------------------------------------------------------------- 1 | # `require-version` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | The `require-version` rule verifies that a valid version attribute is present. 7 | 8 | ## Why? 9 | 10 | To prevent errors and keeping track of changes and ensuring updates get pushed. 11 | 12 | ## Options 13 | 14 | This rule has a string option: 15 | 16 | - `"required"` (default) requires that the version attribute is present 17 | - `"optional"` makes the version attribute optional 18 | 19 | ## Examples 20 | 21 | ## `"required"` 22 | 23 | 👍 Examples of **correct** code for this rule 24 | 25 | ```js 26 | /* eslint userscripts/require-version: "error" */ 27 | 28 | // ==UserScript== 29 | // @name Deletes the X Button 30 | // @description Some info on my userscript 31 | // @version 2.0.0 32 | // ==/UserScript== 33 | ``` 34 | 35 | 👎︎ Examples of **incorrect** code for this rule 36 | 37 | ```js 38 | /* eslint userscripts/require-version: "error" */ 39 | 40 | // ==UserScript== 41 | // @name Deletes the X Button 42 | // @description Some info on my userscript 43 | // @version 0.0 44 | // ==/UserScript== 45 | ``` 46 | 47 | ## `"optional"` 48 | 49 | 👍 Examples of **correct** code for this rule 50 | 51 | ```js 52 | /* eslint userscripts/require-version: ["error", "optional"] */ 53 | 54 | // ==UserScript== 55 | // @description Some info on my userscript 56 | // ==/UserScript== 57 | ``` 58 | 59 | 👎︎ Examples of **incorrect** code for this rule 60 | 61 | ```js 62 | /* eslint userscripts/require-version: "error" */ 63 | 64 | // ==UserScript== 65 | // @name Deletes the X Button 66 | // @description Some info on my userscript 67 | // @version 0.0 68 | // ==/UserScript== 69 | ``` 70 | 71 | ## When Not to Use It 72 | 73 | Don't use this rule when you are not using versions and pushing to production. 74 | It is recommended that you enable this rule with the `"optional"` option in that 75 | case. 76 | -------------------------------------------------------------------------------- /tests/lib/rules/align-attributes.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['align-attributes']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('align-attributes', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // ==/UserScript==`, 9 | `// ==UserScript== 10 | // @name hello 11 | // ==/UserScript==`, 12 | `// ==UserScript== 13 | // @name hello 14 | // @description hello also 15 | // ==/UserScript==`, 16 | { 17 | code: `// ==UserScript== 18 | // @name hello 19 | // @description hello again 20 | // ==/UserScript==`, 21 | options: [3] 22 | } 23 | ], 24 | invalid: [ 25 | { 26 | code: `// ==UserScript== 27 | // @name hello 28 | // @description hello again 29 | // ==/UserScript==`, 30 | output: `// ==UserScript== 31 | // @name hello 32 | // @description hello again 33 | // ==/UserScript==`, 34 | errors: [ 35 | { 36 | messageId: 'spaceMetadata' 37 | } 38 | ] 39 | }, 40 | { 41 | code: `// ==UserScript== 42 | // @name some name 43 | // @description hey there 44 | `, 45 | output: `// ==UserScript== 46 | // @name some name 47 | // @description hey there 48 | `, 49 | options: [5], 50 | errors: [ 51 | { 52 | messageId: 'spaceMetadata' 53 | } 54 | ] 55 | }, 56 | { 57 | code: `// ==UserScript== 58 | // @name some name 59 | // @description hey there 60 | // ==/UserScript== 61 | // stuff 62 | `, 63 | output: `// ==UserScript== 64 | // @name some name 65 | // @description hey there 66 | // ==/UserScript== 67 | // stuff 68 | `, 69 | options: [5], 70 | errors: [ 71 | { 72 | messageId: 'spaceMetadata' 73 | } 74 | ] 75 | } 76 | ] 77 | }); 78 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | eslint-plugin-userscripts 6 | 7 | 8 | 9 | 13 | 14 | 15 | 28 |
29 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/lib/rules/no-invalid-grant.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['no-invalid-grant']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('no-invalid-grant', rule, { 6 | valid: [ 7 | // "@grant" should not be detected if it's part of another header 8 | `// ==UserScript== 9 | // @name Test if @grant GM_doesNotExist works 10 | // @grant GM_info 11 | // @grant GM.info 12 | // @grant GM_getValue 13 | // @grant GM.getValue 14 | // @grant GM_getResourceURL 15 | // @grant GM.getResourceUrl 16 | // @grant GM_xmlhttpRequest 17 | // @grant GM.xmlHttpRequest 18 | // @grant GM_download 19 | // @grant GM.download 20 | // @grant GM_cookie 21 | // @grant GM.cookie 22 | // @grant GM.getValues 23 | // @grant GM.setValues 24 | // @grant unsafeWindow 25 | // @grant window.onurlchange 26 | // ==/UserScript== 27 | /* globals globalObj */` 28 | ], 29 | invalid: [ 30 | { 31 | // one of the @grant values doesn't exist 32 | code: `// ==UserScript== 33 | // @grant GM_doesNotExist 34 | // @grant GM.doesNotExist 35 | // @grant GM_notification 36 | // ==/UserScript==`, 37 | errors: [ 38 | { 39 | messageId: 'grantHasInvalidArgument', 40 | data: { argument: 'GM_doesNotExist' } 41 | }, 42 | { 43 | messageId: 'grantHasInvalidArgument', 44 | data: { argument: 'GM.doesNotExist' } 45 | } 46 | ] 47 | }, 48 | { 49 | code: `// ==UserScript== 50 | // @grant GM_notification text 51 | // ==/UserScript==`, 52 | errors: [ 53 | { 54 | messageId: 'grantHasInvalidArgument', 55 | data: { argument: 'GM_notification text' } 56 | } 57 | ] 58 | }, 59 | { 60 | code: `// ==UserScript== 61 | // @grant GM.getResourceURL 62 | // ==/UserScript==`, 63 | errors: [ 64 | { 65 | messageId: 'grantHasInvalidArgument', 66 | data: { argument: 'GM.getResourceURL' } 67 | } 68 | ] 69 | } 70 | ] 71 | }); 72 | -------------------------------------------------------------------------------- /docs/rules/require-description.md: -------------------------------------------------------------------------------- 1 | # `require-description` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | The `require-description` rule verifies that the description attribute is present 7 | and there is no more than one of it. 8 | 9 | ## Why? 10 | 11 | To give a better description on the userscript and to make sure that there is not 12 | accidentally more than one. 13 | 14 | ## Options 15 | 16 | This rule has a string option: 17 | 18 | - `"required"` (default) requires that the description attribute is present 19 | - `"optional"` makes the description attribute optional 20 | 21 | ## Examples 22 | 23 | ## `"required"` 24 | 25 | 👍 Examples of **correct** code for this rule 26 | 27 | ```js 28 | /* eslint userscripts/require-description: "error" */ 29 | 30 | // ==UserScript== 31 | // @name Deletes the X Button 32 | // @description Some info on my userscript 33 | // ==/UserScript== 34 | ``` 35 | 36 | 👎︎ Examples of **incorrect** code for this rule 37 | 38 | ```js 39 | /* eslint userscripts/require-description: "error" */ 40 | 41 | // ==UserScript== 42 | // @name Deletes the X Button 43 | // ==/UserScript== 44 | ``` 45 | 46 | ```js 47 | /* eslint userscripts/require-description: "error" */ 48 | 49 | // ==UserScript== 50 | // @name Deletes the X Button 51 | // @description Some info on my userscript 52 | // @description And more stuff 53 | // ==/UserScript== 54 | ``` 55 | 56 | ## `"optional"` 57 | 58 | 👍 Examples of **correct** code for this rule 59 | 60 | ```js 61 | /* eslint userscripts/require-description: ["error", "optional"] */ 62 | 63 | // ==UserScript== 64 | // @name Deletes the X Button 65 | // ==/UserScript== 66 | ``` 67 | 68 | 👎︎ Examples of **incorrect** code for this rule 69 | 70 | ```js 71 | /* eslint userscripts/require-description: ["error", "optional"] */ 72 | 73 | // ==UserScript== 74 | // @name Deletes the X Button 75 | // @description Some info on my userscript 76 | // @description And more stuff 77 | // ==/UserScript== 78 | ``` 79 | 80 | ## When Not to Use It 81 | 82 | This rule should apply to all userscripts which want to be descriptive about what 83 | they do. 84 | -------------------------------------------------------------------------------- /docs/rules/require-name.md: -------------------------------------------------------------------------------- 1 | # `require-name` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | 7 | 8 | > 🔧 The `--fix` option on the command line can automatically fix some of the 9 | > problems reported by this rule. 10 | 11 | The `require-name` rule verifies that the name attribute is present and there is 12 | no more than one of it. It also ensures that it is the first attribute. 13 | 14 | ## Why? 15 | 16 | To prevent errors and allow the user to understand what userscripts they have installed. 17 | 18 | ## Options 19 | 20 | This rule has a string option: 21 | 22 | - `"required"` (default) requires that the name attribute is present 23 | - `"optional"` makes the name attribute optional 24 | 25 | ## Examples 26 | 27 | ## `"required"` 28 | 29 | 👍 Examples of **correct** code for this rule 30 | 31 | ```js 32 | /* eslint userscripts/require-name: "error" */ 33 | 34 | // ==UserScript== 35 | // @name Deletes the X Button 36 | // @description Some info on my userscript 37 | // ==/UserScript== 38 | ``` 39 | 40 | 👎︎ Examples of **incorrect** code for this rule 41 | 42 | ```js 43 | /* eslint userscripts/require-name: "error" */ 44 | 45 | // ==UserScript== 46 | // @description Some info on my userscript 47 | // ==/UserScript== 48 | ``` 49 | 50 | ```js 51 | /* eslint userscripts/require-name: "error" */ 52 | 53 | // ==UserScript== 54 | // @description Some info on my userscript 55 | // @name Deletes the X Button 56 | // @name Deletes the X Button 2 57 | // ==/UserScript== 58 | ``` 59 | 60 | ## `"optional"` 61 | 62 | 👍 Examples of **correct** code for this rule 63 | 64 | ```js 65 | /* eslint userscripts/require-name: ["error", "optional"] */ 66 | 67 | // ==UserScript== 68 | // @description Some info on my userscript 69 | // ==/UserScript== 70 | ``` 71 | 72 | 👎︎ Examples of **incorrect** code for this rule 73 | 74 | ```js 75 | /* eslint userscripts/require-name: ["error", "optional"] */ 76 | 77 | // ==UserScript== 78 | // @description Some info on my userscript 79 | // @name Deletes the X Button 80 | // ==/UserScript== 81 | ``` 82 | 83 | ## When Not to Use It 84 | 85 | This rule should apply to all userscripts. 86 | -------------------------------------------------------------------------------- /tests/lib/rules/require-version.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['require-version']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('require-version', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // @version Alpha-v1 9 | // ==/UserScript==`, 10 | `// ==UserScript== 11 | // @version 0 12 | // ==/UserScript==`, 13 | `// ==UserScript== 14 | // @version 0.0.0 15 | // ==/UserScript==`, 16 | `// ==UserScript== 17 | // @version 000.0.1 18 | // ==/UserScript==`, 19 | `// ==UserScript== 20 | // @version 0.6pre4 21 | // ==/UserScript==`, 22 | `// ==UserScript== 23 | // @version 1.00 24 | // ==/UserScript==`, 25 | `// ==UserScript== 26 | // @version 1.0.0 27 | // ==/UserScript==`, 28 | `// ==UserScript== 29 | // @version 1.-1 30 | // ==/UserScript==`, 31 | `// ==UserScript== 32 | // @version 1.1a 33 | // ==/UserScript==`, 34 | `// ==UserScript== 35 | // @version 1.1.1.1.2.0.1.1.1.1.1 36 | // ==/UserScript==`, 37 | `// ==UserScript== 38 | // @version @.€.$ 39 | // ==/UserScript==`, 40 | ], 41 | invalid: [ 42 | { 43 | code: `// ==UserScript== 44 | // @description abc 45 | // ==/UserScript==`, 46 | errors: [{ messageId: 'missingAttribute' }] 47 | }, 48 | { 49 | code: `// ==UserScript== 50 | // @description abc 51 | // ==/UserScript==`, 52 | options: ['required'], 53 | errors: [{ messageId: 'missingAttribute' }] 54 | }, 55 | { 56 | code: `// ==UserScript== 57 | // @version 2.4.5 58 | // @version 2.4.5 59 | // ==/UserScript==`, 60 | errors: [{ messageId: 'multipleVersions' }] 61 | }, 62 | { 63 | code: `// ==UserScript== 64 | // @version .5.6 65 | // ==/UserScript==`, 66 | errors: [{ messageId: 'invalidVersion' }] 67 | }, 68 | { 69 | code: `// ==UserScript== 70 | // @version 5.6. 71 | // ==/UserScript==`, 72 | errors: [{ messageId: 'invalidVersion' }] 73 | }, 74 | { 75 | code: `// ==UserScript== 76 | // @version 5 .6 77 | // ==/UserScript==`, 78 | errors: [{ messageId: 'invalidVersion' }] 79 | } 80 | ] 81 | }); 82 | -------------------------------------------------------------------------------- /tests/lib/rules/require-attribute-space-prefix.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['require-attribute-space-prefix']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('require-attribute-space-prefix', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // @name Bottom Padding to Swagger UI 9 | // @namespace https://github.com/Yash-Singh1/UserScripts 10 | // @version 1.3 11 | // @description Adds bottom padding to the Swagger UI 12 | // 13 | // @author Yash Singh 14 | // @match https://*/* 15 | // @match http://*/* 16 | // @icon https://petstore.swagger.io/favicon-32x32.png 17 | // @grant none 18 | // 19 | // @homepage https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme 20 | // @homepageURL https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme 21 | // @supportURL https://github.com/Yash-Singh1/UserScripts/issues 22 | // @downloadURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js 23 | // @updateURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js 24 | // ==/UserScript==` 25 | ], 26 | invalid: [ 27 | { 28 | code: `// ==UserScript== 29 | // @name hello 30 | //@description invalid description 31 | // ==/UserScript== 32 | // more comments`, 33 | errors: [ 34 | { 35 | messageId: 'attributeNotPrefixedBySpace' 36 | } 37 | ] 38 | }, 39 | { 40 | code: `// ==UserScript== 41 | //@name hello 42 | //@description invalid description 43 | // ==/UserScript== 44 | // another comment`, 45 | errors: [ 46 | { 47 | messageId: 'attributeNotPrefixedBySpace' 48 | }, 49 | { 50 | messageId: 'attributeNotPrefixedBySpace' 51 | } 52 | ] 53 | }, 54 | { 55 | code: `// ==UserScript== 56 | // @name hello 57 | // @description invalid description 58 | // ==/UserScript==`, 59 | errors: [ 60 | { 61 | messageId: 'attributeNotPrefixedBySpace' 62 | } 63 | ] 64 | } 65 | ] 66 | }); 67 | -------------------------------------------------------------------------------- /lib/rules/no-invalid-grant.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../utils/createValidator'); 2 | 3 | // Documentation: 4 | // - Tampermonkey: https://www.tampermonkey.net/documentation.php#_grant 5 | // - Violentmonkey: https://violentmonkey.github.io/api/gm 6 | // - Greasemonkey: https://wiki.greasespot.net/Greasemonkey_Manual:API 7 | const gmFunctions = [ 8 | 'addElement', 9 | 'addStyle', 10 | 'addValueChangeListener', 11 | 'cookie', 12 | 'deleteValue', 13 | 'deleteValues', 14 | 'download', 15 | 'getResourceText', 16 | 'getResourceURL', 17 | 'getTab', 18 | 'getTabs', 19 | 'getValue', 20 | 'getValues', 21 | 'info', 22 | 'listValues', 23 | 'log', 24 | 'notification', 25 | 'openInTab', 26 | 'registerMenuCommand', 27 | 'removeValueChangeListener', 28 | 'saveTab', 29 | 'setClipboard', 30 | 'setValue', 31 | 'setValues', 32 | 'unregisterMenuCommand', 33 | 'webRequest', 34 | 'xmlhttpRequest' 35 | ].map((item) => `GM_${item}`); 36 | const greasemonkey = [ 37 | 'addElement', 38 | 'addStyle', 39 | 'addValueChangeListener', 40 | 'cookie', 41 | 'deleteValue', 42 | 'deleteValues', 43 | 'download', 44 | 'getResourceText', 45 | 'getResourceUrl', // note lowercase "rl" 46 | 'getTab', 47 | 'getTabs', 48 | 'getValue', 49 | 'getValues', 50 | 'info', 51 | 'listValues', 52 | 'log', 53 | 'notification', 54 | 'openInTab', 55 | 'registerMenuCommand', 56 | 'removeValueChangeListener', 57 | 'saveTab', 58 | 'setClipboard', 59 | 'setValue', 60 | 'setValues', 61 | 'unregisterMenuCommand', 62 | 'webRequest', 63 | 'xmlHttpRequest' // note uppercase "H" 64 | ].map((item) => `GM.${item}`); 65 | const miscellaneous = [ 66 | 'none', 67 | 'unsafeWindow', 68 | 'window.close', 69 | 'window.focus', 70 | 'window.onurlchange' 71 | ]; 72 | 73 | const acceptable = new Set([...gmFunctions, ...greasemonkey, ...miscellaneous]); 74 | 75 | module.exports = createValidator( 76 | 'grant', 77 | false, 78 | ({ attrVal, context }) => { 79 | const argument = attrVal.val; 80 | 81 | if (!acceptable.has(argument)) { 82 | context.report({ 83 | loc: { 84 | start: { 85 | line: attrVal.loc.start.line, 86 | column: 0 87 | }, 88 | end: attrVal.loc.end 89 | }, 90 | messageId: 'grantHasInvalidArgument', 91 | data: { argument } 92 | }); 93 | } 94 | }, 95 | { 96 | grantHasInvalidArgument: "'{{ argument }}' is not a valid @grant argument" 97 | } 98 | ); 99 | -------------------------------------------------------------------------------- /docs/rules/no-invalid-headers.md: -------------------------------------------------------------------------------- 1 | # `no-invalid-headers` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | The `no-invalid-headers` rule verifies that all the userscript headers are valid. 7 | 8 | ## Why? 9 | 10 | So as to avoid typos in the userscript headers which might have unintended consequences. 11 | 12 | ## Options 13 | 14 | This rule has an object option: 15 | 16 | - `"allowed"`: an array of headers to whitelist 17 | 18 | ## Examples 19 | 20 | ### `allowed`: `[]` 21 | 22 | 👍 Examples of **correct** code for this rule 23 | 24 | ```js 25 | /* eslint userscripts/no-invalid-headers: ["error", { allowed: [] }] */ 26 | 27 | // ==UserScript== 28 | // @name Bottom Padding to Swagger UI 29 | // @namespace https://github.com/Yash-Singh1/UserScripts 30 | // @version 1.3 31 | // @description My description 32 | // @description:en My description internationalized 33 | // @author John Doe 34 | // @match https://*/* 35 | // @match http://*/* 36 | // @grant none 37 | // @nocompat Chrome 38 | // ==/UserScript== 39 | ``` 40 | 41 | 👎︎ Examples of **incorrect** code for this rule 42 | 43 | ```js 44 | /* eslint userscripts/no-invalid-headers: ["error", { allowed: [] }] */ 45 | 46 | // ==UserScript== 47 | // @naem MyName 48 | // @description: My description 49 | // @supportUrl https://example.com 50 | // ==/UserScript== 51 | ``` 52 | 53 | ### `allowed`: `[ "whitelisted" ]` 54 | 55 | 👍 Examples of **correct** code for this rule 56 | 57 | ```js 58 | /* eslint userscripts/no-invalid-headers: ["error", { allowed: [ "whitelisted" ] }] */ 59 | 60 | // ==UserScript== 61 | // @name Bottom Padding to Swagger UI 62 | // @namespace https://github.com/Yash-Singh1/UserScripts 63 | // @version 1.3 64 | // @description My description 65 | // @description:en My description internationalized 66 | // @author John Doe 67 | // @match https://*/* 68 | // @match http://*/* 69 | // @grant none 70 | // @nocompat Chrome 71 | // @whitelisted Custom header 72 | // ==/UserScript== 73 | ``` 74 | 75 | 👎︎ Examples of **incorrect** code for this rule 76 | 77 | ```js 78 | /* eslint userscripts/no-invalid-headers: ["error", { allowed: [ "whitelisted" ] }] */ 79 | 80 | // ==UserScript== 81 | // @name MyName 82 | // @description My description 83 | // @whitelisted whitelisted value 84 | // @notwhitelisted this header is not whitelisted 85 | // ==/UserScript== 86 | ``` 87 | 88 | ## When Not to Use It 89 | 90 | Turn off this rule if you don't want to check the validity of the userscript headers. 91 | -------------------------------------------------------------------------------- /lib/rules/no-invalid-headers.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../utils/createValidator'); 2 | 3 | // Documentation: 4 | // - Tampermonkey: https://www.tampermonkey.net/documentation.php 5 | // - Violentmonkey: https://violentmonkey.github.io/api/metadata-block/ 6 | // - Greasemonkey: https://wiki.greasespot.net/Metadata_Block 7 | const validHeaders = new Set( 8 | [ 9 | 'antifeature', 10 | 'author', 11 | 'connect', 12 | 'contributor', 13 | 'contributors', 14 | 'copyright', 15 | 'defaulticon', 16 | 'description', 17 | 'developer', 18 | 'downloadURL', 19 | 'exclude', 20 | 'grant', 21 | 'history', 22 | 'homepage', 23 | 'homepageURL', 24 | 'icon', 25 | 'icon64', 26 | 'icon64URL', 27 | 'iconURL', 28 | 'include', 29 | 'license', 30 | 'match', 31 | 'name', 32 | 'namespace', 33 | 'nocompat', 34 | 'noframes', 35 | 'require', 36 | 'resource', 37 | 'run-at', 38 | 'run-in', 39 | 'sandbox', 40 | 'source', 41 | 'supportURL', 42 | 'tag', 43 | 'unwrap', 44 | 'updateURL', 45 | 'version', 46 | 'website' 47 | ].map((header) => `@${header}`) 48 | ); 49 | const internationalized = ['name', 'description', 'antifeature'].map( 50 | (item) => new RegExp(`^@${item}(:\\S+)?$`) 51 | ); 52 | 53 | module.exports = createValidator( 54 | 'headers', 55 | false, 56 | ({ attrVal, context }) => { 57 | const o = context.options[0]; 58 | const optionsHeaders = new Set( 59 | (o && o.allowed || []).map((header) => `@${header}`) 60 | ); 61 | 62 | for (const value of attrVal) { 63 | const headerName = `@${value.key}`; 64 | const isValid = 65 | validHeaders.has(headerName) || 66 | optionsHeaders.has(headerName) || 67 | // use regex for internationalised headers 68 | internationalized.some((regex) => regex.test(headerName)); 69 | 70 | if (!isValid) { 71 | context.report({ 72 | loc: { 73 | start: { 74 | line: value.loc.start.line, 75 | column: 0 76 | }, 77 | end: value.loc.end 78 | }, 79 | messageId: 'invalidHeader', 80 | data: { 81 | header: headerName 82 | } 83 | }); 84 | } 85 | } 86 | }, 87 | { 88 | invalidHeader: "'{{ header }}' is not a valid userscript header" 89 | }, 90 | false, 91 | /./, // match every header 92 | true, 93 | [ 94 | { 95 | type: 'object', 96 | properties: { 97 | allowed: { 98 | type: 'array' 99 | } 100 | }, 101 | additionalProperties: false 102 | } 103 | ] 104 | ); 105 | -------------------------------------------------------------------------------- /tests/lib/rules/no-invalid-headers.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['no-invalid-headers']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('no-invalid-headers', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // @name Bottom Padding to Swagger UI 9 | // @namespace https://github.com/Yash-Singh1/UserScripts 10 | // @version 1.3 11 | // @description Adds bottom padding to the Swagger UI 12 | // @description:en Adds bottom padding to the Swagger UI 13 | // @author Yash Singh 14 | // @match https://*/* 15 | // @match http://*/* 16 | // @icon https://petstore.swagger.io/favicon-32x32.png 17 | // @grant none 18 | // @license MIT 19 | // @homepage https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme 20 | // @homepageURL https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme 21 | // @supportURL https://github.com/Yash-Singh1/UserScripts/issues 22 | // @downloadURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js 23 | // @updateURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js 24 | // @nocompat Chrome 25 | // @run-in normal-tabs 26 | // @run-in container-id-1 27 | // @history 1.0 Initial release 28 | // @copyright 2020-2021, Yash Singh (https://github.com/Yash-Singh1) 29 | // ==/UserScript== 30 | /* globals globalObj */` 31 | ], 32 | invalid: [ 33 | { 34 | code: `// ==UserScript== 35 | // @naem MyName 36 | // @description: My description 37 | // @supportUrl https://example.com 38 | // ==/UserScript==`, 39 | errors: [ 40 | { 41 | messageId: 'invalidHeader', 42 | data: { header: '@naem' } 43 | }, 44 | { 45 | messageId: 'invalidHeader', 46 | data: { header: '@description:' } 47 | }, 48 | { 49 | messageId: 'invalidHeader', 50 | data: { header: '@supportUrl' } 51 | } 52 | ] 53 | }, 54 | { 55 | code: `// ==UserScript== 56 | // @name MyName 57 | // @description My description 58 | // @whitelisted whitelisted value 59 | // @notwhitelisted this header is not whitelisted 60 | // ==/UserScript==`, 61 | options: [{ allowed: ['whitelisted'] }], 62 | errors: [ 63 | { 64 | messageId: 'invalidHeader', 65 | data: { header: '@notwhitelisted' } 66 | } 67 | ] 68 | } 69 | ] 70 | }); 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `eslint-plugin-userscripts` 2 | 3 | [![codecov](https://codecov.io/gh/Yash-Singh1/eslint-plugin-userscripts/branch/main/graph/badge.svg?token=JD8GRJH9D4)](https://codecov.io/gh/Yash-Singh1/eslint-plugin-userscripts) 4 | 5 | Implements rules for userscripts in `eslint`. 6 | 7 | ## Installation 8 | 9 | You'll first need to install [ESLint](http://eslint.org): 10 | 11 | ```sh 12 | npm install eslint --save-dev 13 | ``` 14 | 15 | Next, install `eslint-plugin-userscripts`: 16 | 17 | ```sh 18 | npm install eslint-plugin-userscripts --save-dev 19 | ``` 20 | 21 | ## Usage 22 | 23 | Add `userscripts` to the plugins section of your `.eslintrc` configuration file: 24 | 25 | ```json 26 | { 27 | "extends": ["plugin:userscripts/recommended"] 28 | } 29 | ``` 30 | 31 | ## Supported Rules 32 | 33 | | Rule | Description | Recommended | 34 | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | :---------: | 35 | | [`filename-user`](docs/rules/filename-user.md) | Ensures userscripts end with .user.js | ✅ | 36 | | [`no-invalid-grant`](docs/rules/no-invalid-grant.md) | Ensures the argument passed to `@grant` is valid | ✅ | 37 | | [`no-invalid-metadata`](docs/rules/no-invalid-metadata.md) | Ensures userscripts have valid metadata | ✅ | 38 | | [`require-name`](docs/rules/require-name.md) | Ensures userscripts have a name | ✅ | 39 | | [`require-description`](docs/rules/require-description.md) | Ensures userscripts have a description | ✅ | 40 | | [`require-version`](docs/rules/require-version.md) | Ensures userscripts have a valid version | ✅ | 41 | | [`use-homepage-and-url`](docs/rules/use-homepage-and-url.md) | Ensures that for each `homepage` attribute, `homepageURL` is also used | ✅ | 42 | | [`use-download-and-update-url`](docs/rules/use-download-and-update-url.md) | Ensures that for each `downloadURL` there is a `updateURL` | ✅ | 43 | | [`align-attributes`](docs/rules/align-attributes.md) | Ensures that attributes are spaced out and aligned | ✅ | 44 | | [`require-attribute-space-prefix`](docs/rules/require-attribute-space-prefix.md) | Ensure that attributes are prefixed by one space | ✅ | 45 | | [`no-invalid-headers`](docs/rules/no-invalid-headers.md) | Ensures userscripts have valid headers | ✅ | 46 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # `eslint-plugin-userscripts` 2 | 3 | [![codecov](https://codecov.io/gh/Yash-Singh1/eslint-plugin-userscripts/branch/main/graph/badge.svg?token=JD8GRJH9D4)](https://codecov.io/gh/Yash-Singh1/eslint-plugin-userscripts) 4 | 5 | Implements rules for userscripts in `eslint`. 6 | 7 | ## Installation 8 | 9 | You'll first need to install [ESLint](http://eslint.org): 10 | 11 | ```sh 12 | npm install eslint --save-dev 13 | ``` 14 | 15 | Next, install `eslint-plugin-userscripts`: 16 | 17 | ```sh 18 | npm install eslint-plugin-userscripts --save-dev 19 | ``` 20 | 21 | ## Usage 22 | 23 | Add `userscripts` to the plugins section of your `.eslintrc` configuration file: 24 | 25 | ```json 26 | { 27 | "extends": ["plugin:userscripts/recommended"] 28 | } 29 | ``` 30 | 31 | ## Supported Rules 32 | 33 | | Rule | Description | Recommended | 34 | | -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | :---------: | 35 | | [`filename-user`](docs/rules/filename-user.md) | Ensures userscripts end with .user.js | ✅ | 36 | | [`no-invalid-grant`](docs/rules/no-invalid-grant.md) | Ensures the argument passed to `@grant` is valid | ✅ | 37 | | [`no-invalid-metadata`](docs/rules/no-invalid-metadata.md) | Ensures userscripts have valid metadata | ✅ | 38 | | [`require-name`](docs/rules/require-name.md) | Ensures userscripts have a name | ✅ | 39 | | [`require-description`](docs/rules/require-description.md) | Ensures userscripts have a description | ✅ | 40 | | [`require-version`](docs/rules/require-version.md) | Ensures userscripts have a valid version | ✅ | 41 | | [`use-homepage-and-url`](docs/rules/use-homepage-and-url.md) | Ensures that for each `homepage` attribute, `homepageURL` is also used | ✅ | 42 | | [`use-download-and-update-url`](docs/rules/use-download-and-update-url.md) | Ensures that for each `downloadURL` there is a `updateURL` | ✅ | 43 | | [`align-attributes`](docs/rules/align-attributes.md) | Ensures that attributes are spaced out and aligned | ✅ | 44 | | [`require-attribute-space-prefix`](docs/rules/require-attribute-space-prefix.md) | Ensure that attributes are prefixed by one space | ✅ | 45 | | [`no-invalid-headers`](docs/rules/no-invalid-headers.md) | Ensures userscripts have valid headers | ✅ | 46 | -------------------------------------------------------------------------------- /docs/rules/no-invalid-metadata.md: -------------------------------------------------------------------------------- 1 | # `no-invalid-metadata` 2 | 3 | > ✅ The "extends": "plugin:userscripts/recommended" property in a configuration 4 | > file enables this rule. 5 | 6 | The `no-invalid-metadata` rule verifies that the userscript metadata for the file 7 | is valid. 8 | 9 | ## Why? 10 | 11 | So errors don't come and the metadata is provided for ease of handling userscripts 12 | and users in production. 13 | 14 | ## Options 15 | 16 | This rule has an object option: 17 | 18 | - `"top"` (default: `"required"`) requires that the metadata be on the top of the 19 | file 20 | 21 | ## Examples 22 | 23 | ### `top`: `"required"` 24 | 25 | 👍 Examples of **correct** code for this rule 26 | 27 | ```js 28 | /* eslint userscripts/no-invalid-metadata: ["error", { top: "required" }] */ 29 | 30 | // ==UserScript== 31 | // ==/UserScript== 32 | ``` 33 | 34 | ```js 35 | /* eslint userscripts/no-invalid-metadata: ["error", { top: "required" }] */ 36 | 37 | // ==UserScript== 38 | // @name My Userscript's Name 39 | // @description Description on my userscript 40 | // 41 | // @version 1.0.0 42 | // @license ISC 43 | // 44 | // @grant none 45 | // ==/UserScript== 46 | ``` 47 | 48 | 👎︎ Examples of **incorrect** code for this rule 49 | 50 | ```js 51 | /* eslint userscripts/no-invalid-metadata: ["error", { top: "required" }] */ 52 | 53 | console.log('starting userscript'); 54 | 55 | // ==UserScript== 56 | // ==/UserScript== 57 | ``` 58 | 59 | ```js 60 | /* eslint userscripts/no-invalid-metadata: ["error", { top: "required" }] */ 61 | 62 | // ==UserScript== 63 | // @name My Userscript's Name 64 | // description Description on my userscript 65 | // @license ISC 66 | // ==/UserScript== 67 | ``` 68 | 69 | ```js 70 | /* eslint userscripts/no-invalid-metadata: ["error", { top: "required" }] */ 71 | 72 | // ==UserScript== 73 | // @name My Userscript's Name 74 | // @description Description on my userscript 75 | // @license ISC 76 | ``` 77 | 78 | ```js 79 | /* eslint userscripts/no-invalid-metadata: ["error", { top: "required" }] */ 80 | 81 | // ==UserScript== 82 | // @name My Userscript's Name 83 | console.log('some code in between'); 84 | // @description Description on my userscript 85 | // @license ISC 86 | // ==/UserScript== 87 | ``` 88 | 89 | ### `top`: `"optional"` 90 | 91 | 👍 Examples of **correct** code for this rule 92 | 93 | ```js 94 | /* eslint userscripts/no-invalid-metadata: ["error", { top: "required" }] */ 95 | 96 | console.log('starting userscript'); 97 | 98 | // ==UserScript== 99 | // ==/UserScript== 100 | ``` 101 | 102 | 👎︎ Examples of **incorrect** code for this rule 103 | 104 | ```js 105 | /* eslint userscripts/no-invalid-metadata: ["error", { top: "required" }] */ 106 | 107 | // ==UserScript== 108 | // @name My Userscript's Name 109 | // @description Description on my userscript 110 | // @license ISC 111 | ``` 112 | 113 | ## When Not to Use It 114 | 115 | Turn off this rule when you are not linting userscripts or know that any of the 116 | above conditions won't cause a problem on your end. 117 | -------------------------------------------------------------------------------- /lib/rules/align-attributes.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | meta: { 3 | type: 'suggestion', 4 | docs: { 5 | description: 'aligns attributes in the metadata', 6 | category: 'Stylistic Issues' 7 | }, 8 | schema: [ 9 | { 10 | type: 'integer', 11 | minimum: 1, 12 | default: 2 13 | } 14 | ], 15 | messages: { 16 | spaceMetadata: 'The metadata is not spaced' 17 | }, 18 | fixable: 'code' 19 | }, 20 | create: (context) => { 21 | const spacing = context.options[0] || 2; 22 | 23 | const sourceCode = context.getSourceCode(); 24 | const comments = sourceCode.getAllComments(); 25 | 26 | let inMetadata = false; 27 | let done = false; 28 | let metadata = []; 29 | let start = {}; 30 | let end = {}; 31 | for (const comment of comments.filter( 32 | (comment) => comment.type === 'Line' 33 | )) { 34 | if (done) { 35 | continue; 36 | } 37 | 38 | // istanbul ignore else 39 | if (inMetadata && comment.value.trim() === '==/UserScript==') { 40 | end = comment.loc.end; 41 | done = true; 42 | } else if (!inMetadata && comment.value.trim() === '==UserScript==') { 43 | start = comment.loc.start; 44 | inMetadata = true; 45 | } else if (inMetadata && comment.value.trim().startsWith('@')) { 46 | metadata.push({ 47 | key: comment.value.trim().slice(1).split(/\s/)[0], 48 | space: /^\S*(\s+)/.exec(comment.value.trim().slice(1))[1].length, 49 | line: comment.loc.start.line, 50 | comment 51 | }); 52 | } 53 | } 54 | 55 | if (Object.keys(end).length === 0) { 56 | end = sourceCode.getLocFromIndex(sourceCode.getText().length); 57 | } 58 | 59 | if (metadata.length === 0) { 60 | return {}; 61 | } 62 | 63 | const totalSpacing = 64 | Math.max(...metadata.map((metadatapoint) => metadatapoint.key.length)) + 65 | spacing; 66 | 67 | if ( 68 | metadata.map((metadatapoint) => metadatapoint.space).sort()[0] < 69 | spacing || 70 | metadata 71 | .map((metadatapoint) => metadatapoint.key.length + metadatapoint.space) 72 | .find((val) => val !== totalSpacing) 73 | ) { 74 | context.report({ 75 | loc: { 76 | start, 77 | end 78 | }, 79 | messageId: 'spaceMetadata', 80 | fix: function (fixer) { 81 | const fixerRules = []; 82 | for (const metadatapoint of metadata) { 83 | // istanbul ignore else 84 | if ( 85 | metadatapoint.key.length + metadatapoint.space !== 86 | totalSpacing 87 | ) { 88 | const startColumn = /^(.*?@\S*)/.exec( 89 | sourceCode.getLines()[metadatapoint.line - 1] 90 | )[1].length; 91 | fixerRules.push( 92 | fixer.replaceTextRange( 93 | [ 94 | sourceCode.getIndexFromLoc({ 95 | line: metadatapoint.line, 96 | column: startColumn 97 | }), 98 | sourceCode.getIndexFromLoc({ 99 | line: metadatapoint.line, 100 | column: startColumn + metadatapoint.space 101 | }) 102 | ], 103 | ' '.repeat(totalSpacing - metadatapoint.key.length) 104 | ) 105 | ); 106 | } 107 | } 108 | return fixerRules; 109 | } 110 | }); 111 | } 112 | 113 | return {}; 114 | } 115 | }; 116 | -------------------------------------------------------------------------------- /tests/lib/rules/filename-user.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['filename-user']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('filename-user', rule, { 6 | valid: [ 7 | { 8 | filename: 'hello.user.js', 9 | code: 'foo()', 10 | options: ['always'] 11 | }, 12 | { 13 | filename: 'src/userscripts/more/dirs/down/something.user.js', 14 | code: 'var stuff = "hello"; console.log(stuff.slice(1))', 15 | options: ['always'] 16 | }, 17 | { 18 | filename: '/home/john/dir/theirfiledir/heythere.user.js', 19 | code: '', 20 | options: ['always'] 21 | }, 22 | { 23 | filename: 'src/userscripts/thing.js', 24 | code: 'somecodeimeanfunctiontorun()', 25 | options: ['never'] 26 | }, 27 | { 28 | filename: 'thing2.js', 29 | code: 'somecodeimeanfunctiontorun();//just filename', 30 | options: ['never'] 31 | }, 32 | { 33 | filename: '/home/person/thing3eee.js', 34 | code: 'var hey = 2', 35 | options: ['never'] 36 | }, 37 | { 38 | code: 'nofilename', 39 | options: ['always'] 40 | }, 41 | { 42 | filename: '', 43 | code: 'inputfilename', 44 | options: ['always'] 45 | }, 46 | { 47 | filename: '', 48 | code: 'textfilename', 49 | options: ['always'] 50 | } 51 | ], 52 | invalid: [ 53 | { 54 | filename: 'hello.js', 55 | code: 'foo()', 56 | options: ['always'], 57 | errors: [ 58 | { 59 | messageId: 'filenameExtension', 60 | data: { 61 | oldFilename: 'hello.js', 62 | newFilename: 'hello.user.js' 63 | } 64 | } 65 | ] 66 | }, 67 | { 68 | filename: 'src/userscripts/more/dirs/down/something.js', 69 | code: 'var stuff = "hello"; console.log(stuff.slice(1))', 70 | options: ['always'], 71 | errors: [ 72 | { 73 | messageId: 'filenameExtension', 74 | data: { 75 | oldFilename: 'src/userscripts/more/dirs/down/something.js', 76 | newFilename: 'src/userscripts/more/dirs/down/something.user.js' 77 | } 78 | } 79 | ] 80 | }, 81 | { 82 | filename: '/home/john/dir/theirfiledir/heythere.js', 83 | code: '', 84 | options: ['always'], 85 | errors: [ 86 | { 87 | messageId: 'filenameExtension', 88 | data: { 89 | oldFilename: '/home/john/dir/theirfiledir/heythere.js', 90 | newFilename: '/home/john/dir/theirfiledir/heythere.user.js' 91 | } 92 | } 93 | ] 94 | }, 95 | { 96 | filename: 'src/userscripts/thing.user.js', 97 | code: 'somecodeimeanfunctiontorun()', 98 | options: ['never'], 99 | errors: [ 100 | { 101 | messageId: 'filenameExtension', 102 | data: { 103 | oldFilename: 'src/userscripts/thing.user.js', 104 | newFilename: 'src/userscripts/thing.js' 105 | } 106 | } 107 | ] 108 | }, 109 | { 110 | filename: 'thing2.user.js', 111 | code: 'somecodeimeanfunctiontorun();//just filename', 112 | options: ['never'], 113 | errors: [ 114 | { 115 | messageId: 'filenameExtension', 116 | data: { 117 | oldFilename: 'thing2.user.js', 118 | newFilename: 'thing2.js' 119 | } 120 | } 121 | ] 122 | }, 123 | { 124 | filename: '/home/person/thing3eee.user.js', 125 | code: 'var hey = 2', 126 | options: ['never'], 127 | errors: [ 128 | { 129 | messageId: 'filenameExtension', 130 | data: { 131 | oldFilename: '/home/person/thing3eee.user.js', 132 | newFilename: '/home/person/thing3eee.js' 133 | } 134 | } 135 | ] 136 | } 137 | ] 138 | }); 139 | -------------------------------------------------------------------------------- /lib/rules/require-name.js: -------------------------------------------------------------------------------- 1 | const createValidator = require('../utils/createValidator'); 2 | 3 | const nameReg = /^name(:\S+)?$/; 4 | 5 | module.exports = createValidator( 6 | 'name', 7 | true, 8 | ({ attrVal, context, metadata }) => { 9 | let iteratedKeyNames = []; 10 | for (let attrValue of attrVal) { 11 | if (iteratedKeyNames.includes(attrValue.key)) { 12 | context.report({ 13 | loc: attrValue.loc, 14 | messageId: 'multipleNames' 15 | }); 16 | } else { 17 | iteratedKeyNames.push(attrValue.key); 18 | } 19 | } 20 | 21 | const metadataValues = Object.values(metadata); 22 | 23 | if ( 24 | metadataValues.find( 25 | (attrValue, attrValIndex) => 26 | attrValIndex !== 0 && 27 | nameReg.test(attrValue[0] ? attrValue[0].key : attrValue.key) && 28 | !nameReg.test( 29 | metadataValues[attrValIndex - 1][0] 30 | ? metadataValues[attrValIndex - 1][0].key 31 | : metadataValues[attrValIndex - 1].key 32 | ) 33 | ) 34 | ) { 35 | const sourceCode = context.getSourceCode(); 36 | const comments = sourceCode.getAllComments(); 37 | const endingMetadataComment = comments.find( 38 | (comment) => 39 | comment.value.trim() === '==/UserScript==' && comment.type === 'Line' 40 | ); 41 | context.report({ 42 | loc: { 43 | start: comments.find( 44 | (comment) => 45 | comment.value.trim() === '==UserScript==' && 46 | comment.type === 'Line' 47 | ).loc.start, 48 | end: endingMetadataComment 49 | ? endingMetadataComment.loc.end 50 | : { line: sourceCode.lines.length, column: 0 } 51 | }, 52 | messageId: 'nameAtBeginning', 53 | fix: function (fixer) { 54 | let fixerRules = []; 55 | for (let attrValue of attrVal) { 56 | // istanbul ignore else 57 | if (!Array.isArray(attrValue)) { 58 | attrValue = [attrValue]; 59 | } 60 | for (let deepAttrValue of attrValue) { 61 | fixerRules.push( 62 | fixer.removeRange( 63 | deepAttrValue.comment.range.map((val, index) => 64 | index === 0 65 | ? val - 66 | context 67 | .getSourceCode() 68 | .lines[deepAttrValue.loc.start.line - 1].split( 69 | '//' 70 | )[0].length - 71 | 1 72 | : val 73 | ) 74 | ) 75 | ); 76 | } 77 | } 78 | fixerRules.push( 79 | fixer.insertTextAfterRange( 80 | context 81 | .getSourceCode() 82 | .getAllComments() 83 | .find((val) => val.value.trim() === '==UserScript==').range, 84 | attrVal 85 | .sort((attrValue1, attrValue2) => 86 | attrValue1.key === 'name' 87 | ? -1 88 | : attrValue2.key === 'name' 89 | ? 1 90 | : 0 91 | ) 92 | .map( 93 | (attrValue) => 94 | `\n${ 95 | context 96 | .getSourceCode() 97 | .lines[attrValue.loc.start.line - 1].split('//')[0] 98 | }//${attrValue.comment.value}` 99 | ) 100 | .join('') 101 | ) 102 | ); 103 | return fixerRules; 104 | } 105 | }); 106 | } 107 | }, 108 | { 109 | multipleNames: 'Include only one name for each language', 110 | nameAtBeginning: 'The names should be at the beginning of the metadata' 111 | }, 112 | true, 113 | nameReg, 114 | true 115 | ); 116 | -------------------------------------------------------------------------------- /lib/rules/no-invalid-metadata.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | meta: { 3 | type: 'suggestion', 4 | docs: { 5 | description: 'ensure userscripts have valid metadata', 6 | category: 'Possible Errors' 7 | }, 8 | messages: { 9 | metadataRequired: 'Add metadata to the userscript', 10 | moveMetadataToTop: 'Move the metadata to the top of the file', 11 | noClosingMetadata: 'Closing metadata comment not found', 12 | noCodeBetween: 'Code found between in metadata', 13 | attributeNotStartsWithAtTheRate: 'Attributes should begin with @' 14 | }, 15 | schema: [ 16 | { 17 | type: 'object', 18 | properties: { 19 | top: { 20 | enum: ['required', 'optional'], 21 | default: 'required' 22 | } 23 | }, 24 | additionalProperties: false 25 | } 26 | ] 27 | }, 28 | create: (context) => { 29 | const sourceCode = context.getSourceCode(); 30 | 31 | const comments = sourceCode.getAllComments(); 32 | const lines = sourceCode.lines; 33 | 34 | let inMetadata = false; 35 | let done = false; 36 | for (const [index, line] of lines.entries()) { 37 | if (done) { 38 | continue; 39 | } 40 | 41 | const lineLoc = { 42 | start: { 43 | line: index + 1, 44 | column: 0 45 | }, 46 | end: { 47 | line: index + 1, 48 | column: line.length 49 | } 50 | }; 51 | 52 | if ( 53 | // https://github.com/Yash-Singh1/eslint-plugin-userscripts/issues/8 54 | inMetadata && 55 | !line.trim().startsWith('//') && // line is not a comment, 56 | line.trim() // and *actually* contains something other than spaces 57 | ) { 58 | context.report({ 59 | loc: lineLoc, 60 | messageId: 'noCodeBetween' 61 | }); 62 | } else if ( 63 | inMetadata && 64 | line.trim().startsWith('//') && 65 | line.trim().slice(2).trim() === '==/UserScript==' 66 | ) { 67 | done = true; 68 | } else if ( 69 | !inMetadata && 70 | line.trim().startsWith('//') && 71 | line.trim().slice(2).trim() === '==UserScript==' 72 | ) { 73 | inMetadata = true; 74 | } else if ( 75 | inMetadata && 76 | !line.trim().slice(2).trim().startsWith('@') && // not a header, 77 | line.trim().slice(2).trim() // nor an empty line, (see #8 above) 78 | ) { 79 | context.report({ 80 | loc: lineLoc, 81 | messageId: 'attributeNotStartsWithAtTheRate' 82 | }); 83 | } 84 | } 85 | 86 | return { 87 | Program(node) { 88 | if ( 89 | comments.length === 0 || 90 | !comments.find( 91 | (comment) => 92 | comment.value.trim() === '==UserScript==' && 93 | comment.type === 'Line' 94 | ) 95 | ) { 96 | context.report({ 97 | node, 98 | messageId: 'metadataRequired' 99 | }); 100 | return; 101 | } else if ( 102 | !comments.find( 103 | (comment) => 104 | comment.value.trim() === '==/UserScript==' && 105 | comment.type === 'Line' 106 | ) 107 | ) { 108 | context.report({ 109 | loc: comments.find( 110 | (comment) => 111 | comment.value.trim() === '==UserScript==' && 112 | comment.type === 'Line' 113 | ).loc, 114 | messageId: 'noClosingMetadata' 115 | }); 116 | } 117 | if ( 118 | (!context.options[0] || 119 | !context.options[0].top || 120 | context.options[0].top === 'required') && 121 | (comments[0].value.trim() !== '==UserScript==' || 122 | comments[0].loc.start.line !== 1) 123 | ) { 124 | context.report({ 125 | loc: comments.find( 126 | (comment) => 127 | comment.value.trim() === '==UserScript==' && 128 | comment.type === 'Line' 129 | ).loc, 130 | messageId: 'moveMetadataToTop' 131 | }); 132 | } 133 | } 134 | }; 135 | } 136 | }; 137 | -------------------------------------------------------------------------------- /tests/lib/rules/no-invalid-metadata.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['no-invalid-metadata']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('no-invalid-metadata', rule, { 6 | valid: [ 7 | // handle empty comment lines 8 | `// ==UserScript== 9 | // @name Bottom Padding to Swagger UI 10 | // @namespace https://github.com/Yash-Singh1/UserScripts 11 | // @version 1.3 12 | // @description Adds bottom padding to the Swagger UI 13 | // 14 | // @author Yash Singh 15 | // @match https://*/* 16 | // @match http://*/* 17 | // @icon https://petstore.swagger.io/favicon-32x32.png 18 | // @grant none 19 | // 20 | // @homepage https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme 21 | // @homepageURL https://github.com/Yash-Singh1/UserScripts/tree/main/Bottom_Padding_to_Swagger_UI#readme 22 | // @supportURL https://github.com/Yash-Singh1/UserScripts/issues 23 | // @downloadURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js 24 | // @updateURL https://raw.githubusercontent.com/Yash-Singh1/UserScripts/main/Bottom_Padding_to_Swagger_UI/Bottom_Padding_to_Swagger_UI.user.js 25 | // ==/UserScript==`, 26 | `// ==UserScript== 27 | // ==/UserScript==`, 28 | 29 | // handle empty lines 30 | `// ==UserScript== 31 | // @name My cool name 32 | 33 | // @version 1.0.0 34 | // ==/UserScript==`, 35 | { 36 | code: `/* global myGlobalVar */ 37 | // ==UserScript== 38 | // ==/UserScript==`, 39 | options: [{ top: 'optional' }] 40 | } 41 | ], 42 | invalid: [ 43 | { 44 | code: '', 45 | errors: [ 46 | { 47 | messageId: 'metadataRequired' 48 | } 49 | ] 50 | }, 51 | { 52 | code: 'abcd()', 53 | errors: [ 54 | { 55 | messageId: 'metadataRequired' 56 | } 57 | ] 58 | }, 59 | { 60 | code: '// ==UserScript==', 61 | errors: [ 62 | { 63 | messageId: 'noClosingMetadata' 64 | } 65 | ] 66 | }, 67 | { 68 | code: ` 69 | // ==UserScript== 70 | // ==/UserScript== 71 | // abc`, 72 | errors: [ 73 | { 74 | messageId: 'moveMetadataToTop' 75 | } 76 | ] 77 | }, 78 | { 79 | code: `/* global myGlobalVar */ 80 | // ==UserScript==`, 81 | errors: [ 82 | { 83 | messageId: 'noClosingMetadata' 84 | }, 85 | { 86 | messageId: 'moveMetadataToTop' 87 | } 88 | ] 89 | }, 90 | { 91 | code: `/* global myGlobalVar */ 92 | // ==UserScript== 93 | console.log("hello") 94 | // ==/UserScript==`, 95 | options: [{ top: 'optional' }], 96 | errors: [ 97 | { 98 | messageId: 'noCodeBetween' 99 | } 100 | ] 101 | }, 102 | { 103 | code: `// ==UserScript== 104 | console.log("hello") 105 | // ==/UserScript==`, 106 | errors: [ 107 | { 108 | messageId: 'noCodeBetween' 109 | } 110 | ] 111 | }, 112 | { 113 | code: `// ==UserScript== 114 | // @name hello 115 | // description invalid description 116 | // ==/UserScript==`, 117 | errors: [ 118 | { 119 | messageId: 'attributeNotStartsWithAtTheRate' 120 | } 121 | ] 122 | }, 123 | { 124 | code: `// ==UserScript== 125 | // @name foo 126 | //// 127 | // @description bar 128 | // ==/UserScript==`, 129 | errors: [ 130 | { 131 | messageId: 'attributeNotStartsWithAtTheRate' 132 | } 133 | ] 134 | }, 135 | { 136 | code: `// ==UserScript== 137 | // name hello 138 | // description invalid description 139 | // ==/UserScript==`, 140 | errors: [ 141 | { 142 | messageId: 'attributeNotStartsWithAtTheRate' 143 | }, 144 | { 145 | messageId: 'attributeNotStartsWithAtTheRate' 146 | } 147 | ] 148 | }, 149 | { 150 | code: `// abc 151 | // ==UserScript== 152 | // @name hello 153 | // @description invalid description`, 154 | errors: [ 155 | { 156 | messageId: 'noClosingMetadata' 157 | }, 158 | { 159 | messageId: 'moveMetadataToTop' 160 | } 161 | ] 162 | } 163 | ] 164 | }); 165 | -------------------------------------------------------------------------------- /tests/lib/rules/require-name.js: -------------------------------------------------------------------------------- 1 | var rule = require('..')['require-name']; 2 | var RuleTester = require('eslint').RuleTester; 3 | 4 | var ruleTester = new RuleTester(); 5 | ruleTester.run('require-name', rule, { 6 | valid: [ 7 | `// ==UserScript== 8 | // @name This is my description 9 | // ==/UserScript==` 10 | ], 11 | invalid: [ 12 | { 13 | code: `// ==UserScript== 14 | // @description abc 15 | // ==/UserScript==`, 16 | errors: [{ messageId: 'missingAttribute' }] 17 | }, 18 | { 19 | code: `// ==UserScript== 20 | // @name This is my name 21 | // @name This is my second name 22 | // ==/UserScript== 23 | console.info(variable) 24 | debugger 25 | /* debugging above */`, 26 | output: null, 27 | errors: [{ messageId: 'multipleNames' }] 28 | }, 29 | { 30 | code: `// ==UserScript== 31 | // @description This is my description 32 | // @name This is my name 33 | // ==/UserScript==`, 34 | output: `// ==UserScript== 35 | // @name This is my name 36 | // @description This is my description 37 | // ==/UserScript==`, 38 | errors: [{ messageId: 'nameAtBeginning' }] 39 | }, 40 | { 41 | code: `// ==UserScript== 42 | // @description This is my description 43 | // @name:en This is my name 44 | // ==/UserScript==`, 45 | output: `// ==UserScript== 46 | // @name:en This is my name 47 | // @description This is my description 48 | // ==/UserScript==`, 49 | errors: [{ messageId: 'nameAtBeginning' }] 50 | }, 51 | { 52 | code: `// ==UserScript== 53 | // @description This is my description 54 | // @name:en This is my name 55 | // @name:es This is my name 56 | // ==/UserScript==`, 57 | output: `// ==UserScript== 58 | // @name:en This is my name 59 | // @name:es This is my name 60 | // @description This is my description 61 | // ==/UserScript==`, 62 | errors: [{ messageId: 'nameAtBeginning' }] 63 | }, 64 | { 65 | code: `// ==UserScript== 66 | // @description This is my description 67 | // @name:es This is my name 68 | // @name This is my name 69 | // ==/UserScript==`, 70 | output: `// ==UserScript== 71 | // @name This is my name 72 | // @name:es This is my name 73 | // @description This is my description 74 | // ==/UserScript==`, 75 | errors: [{ messageId: 'nameAtBeginning' }] 76 | }, 77 | { 78 | code: `// ==UserScript== 79 | // @description This is my description 80 | // @name:es This is my name 81 | // @name:es This is my name 82 | // ==/UserScript==`, 83 | output: `// ==UserScript== 84 | // @name:es This is my name 85 | // @name:es This is my name 86 | // @description This is my description 87 | // ==/UserScript==`, 88 | errors: [{ messageId: 'nameAtBeginning' }, { messageId: 'multipleNames' }] 89 | }, 90 | { 91 | code: `// ==UserScript== 92 | // @description This is my description 93 | // @name:es This is my name in Spanish 94 | // @name This is my name 95 | // @version 1.0.0 96 | // @name:en This is my name in English 97 | // ==/UserScript==`, 98 | output: `// ==UserScript== 99 | // @name This is my name 100 | // @name:es This is my name in Spanish 101 | // @name:en This is my name in English 102 | // @description This is my description 103 | // @version 1.0.0 104 | // ==/UserScript==`, 105 | errors: [{ messageId: 'nameAtBeginning' }] 106 | }, 107 | { 108 | code: `// ==UserScript== 109 | // @description This is my description 110 | // @name:es This is my name in Spanish 111 | // @name This is my name 112 | // @version 1.0.0 113 | // @name:en This is my name in English`, 114 | output: `// ==UserScript== 115 | // @name This is my name 116 | // @name:es This is my name in Spanish 117 | // @name:en This is my name in English 118 | // @description This is my description 119 | // @version 1.0.0`, 120 | errors: [{ messageId: 'nameAtBeginning' }] 121 | }, 122 | { 123 | code: `// ==UserScript== 124 | // @description This is my description 125 | // @description This is my description2 126 | // @name:es This is my name in Spanish 127 | // @name This is my name 128 | // @version 1.0.0 129 | // @name:en This is my name in English`, 130 | output: `// ==UserScript== 131 | // @name This is my name 132 | // @name:es This is my name in Spanish 133 | // @name:en This is my name in English 134 | // @description This is my description 135 | // @description This is my description2 136 | // @version 1.0.0`, 137 | errors: [{ messageId: 'nameAtBeginning' }] 138 | } 139 | ] 140 | }); 141 | -------------------------------------------------------------------------------- /lib/utils/createValidator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * The metadata information on an attribute 3 | * @typedef {Object} Metadata 4 | * @property {string} val The value extracted from the comment 5 | * @property {import('acorn')['SourceLocation']} loc The location of the comment 6 | * @property {import('acorn')['Node']} comment The comment itself 7 | * @property {string} key The name of the key of the attribute 8 | */ 9 | 10 | /** 11 | * The callback for validators on validation rules 12 | * @callback validatorCallback 13 | * @param {{attrVal: Metadata | Metadata[], index: number | number[], indexMatch: number | number[], metadata: Object. | Array>, context: RuleContext, keyName: string | string[] }} validationInfo The information based on which the validator validates the metadata 14 | */ 15 | 16 | /** 17 | * Function to create a validator rule 18 | * @param {string|string[]} name The name of the attribute that it validates, can be a list of attributes 19 | * @param {boolean} required Whether the attribute(s) are required or not 20 | * @param {false|validatorCallback} [validator=false] The custom validator function, defaults to false 21 | * @param {Object.} [messages={}] Messages that are needed when reporting in the validator 22 | * @param {boolean} [fixable=false] Whether the rule is fixable or not 23 | * @param {RegExp} [regexMatch] A regular expression to match all keys, defaults to usage of name 24 | * @param {boolean} [runOnce=false] A boolean representing whether the validator should run once on all matches 25 | * @param {Object} [schema] The configuration options 26 | * @returns {import('eslint/lib/shared/types.js')['Rule']} The resulting validation rule 27 | */ 28 | module.exports = function createValidator( 29 | name, 30 | required, 31 | validator = false, 32 | messages = {}, 33 | fixable = false, 34 | regexMatch = new RegExp( 35 | '^(' + (typeof name === 'string' ? name : name.join('|')) + ')$' 36 | ), 37 | runOnce = false, 38 | schema 39 | ) { 40 | if (typeof name === 'string') { 41 | name = [name]; 42 | } 43 | 44 | return { 45 | meta: { 46 | type: 'suggestion', 47 | docs: { 48 | description: `${ 49 | required ? `require ${validator ? 'and validate ' : ''}` : 'validate ' 50 | }${name.join(' and ')} in the metadata for userscripts`, 51 | category: 'Best Practices' 52 | }, 53 | schema: required 54 | ? [ 55 | { 56 | enum: ['required', 'optional'], 57 | default: 'required' 58 | } 59 | ] 60 | : schema || undefined, 61 | messages: { 62 | missingAttribute: `Didn't find attribute '${name}' in the metadata`, 63 | ...messages 64 | }, 65 | fixable: fixable ? 'code' : undefined 66 | }, 67 | create: (context) => { 68 | const sourceCode = context.getSourceCode(); 69 | const comments = sourceCode.getAllComments(); 70 | 71 | let inMetadata = false; 72 | let wentInMetadata = false; 73 | let done = false; 74 | let metadata = {}; 75 | for (const comment of comments.filter( 76 | (comment) => comment.type === 'Line' 77 | )) { 78 | if (done) { 79 | continue; 80 | } 81 | 82 | // istanbul ignore else 83 | if (inMetadata && comment.value.trim() === '==/UserScript==') { 84 | done = true; 85 | } else if (!inMetadata && comment.value.trim() === '==UserScript==') { 86 | inMetadata = true; 87 | wentInMetadata = true; 88 | } else if (inMetadata && comment.value.trim().startsWith('@')) { 89 | const key = comment.value.trim().slice(1).split(/[\t ]/)[0]; 90 | const val = { 91 | val: comment.value 92 | .trim() 93 | .slice(1) 94 | .split(/[\t ]/) 95 | .slice(1) 96 | .join(' ') 97 | .trim(), 98 | loc: comment.loc, 99 | comment, 100 | key 101 | }; 102 | // istanbul ignore else 103 | if (metadata[key]) { 104 | if (!Array.isArray(metadata[key])) metadata[key] = [metadata[key]]; 105 | metadata[key].push(val); 106 | continue; 107 | } 108 | metadata[key] = val; 109 | } 110 | } 111 | 112 | const metadataKeys = Object.keys(metadata); 113 | // istanbul ignore else 114 | if ( 115 | required && 116 | wentInMetadata && 117 | (!context.options[0] || context.options[0] === 'required') && 118 | !metadataKeys.find((name) => regexMatch.test(name)) 119 | ) { 120 | context.report({ 121 | loc: comments.find( 122 | (comment) => 123 | comment.value.trim() === '==UserScript==' && 124 | comment.type === 'Line' 125 | ).loc, 126 | messageId: 'missingAttribute' 127 | }); 128 | } else if ( 129 | validator && 130 | metadataKeys.find((name) => regexMatch.test(name)) 131 | ) { 132 | if (runOnce) { 133 | const matchingMetadataKeyIndex = []; 134 | for (const metadataKeyIndex in metadataKeys) { 135 | if (regexMatch.test(metadataKeys[metadataKeyIndex])) { 136 | matchingMetadataKeyIndex.push(+metadataKeyIndex); 137 | } 138 | } 139 | const attributeValues = matchingMetadataKeyIndex 140 | .map((index) => metadata[metadataKeys[index]]) 141 | .reduce( 142 | (accumalator, metadataPart) => 143 | Array.isArray(metadataPart) 144 | ? [...accumalator, ...metadataPart] 145 | : [...accumalator, metadataPart], 146 | [] 147 | ); 148 | validator({ 149 | attrVal: attributeValues, 150 | index: [...attributeValues.keys()], 151 | indexMatch: matchingMetadataKeyIndex.reduce( 152 | (accumalator, metadataKeyIndex) => 153 | Array.isArray(metadata[metadataKeys[metadataKeyIndex]]) 154 | ? [ 155 | ...accumalator, 156 | ...metadata[metadataKeys[metadataKeyIndex]].map( 157 | () => metadataKeys 158 | ) 159 | ] 160 | : [...accumalator, metadataKeyIndex], 161 | [] 162 | ), 163 | metadata, 164 | context, 165 | keyName: matchingMetadataKeyIndex.map( 166 | (index) => metadataKeys[index] 167 | ) 168 | }); 169 | } else { 170 | for (const metadataKeyIndex in metadataKeys) { 171 | if (!regexMatch.test(metadataKeys[metadataKeyIndex])) { 172 | continue; 173 | } 174 | if (Array.isArray(metadata[metadataKeys[metadataKeyIndex]])) { 175 | for (const [index, attrVal] of metadata[ 176 | metadataKeys[metadataKeyIndex] 177 | ].entries()) { 178 | validator({ 179 | attrVal, 180 | index, 181 | indexMatch: metadataKeyIndex, 182 | metadata, 183 | context, 184 | keyName: metadataKeys[metadataKeyIndex] 185 | }); 186 | } 187 | } else { 188 | validator({ 189 | attrVal: metadata[metadataKeys[metadataKeyIndex]], 190 | index: 0, 191 | indexMatch: metadataKeyIndex, 192 | metadata, 193 | context, 194 | keyName: metadataKeys[metadataKeyIndex] 195 | }); 196 | } 197 | } 198 | } 199 | } 200 | 201 | return {}; 202 | } 203 | }; 204 | }; 205 | --------------------------------------------------------------------------------