├── 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 | 
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 | [](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 | [](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 |
--------------------------------------------------------------------------------