eslint-plugin-greasemonkey 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -37
- package/docs/LICENSE.md +9 -0
- package/docs/SECURITY.md +3 -0
- package/package.json +19 -5
- package/src/index.d.ts +6 -0
- package/src/index.js +33 -33
- package/src/rules/meta-spacing.js +60 -47
- package/src/rules/no-unused-grants.js +33 -36
- package/src/rules/no-unused-resources.js +37 -40
- package/src/rules/require-matching-localized-fields.js +53 -53
- package/src/rules/require-required-fields.js +41 -41
- package/src/rules/valid-field-values.js +68 -60
- package/src/utils.js +19 -19
package/README.md
CHANGED
|
@@ -1,37 +1,40 @@
|
|
|
1
|
-
# eslint-plugin-greasemonkey
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/eslint-plugin-greasemonkey)
|
|
4
|
+
<a href="./#license">
|
|
5
|
+
<img src="https://img.shields.io/badge/License-MIT-f99b27.svg?logo=internetarchive&logoColor=white&labelColor=464646&style=for-the-badge"></a>
|
|
6
|
+
[](https://eslint.org/)
|
|
7
|
+
|
|
8
|
+
ESLint rules for Greasemonkey userscripts.
|
|
9
|
+
|
|
10
|
+
<img src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/b7c510f949/assets/images/screenshots/meta-spacing/auto-fix.png">
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm i -D eslint-plugin-greasemonkey
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Usage (flat config)
|
|
19
|
+
|
|
20
|
+
```js
|
|
21
|
+
import greasemonkey from 'eslint-plugin-greasemonkey'
|
|
22
|
+
export default [
|
|
23
|
+
{
|
|
24
|
+
files: ['**/*.user.js'],
|
|
25
|
+
plugins: { greasemonkey },
|
|
26
|
+
rules: {
|
|
27
|
+
'greasemonkey/meta-spacing': 'error',
|
|
28
|
+
'greasemonkey/no-unused-grants': 'error',
|
|
29
|
+
'greasemonkey/no-unused-resources': 'error',
|
|
30
|
+
'greasemonkey/require-matching-localized-fields': 'error',
|
|
31
|
+
'greasemonkey/require-required-fields': 'error',
|
|
32
|
+
'greasemonkey/valid-field-values': 'error'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## License
|
|
39
|
+
|
|
40
|
+
Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui).
|
package/docs/LICENSE.md
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# 🏛️ MIT License
|
|
2
|
+
|
|
3
|
+
**Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui).**
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
6
|
+
|
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
8
|
+
|
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE
|
package/docs/SECURITY.md
ADDED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-greasemonkey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "ESLint rules for Greasemonkey/Tampermonkey userscripts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -14,7 +14,11 @@
|
|
|
14
14
|
"homepage": "https://codeberg.org/adamlui/eslint-plugin-greasemonkey/#readme",
|
|
15
15
|
"license": "MIT",
|
|
16
16
|
"main": "src/index.js",
|
|
17
|
-
"files": [
|
|
17
|
+
"files": [
|
|
18
|
+
"docs/",
|
|
19
|
+
"src/"
|
|
20
|
+
],
|
|
21
|
+
"types": "src/index.d.ts",
|
|
18
22
|
"scripts": {
|
|
19
23
|
"prepare": "husky",
|
|
20
24
|
"lint": "eslint . --cache",
|
|
@@ -22,7 +26,11 @@
|
|
|
22
26
|
"lint:fix": "eslint . --fix --cache",
|
|
23
27
|
"lint:fix-all": "eslint . --fix",
|
|
24
28
|
"test": "node tests/run.js",
|
|
25
|
-
"demo": "eslint --config demo/eslint.config.js demo/example.user.js"
|
|
29
|
+
"demo": "eslint --config demo/eslint.config.js demo/example.user.js",
|
|
30
|
+
"bump:patch": "bash utils/bump.sh patch",
|
|
31
|
+
"bump:minor": "bash utils/bump.sh minor",
|
|
32
|
+
"bump:feat": "npm run bump:minor",
|
|
33
|
+
"bump:major": "bash utils/bump.sh major"
|
|
26
34
|
},
|
|
27
35
|
"keywords": [
|
|
28
36
|
"greasemonkey",
|
|
@@ -30,19 +38,25 @@
|
|
|
30
38
|
"userscript",
|
|
31
39
|
"eslint",
|
|
32
40
|
"eslint-plugin",
|
|
41
|
+
"eslintplugin",
|
|
33
42
|
"metadata"
|
|
34
43
|
],
|
|
35
44
|
"devDependencies": {
|
|
45
|
+
"@e18e/eslint-plugin": "^0.8.0",
|
|
36
46
|
"@eslint/js": "^10.0.1",
|
|
37
47
|
"@eslint/json": "^2.1.0",
|
|
38
48
|
"@eslint/markdown": "^8.0.3",
|
|
39
49
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
40
50
|
"eslint": "^10.10.0",
|
|
41
|
-
"eslint-plugin-
|
|
51
|
+
"eslint-plugin-constructors": "^1.0.2",
|
|
52
|
+
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
42
53
|
"eslint-plugin-import-x": "^4.17.1",
|
|
54
|
+
"eslint-plugin-no-secrets": "^2.3.3",
|
|
55
|
+
"eslint-plugin-no-unsanitized": "^4.1.5",
|
|
43
56
|
"eslint-plugin-promise": "^7.3.0",
|
|
44
57
|
"eslint-plugin-regexp": "^3.3.0",
|
|
45
|
-
"globals": "^17.12.0"
|
|
58
|
+
"globals": "^17.12.0",
|
|
59
|
+
"husky": "^9.1.7"
|
|
46
60
|
},
|
|
47
61
|
"peerDependencies": {
|
|
48
62
|
"eslint": ">=9.0.0"
|
package/src/index.d.ts
ADDED
package/src/index.js
CHANGED
|
@@ -1,33 +1,33 @@
|
|
|
1
|
-
import metaSpacing from './rules/meta-spacing.js'
|
|
2
|
-
import noUnusedGrants from './rules/no-unused-grants.js'
|
|
3
|
-
import noUnusedResources from './rules/no-unused-resources.js'
|
|
4
|
-
import requireMatchingLocalizedFields from './rules/require-matching-localized-fields.js'
|
|
5
|
-
import requireRequiredFields from './rules/require-required-fields.js'
|
|
6
|
-
import validFieldValues from './rules/valid-field-values.js'
|
|
7
|
-
|
|
8
|
-
const plugin = {
|
|
9
|
-
rules: {
|
|
10
|
-
'meta-spacing': metaSpacing,
|
|
11
|
-
'require-matching-localized-fields': requireMatchingLocalizedFields,
|
|
12
|
-
'require-required-fields': requireRequiredFields,
|
|
13
|
-
'valid-field-values': validFieldValues,
|
|
14
|
-
'no-unused-resources': noUnusedResources,
|
|
15
|
-
'no-unused-grants': noUnusedGrants
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
plugin.configs = {
|
|
20
|
-
recommended: {
|
|
21
|
-
plugins: { greasemonkey: plugin },
|
|
22
|
-
rules: {
|
|
23
|
-
'greasemonkey/meta-spacing': 'error',
|
|
24
|
-
'greasemonkey/require-matching-localized-fields': 'error',
|
|
25
|
-
'greasemonkey/require-required-fields': 'error',
|
|
26
|
-
'greasemonkey/valid-field-values': 'error',
|
|
27
|
-
'greasemonkey/no-unused-resources': 'error',
|
|
28
|
-
'greasemonkey/no-unused-grants': 'error'
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export default plugin
|
|
1
|
+
import metaSpacing from './rules/meta-spacing.js'
|
|
2
|
+
import noUnusedGrants from './rules/no-unused-grants.js'
|
|
3
|
+
import noUnusedResources from './rules/no-unused-resources.js'
|
|
4
|
+
import requireMatchingLocalizedFields from './rules/require-matching-localized-fields.js'
|
|
5
|
+
import requireRequiredFields from './rules/require-required-fields.js'
|
|
6
|
+
import validFieldValues from './rules/valid-field-values.js'
|
|
7
|
+
|
|
8
|
+
const plugin = {
|
|
9
|
+
rules: {
|
|
10
|
+
'meta-spacing': metaSpacing,
|
|
11
|
+
'require-matching-localized-fields': requireMatchingLocalizedFields,
|
|
12
|
+
'require-required-fields': requireRequiredFields,
|
|
13
|
+
'valid-field-values': validFieldValues,
|
|
14
|
+
'no-unused-resources': noUnusedResources,
|
|
15
|
+
'no-unused-grants': noUnusedGrants
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
plugin.configs = {
|
|
20
|
+
recommended: {
|
|
21
|
+
plugins: { greasemonkey: plugin },
|
|
22
|
+
rules: {
|
|
23
|
+
'greasemonkey/meta-spacing': 'error',
|
|
24
|
+
'greasemonkey/require-matching-localized-fields': 'error',
|
|
25
|
+
'greasemonkey/require-required-fields': 'error',
|
|
26
|
+
'greasemonkey/valid-field-values': 'error',
|
|
27
|
+
'greasemonkey/no-unused-resources': 'error',
|
|
28
|
+
'greasemonkey/no-unused-grants': 'error'
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export default plugin
|
|
@@ -1,47 +1,60 @@
|
|
|
1
|
-
import { extractMetadata } from '../utils.js'
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
meta: {
|
|
7
|
-
type: 'layout',
|
|
8
|
-
docs: {
|
|
9
|
-
description: 'Align metadata values to the column after the longest key',
|
|
10
|
-
category: 'Stylistic Issues',
|
|
11
|
-
recommended: true
|
|
12
|
-
},
|
|
13
|
-
fixable: 'whitespace',
|
|
14
|
-
schema: [{ type: 'number', description: 'Minimum spaces after the longest key (default 3)' }],
|
|
15
|
-
defaultOptions: [3],
|
|
16
|
-
messages: { misaligned: 'Metadata values should be aligned at column {{column}}' }
|
|
17
|
-
},
|
|
18
|
-
|
|
19
|
-
create(context) {
|
|
20
|
-
const minSpaces = context.options[0] ?? 3
|
|
21
|
-
return {
|
|
22
|
-
Program() {
|
|
23
|
-
const sourceCode = context.sourceCode,
|
|
24
|
-
metaEntries = extractMetadata(sourceCode)
|
|
25
|
-
if (!metaEntries.length) return
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { leadingWhitespace: /^\s*/, whitespace: /\s+/ }
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'layout',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Align metadata values to the column after the longest key',
|
|
10
|
+
category: 'Stylistic Issues',
|
|
11
|
+
recommended: true
|
|
12
|
+
},
|
|
13
|
+
fixable: 'whitespace',
|
|
14
|
+
schema: [{ type: 'number', description: 'Minimum spaces after the longest key (default 3)' }],
|
|
15
|
+
defaultOptions: [3],
|
|
16
|
+
messages: { misaligned: 'Metadata values should be aligned at column {{column}}' }
|
|
17
|
+
},
|
|
18
|
+
|
|
19
|
+
create(context) {
|
|
20
|
+
const minSpaces = context.options[0] ?? 3
|
|
21
|
+
return {
|
|
22
|
+
Program() {
|
|
23
|
+
const sourceCode = context.sourceCode,
|
|
24
|
+
metaEntries = extractMetadata(sourceCode)
|
|
25
|
+
if (!metaEntries.length) return
|
|
26
|
+
let targetCol = metaEntries.reduce((max, entry) => Math.max(max, entry.key.length), 0) + minSpaces +4
|
|
27
|
+
for (const entry of metaEntries) {
|
|
28
|
+
if (entry.key == 'resource') {
|
|
29
|
+
const name = entry.value.trim().split(re.whitespace)[0],
|
|
30
|
+
requiredColumn = 4 + entry.key.length +1 + name.length + minSpaces
|
|
31
|
+
if (requiredColumn > targetCol) targetCol = requiredColumn
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
for (const entry of metaEntries) {
|
|
35
|
+
const commentText = sourceCode.getText(entry.comment),
|
|
36
|
+
keyIdx = commentText.indexOf(entry.key) + entry.key.length
|
|
37
|
+
let fixedText
|
|
38
|
+
if (entry.key == 'resource') {
|
|
39
|
+
const parts = entry.value.trim().split(re.whitespace)
|
|
40
|
+
if (parts.length >= 2) {
|
|
41
|
+
const name = parts[0],
|
|
42
|
+
url = parts.slice(1).join(' '),
|
|
43
|
+
spacesAfterName = targetCol - keyIdx -1 - name.length
|
|
44
|
+
fixedText = commentText.slice(0, keyIdx) + ' ' + name + ' '.repeat(spacesAfterName) + url
|
|
45
|
+
} else
|
|
46
|
+
fixedText = commentText.slice(0, keyIdx) + ' '.repeat(targetCol - keyIdx)
|
|
47
|
+
+ entry.value.trim()
|
|
48
|
+
} else
|
|
49
|
+
fixedText = commentText.slice(0, keyIdx) + ' '.repeat(targetCol - keyIdx)
|
|
50
|
+
+ entry.value.trim()
|
|
51
|
+
if (commentText !== fixedText)
|
|
52
|
+
context.report({
|
|
53
|
+
node: entry.comment, messageId: 'misaligned', data: { column: targetCol },
|
|
54
|
+
fix(fixer) { return fixer.replaceText(entry.comment, fixedText) }
|
|
55
|
+
})
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -1,36 +1,33 @@
|
|
|
1
|
-
import { extractMetadata } from '../utils.js'
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
meta: {
|
|
5
|
-
type: 'problem',
|
|
6
|
-
docs: {
|
|
7
|
-
description: 'Flag @grant entries whose APIs are never used',
|
|
8
|
-
category: 'Possible Errors',
|
|
9
|
-
recommended: true
|
|
10
|
-
},
|
|
11
|
-
schema: [],
|
|
12
|
-
messages: {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Flag @grant entries whose APIs are never used',
|
|
8
|
+
category: 'Possible Errors',
|
|
9
|
+
recommended: true
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: { unusedGrant: 'Unused @grant: "{{grant}}".' }
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
create(context) {
|
|
16
|
+
const usedIdentifiers = new Set()
|
|
17
|
+
return {
|
|
18
|
+
Identifier(node) { usedIdentifiers.add(node.name) },
|
|
19
|
+
'Program:exit'() {
|
|
20
|
+
const sourceCode = context.sourceCode,
|
|
21
|
+
metaEntries = extractMetadata(sourceCode),
|
|
22
|
+
grants = metaEntries.filter(e => e.key == 'grant')
|
|
23
|
+
if (!grants.length) return
|
|
24
|
+
for (const grant of grants) {
|
|
25
|
+
const grantName = grant.value.trim()
|
|
26
|
+
if (['none', 'unsafeWindow'].includes(grantName)) continue
|
|
27
|
+
if (!usedIdentifiers.has(grantName))
|
|
28
|
+
context.report({ node: grant.comment, messageId: 'unusedGrant', data: { grant: grantName }})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -1,40 +1,37 @@
|
|
|
1
|
-
import { extractMetadata } from '../utils.js'
|
|
2
|
-
|
|
3
|
-
const reWhitespace = /\s+/
|
|
4
|
-
|
|
5
|
-
export default {
|
|
6
|
-
meta: {
|
|
7
|
-
type: 'problem',
|
|
8
|
-
docs: {
|
|
9
|
-
description: 'Flag @resource entries that are never referenced directly',
|
|
10
|
-
category: 'Possible Errors',
|
|
11
|
-
recommended: true
|
|
12
|
-
},
|
|
13
|
-
schema: [],
|
|
14
|
-
messages: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const reWhitespace = /\s+/
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Flag @resource entries that are never referenced directly',
|
|
10
|
+
category: 'Possible Errors',
|
|
11
|
+
recommended: true
|
|
12
|
+
},
|
|
13
|
+
schema: [],
|
|
14
|
+
messages: { unusedResource: 'Unused @resource: "{{name}}".' }
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
create(context) {
|
|
18
|
+
return {
|
|
19
|
+
Program() {
|
|
20
|
+
const sourceCode = context.sourceCode,
|
|
21
|
+
metaEntries = extractMetadata(sourceCode),
|
|
22
|
+
resources = metaEntries.filter(e => e.key == 'resource')
|
|
23
|
+
if (!resources.length) return
|
|
24
|
+
const text = sourceCode.getText(),
|
|
25
|
+
usedResources = new Set(),
|
|
26
|
+
regex = /GM_getResource(?:Text|URL)\s*\(\s*['"]([^'"]+)['"]\s*\)/g
|
|
27
|
+
let match
|
|
28
|
+
while ((match = regex.exec(text)) !== null) usedResources.add(match[1])
|
|
29
|
+
for (const res of resources) {
|
|
30
|
+
const name = res.value.split(reWhitespace)[0]
|
|
31
|
+
if (!usedResources.has(name))
|
|
32
|
+
context.report({ node: res.comment, messageId: 'unusedResource', data: { name }})
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -1,53 +1,53 @@
|
|
|
1
|
-
import { extractMetadata } from '../utils.js'
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
meta: {
|
|
5
|
-
type: 'problem',
|
|
6
|
-
docs: {
|
|
7
|
-
description: 'Require matching localized name and description fields',
|
|
8
|
-
category: 'Possible Errors',
|
|
9
|
-
recommended: true
|
|
10
|
-
},
|
|
11
|
-
schema: [{
|
|
12
|
-
type: 'object',
|
|
13
|
-
properties: {
|
|
14
|
-
ignoreLanguages: {
|
|
15
|
-
type: 'array',
|
|
16
|
-
items: { type: 'string' },
|
|
17
|
-
description: 'Language codes to ignore (e.g., ["fr", "de"])'
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
additionalProperties: false
|
|
21
|
-
}],
|
|
22
|
-
defaultOptions: [{}],
|
|
23
|
-
messages: { missingPair: 'Missing matching @{{missing}} for @{{present}}' }
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
create(context) {
|
|
27
|
-
const options = context.options[0] || {},
|
|
28
|
-
ignoreLanguages = options.ignoreLanguages || []
|
|
29
|
-
return {
|
|
30
|
-
Program() {
|
|
31
|
-
const metaEntries = extractMetadata(context.sourceCode),
|
|
32
|
-
nameEntries = metaEntries.filter(e => e.key.startsWith('name:')),
|
|
33
|
-
descEntries = metaEntries.filter(e => e.key.startsWith('description:')),
|
|
34
|
-
nameLangs = new Set(nameEntries.map(e => e.key.split(':')[1])),
|
|
35
|
-
descLangs = new Set(descEntries.map(e => e.key.split(':')[1]))
|
|
36
|
-
for (const lang of nameLangs)
|
|
37
|
-
if (!ignoreLanguages.includes(lang) && !descLangs.has(lang))
|
|
38
|
-
context.report({
|
|
39
|
-
node: nameEntries.find(entry => entry.key == `name:${lang}`).comment,
|
|
40
|
-
messageId: 'missingPair',
|
|
41
|
-
data: { missing: `description:${lang}`, present: `name:${lang}` }
|
|
42
|
-
})
|
|
43
|
-
for (const lang of descLangs)
|
|
44
|
-
if (!ignoreLanguages.includes(lang) && !nameLangs.has(lang))
|
|
45
|
-
context.report({
|
|
46
|
-
node: descEntries.find(e => e.key == `description:${lang}`).comment,
|
|
47
|
-
messageId: 'missingPair',
|
|
48
|
-
data: { missing: `name:${lang}`, present: `description:${lang}` }
|
|
49
|
-
})
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Require matching localized name and description fields',
|
|
8
|
+
category: 'Possible Errors',
|
|
9
|
+
recommended: true
|
|
10
|
+
},
|
|
11
|
+
schema: [{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
ignoreLanguages: {
|
|
15
|
+
type: 'array',
|
|
16
|
+
items: { type: 'string' },
|
|
17
|
+
description: 'Language codes to ignore (e.g., ["fr", "de"])'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
}],
|
|
22
|
+
defaultOptions: [{}],
|
|
23
|
+
messages: { missingPair: 'Missing matching @{{missing}} for @{{present}}' }
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create(context) {
|
|
27
|
+
const options = context.options[0] || {},
|
|
28
|
+
ignoreLanguages = options.ignoreLanguages || []
|
|
29
|
+
return {
|
|
30
|
+
Program() {
|
|
31
|
+
const metaEntries = extractMetadata(context.sourceCode),
|
|
32
|
+
nameEntries = metaEntries.filter(e => e.key.startsWith('name:')),
|
|
33
|
+
descEntries = metaEntries.filter(e => e.key.startsWith('description:')),
|
|
34
|
+
nameLangs = new Set(nameEntries.map(e => e.key.split(':')[1])),
|
|
35
|
+
descLangs = new Set(descEntries.map(e => e.key.split(':')[1]))
|
|
36
|
+
for (const lang of nameLangs)
|
|
37
|
+
if (!ignoreLanguages.includes(lang) && !descLangs.has(lang))
|
|
38
|
+
context.report({
|
|
39
|
+
node: nameEntries.find(entry => entry.key == `name:${lang}`).comment,
|
|
40
|
+
messageId: 'missingPair',
|
|
41
|
+
data: { missing: `description:${lang}`, present: `name:${lang}` }
|
|
42
|
+
})
|
|
43
|
+
for (const lang of descLangs)
|
|
44
|
+
if (!ignoreLanguages.includes(lang) && !nameLangs.has(lang))
|
|
45
|
+
context.report({
|
|
46
|
+
node: descEntries.find(e => e.key == `description:${lang}`).comment,
|
|
47
|
+
messageId: 'missingPair',
|
|
48
|
+
data: { missing: `name:${lang}`, present: `description:${lang}` }
|
|
49
|
+
})
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,41 +1,41 @@
|
|
|
1
|
-
import { extractMetadata } from '../utils.js'
|
|
2
|
-
|
|
3
|
-
export default {
|
|
4
|
-
meta: {
|
|
5
|
-
type: 'problem',
|
|
6
|
-
docs: {
|
|
7
|
-
description: 'Require essential metadata fields',
|
|
8
|
-
category: 'Possible Errors',
|
|
9
|
-
recommended: true
|
|
10
|
-
},
|
|
11
|
-
schema: [{
|
|
12
|
-
type: 'object',
|
|
13
|
-
properties: {
|
|
14
|
-
fields: {
|
|
15
|
-
type: 'array',
|
|
16
|
-
items: { type: 'string' },
|
|
17
|
-
description: 'Required metadata fields (default: name, namespace, version)'
|
|
18
|
-
}
|
|
19
|
-
},
|
|
20
|
-
additionalProperties: false
|
|
21
|
-
}],
|
|
22
|
-
defaultOptions: [{}],
|
|
23
|
-
messages: { missingField: 'Missing required metadata field: @{{field}}' }
|
|
24
|
-
},
|
|
25
|
-
|
|
26
|
-
create(context) {
|
|
27
|
-
const options = context.options[0] || {},
|
|
28
|
-
requiredFields = options.fields || ['name', 'namespace', 'version']
|
|
29
|
-
return {
|
|
30
|
-
Program(node) {
|
|
31
|
-
const metaEntries = extractMetadata(context.sourceCode),
|
|
32
|
-
keys = new Set(metaEntries.map(e => e.key))
|
|
33
|
-
for (const field of requiredFields)
|
|
34
|
-
if (!keys.has(field))
|
|
35
|
-
context.report({ node, messageId: 'missingField', data: { field }})
|
|
36
|
-
if (!metaEntries.some(entry => ['include', 'match'].includes(entry.key)))
|
|
37
|
-
context.report({ node, messageId: 'missingField', data: { field: 'match or include' }})
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Require essential metadata fields',
|
|
8
|
+
category: 'Possible Errors',
|
|
9
|
+
recommended: true
|
|
10
|
+
},
|
|
11
|
+
schema: [{
|
|
12
|
+
type: 'object',
|
|
13
|
+
properties: {
|
|
14
|
+
fields: {
|
|
15
|
+
type: 'array',
|
|
16
|
+
items: { type: 'string' },
|
|
17
|
+
description: 'Required metadata fields (default: name, namespace, version)'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
additionalProperties: false
|
|
21
|
+
}],
|
|
22
|
+
defaultOptions: [{}],
|
|
23
|
+
messages: { missingField: 'Missing required metadata field: @{{field}}' }
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create(context) {
|
|
27
|
+
const options = context.options[0] || {},
|
|
28
|
+
requiredFields = options.fields || ['name', 'namespace', 'version']
|
|
29
|
+
return {
|
|
30
|
+
Program(node) {
|
|
31
|
+
const metaEntries = extractMetadata(context.sourceCode),
|
|
32
|
+
keys = new Set(metaEntries.map(e => e.key))
|
|
33
|
+
for (const field of requiredFields)
|
|
34
|
+
if (!keys.has(field))
|
|
35
|
+
context.report({ node, messageId: 'missingField', data: { field }})
|
|
36
|
+
if (!metaEntries.some(entry => ['include', 'match'].includes(entry.key)))
|
|
37
|
+
context.report({ node, messageId: 'missingField', data: { field: 'match or include' }})
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -1,60 +1,68 @@
|
|
|
1
|
-
import { extractMetadata } from '../utils.js'
|
|
2
|
-
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
}
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = {
|
|
4
|
+
semVer: /^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i,
|
|
5
|
+
dateVer: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export default {
|
|
9
|
+
meta: {
|
|
10
|
+
type: 'problem',
|
|
11
|
+
docs: {
|
|
12
|
+
description: 'Validate values of metadata fields',
|
|
13
|
+
category: 'Possible Errors',
|
|
14
|
+
recommended: true
|
|
15
|
+
},
|
|
16
|
+
schema: [{
|
|
17
|
+
type: 'object',
|
|
18
|
+
properties: {
|
|
19
|
+
versionType: {
|
|
20
|
+
type: 'string',
|
|
21
|
+
enum: ['semver', 'date'],
|
|
22
|
+
description: 'Version format to enforce (default: semver)'
|
|
23
|
+
},
|
|
24
|
+
allowedGrants: {
|
|
25
|
+
type: 'array',
|
|
26
|
+
items: { type: 'string' },
|
|
27
|
+
description: 'Allowed @grant values to check against; if empty, no grant validation'
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
additionalProperties: false
|
|
31
|
+
}],
|
|
32
|
+
defaultOptions: [{}],
|
|
33
|
+
messages: {
|
|
34
|
+
invalidVersion: 'Invalid @version: must be a valid {{versionType}} (expected format: {{example}})',
|
|
35
|
+
invalidURL: 'Invalid URL for @{{field}}',
|
|
36
|
+
invalidGrant: 'Unknown @grant: "{{grant}}"'
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
|
|
40
|
+
create(context) {
|
|
41
|
+
const options = context.options[0] || {},
|
|
42
|
+
versionType = options.versionType || 'semver',
|
|
43
|
+
allowedGrants = options.allowedGrants || []
|
|
44
|
+
const example = versionType == 'semver' ? '1.2.3' : 'YYYY.M.D or YYYY.MM.DD'
|
|
45
|
+
return {
|
|
46
|
+
Program() {
|
|
47
|
+
const metaEntries = extractMetadata(context.sourceCode)
|
|
48
|
+
for (const entry of metaEntries) {
|
|
49
|
+
if (entry.key == 'version') {
|
|
50
|
+
const isValid = versionType == 'semver' ? re.semVer.test(entry.value)
|
|
51
|
+
: versionType == 'date' ? re.dateVer.test(entry.value)
|
|
52
|
+
: false
|
|
53
|
+
if (!isValid)
|
|
54
|
+
context.report({
|
|
55
|
+
node: entry.comment, messageId: 'invalidVersion', data: { versionType, example }})
|
|
56
|
+
}
|
|
57
|
+
if (['homepage', 'icon', 'updateURL', 'downloadURL', 'supportURL'].includes(entry.key))
|
|
58
|
+
try { new URL(entry.value) }
|
|
59
|
+
catch {
|
|
60
|
+
context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
|
|
61
|
+
}
|
|
62
|
+
if (entry.key == 'grant' && allowedGrants.length && !allowedGrants.includes(entry.value))
|
|
63
|
+
context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
export function extractMetadata(sourceCode) {
|
|
2
|
-
const comments = sourceCode.getAllComments(),
|
|
3
|
-
metaEntries = []
|
|
4
|
-
let inMetaBlock = false
|
|
5
|
-
for (const comment of comments) {
|
|
6
|
-
const text = comment.value.trim()
|
|
7
|
-
if (!inMetaBlock && text == '==UserScript==') { inMetaBlock = true ; continue }
|
|
8
|
-
if (inMetaBlock && text == '==/UserScript==') break
|
|
9
|
-
if (inMetaBlock && text.startsWith('@')) {
|
|
10
|
-
const spaceIdx = text.indexOf(' ')
|
|
11
|
-
if (spaceIdx > 1) { // '@' at 0, key starts at 1, need space after key
|
|
12
|
-
const key = text.slice(1, spaceIdx).trim(),
|
|
13
|
-
value = text.slice(spaceIdx +1).trim()
|
|
14
|
-
metaEntries.push({ key, value, comment, line: comment.loc.start.line })
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
return metaEntries
|
|
19
|
-
}
|
|
1
|
+
export function extractMetadata(sourceCode) {
|
|
2
|
+
const comments = sourceCode.getAllComments(),
|
|
3
|
+
metaEntries = []
|
|
4
|
+
let inMetaBlock = false
|
|
5
|
+
for (const comment of comments) {
|
|
6
|
+
const text = comment.value.trim()
|
|
7
|
+
if (!inMetaBlock && text == '==UserScript==') { inMetaBlock = true ; continue }
|
|
8
|
+
if (inMetaBlock && text == '==/UserScript==') break
|
|
9
|
+
if (inMetaBlock && text.startsWith('@')) {
|
|
10
|
+
const spaceIdx = text.indexOf(' ')
|
|
11
|
+
if (spaceIdx > 1) { // '@' at 0, key starts at 1, need space after key
|
|
12
|
+
const key = text.slice(1, spaceIdx).trim(),
|
|
13
|
+
value = text.slice(spaceIdx +1).trim()
|
|
14
|
+
metaEntries.push({ key, value, comment, line: comment.loc.start.line })
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
return metaEntries
|
|
19
|
+
}
|