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 CHANGED
@@ -1,37 +1,40 @@
1
- # eslint-plugin-greasemonkey
2
-
3
- [![](https://img.shields.io/badge/Requires-ESLint_%3E%3D9.0.0-4B32C3?style=for-the-badge&logo=eslint)](https://eslint.org/)
4
-
5
- ESLint rules for Greasemonkey userscripts.
6
-
7
- <img src="https://cdn.staticdelivr.com/gl/adamlui/eslint-plugin-greasemonkey/b7c510f949/assets/images/screenshots/meta-spacing/auto-fix.png">
8
-
9
- ## Installation
10
-
11
- ```bash
12
- npm i -D eslint-plugin-greasemonkey
13
- ```
14
-
15
- ## Usage (flat config)
16
-
17
- ```js
18
- import greasemonkey from 'eslint-plugin-greasemonkey'
19
- export default [
20
- {
21
- files: ['**/*.user.js'],
22
- plugins: { greasemonkey },
23
- rules: {
24
- 'greasemonkey/meta-spacing': 'error',
25
- 'greasemonkey/no-unused-grants': 'error',
26
- 'greasemonkey/no-unused-resources': 'error',
27
- 'greasemonkey/require-matching-localized-fields': 'error',
28
- 'greasemonkey/require-required-fields': 'error',
29
- 'greasemonkey/valid-field-values': 'error'
30
- }
31
- }
32
- ]
33
- ```
34
-
35
- ## License
36
-
37
- Copyright © 2026 [Adam Lui](https://codeberg.org/adamlui).
1
+ # eslint-plugin-greasemonkey
2
+
3
+ [![](https://img.shields.io/npm/v/eslint-plugin-greasemonkey?style=for-the-badge&label=Latest%20Build&color=32fcee)](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://img.shields.io/badge/Requires-ESLint_%3E%3D9.0.0-4B32C3?style=for-the-badge&logo=eslint)](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).
@@ -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
@@ -0,0 +1,3 @@
1
+ # Security Policy
2
+
3
+ If you find a vulnerability, please e-mail <security@kudoai.org>.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-greasemonkey",
3
- "version": "1.0.0",
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": ["src"],
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-eslint-plugin": "^6.5.0",
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
@@ -0,0 +1,6 @@
1
+ declare const plugin: {
2
+ rules: Record<string, unknown>
3
+ configs: { recommended: Record<string, unknown> }
4
+ }
5
+
6
+ export default plugin
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 reLeadingWhitespace = /^\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
- const longestKey = metaEntries.reduce((max, entry) => Math.max(max, entry.key.length), 0),
27
- targetColumn = longestKey + minSpaces +4
28
- for (const entry of metaEntries) {
29
- const keyLen = entry.key.length,
30
- commentText = sourceCode.getText(entry.comment),
31
- actualSpaces = commentText.split(entry.key)[1].match(reLeadingWhitespace)[0].length,
32
- desiredSpaces = targetColumn - keyLen -4
33
- if (actualSpaces != desiredSpaces)
34
- context.report({
35
- node: entry.comment, messageId: 'misaligned', data: { column: targetColumn },
36
- fix(fixer) {
37
- const keyIdx = commentText.indexOf(entry.key) + entry.key.length,
38
- rest = commentText.slice(keyIdx).replace(reLeadingWhitespace, ''),
39
- fixedText = commentText.slice(0, keyIdx) + ' '.repeat(desiredSpaces) + rest
40
- return fixer.replaceText(entry.comment, fixedText)
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
- unusedGrant: 'Unused @grant: "{{grant}}". If used indirectly,'
14
- + ' add // eslint-disable-line greasemonkey/no-unused-grants'
15
- }
16
- },
17
-
18
- create(context) {
19
- const usedIdentifiers = new Set()
20
- return {
21
- Identifier(node) { usedIdentifiers.add(node.name) },
22
- 'Program:exit'() {
23
- const sourceCode = context.sourceCode,
24
- metaEntries = extractMetadata(sourceCode),
25
- grants = metaEntries.filter(e => e.key == 'grant')
26
- if (!grants.length) return
27
- for (const grant of grants) {
28
- const grantName = grant.value.trim()
29
- if (['none', 'unsafeWindow'].includes(grantName)) continue
30
- if (!usedIdentifiers.has(grantName))
31
- context.report({ node: grant.comment, messageId: 'unusedGrant', data: { grant: grantName }})
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
- unusedResource: 'Unused @resource: "{{name}}". If used indirectly,'
16
- + ' add // eslint-disable-line greasemonkey/no-unused-resources'
17
- }
18
- },
19
-
20
- create(context) {
21
- return {
22
- Program() {
23
- const sourceCode = context.sourceCode,
24
- metaEntries = extractMetadata(sourceCode),
25
- resources = metaEntries.filter(e => e.key == 'resource')
26
- if (!resources.length) return
27
- const text = sourceCode.getText(),
28
- usedResources = new Set(),
29
- regex = /GM_getResource(?:Text|URL)\s*\(\s*['"]([^'"]+)['"]\s*\)/g
30
- let match
31
- while ((match = regex.exec(text)) !== null) usedResources.add(match[1])
32
- for (const res of resources) {
33
- const name = res.value.split(reWhitespace)[0]
34
- if (!usedResources.has(name))
35
- context.report({ node: res.comment, messageId: 'unusedResource', data: { name }})
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 RE_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i
4
-
5
- export default {
6
- meta: {
7
- type: 'problem',
8
- docs: {
9
- description: 'Validate values of metadata fields',
10
- category: 'Possible Errors',
11
- recommended: true
12
- },
13
- schema: [{
14
- type: 'object',
15
- properties: {
16
- reVer: {
17
- type: 'string',
18
- description: 'Regular expression pattern for @version validation (default: ^\\d+\\.\\d+\\.\\d+$)'
19
- },
20
- allowedGrants: {
21
- type: 'array',
22
- items: { type: 'string' },
23
- description: 'Allowed @grant values to check against; if empty, no grant validation'
24
- }
25
- },
26
- additionalProperties: false
27
- }],
28
- defaultOptions: [{}],
29
- messages: {
30
- invalidVersion: 'Invalid @version: must match pattern "{{pattern}}"',
31
- invalidURL: 'Invalid URL for @{{field}}',
32
- invalidGrant: 'Unknown @grant: "{{grant}}"'
33
- }
34
- },
35
-
36
- create(context) {
37
- const options = context.options[0] || {},
38
- reVer = options.reVer || '^\\d+\\.\\d+\\.\\d+$',
39
- allowedGrants = options.allowedGrants || []
40
- return {
41
- Program() {
42
- const metaEntries = extractMetadata(context.sourceCode)
43
- for (const entry of metaEntries) {
44
- if (entry.key == 'version') {
45
- if (!RE_SEMVER.test(entry.value) && !(new RegExp(reVer).test(entry.value)))
46
- context.report({
47
- node: entry.comment, messageId: 'invalidVersion', data: { pattern: reVer }})
48
- }
49
- if (['homepage', 'icon', 'updateURL', 'downloadURL', 'supportURL'].includes(entry.key))
50
- try { new URL(entry.value) }
51
- catch {
52
- context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
53
- }
54
- if (entry.key == 'grant' && allowedGrants.length && !allowedGrants.includes(entry.value))
55
- context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
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
+ }