eslint-plugin-greasemonkey 1.2.0 → 1.4.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 +706 -191
- package/package.json +9 -8
- package/src/data/apis.js +35 -0
- package/src/data/deprecations.js +3 -0
- package/src/data/headers.js +30 -0
- package/src/index.js +64 -1
- package/src/rules/no-duplicate-keys.js +2 -2
- package/src/rules/no-invalid-headers.js +80 -0
- package/src/rules/no-missing-grants.js +51 -0
- package/src/rules/no-missing-resources.js +61 -0
- package/src/rules/no-unused-grants.js +16 -4
- package/src/rules/no-unused-resources.js +24 -13
- package/src/rules/prefer-match.js +24 -0
- package/src/rules/require-blank-line-after-meta-block.js +33 -0
- package/src/rules/require-homepage-pair.js +43 -0
- package/src/rules/require-matching-localized-fields.js +6 -6
- package/src/rules/require-required-fields.js +3 -3
- package/src/rules/require-space-before-meta-key.js +39 -0
- package/src/rules/require-update-download-pair.js +32 -0
- package/src/rules/require-user-js-filename.js +2 -2
- package/src/rules/valid-field-values.js +18 -20
- package/src/rules/valid-grants.js +31 -37
- package/src/utils.js +20 -1
|
@@ -14,7 +14,7 @@ export default {
|
|
|
14
14
|
ignoreLanguages: {
|
|
15
15
|
type: 'array',
|
|
16
16
|
items: { type: 'string' },
|
|
17
|
-
description: 'Language codes to ignore (e.g
|
|
17
|
+
description: 'Language codes to ignore (e.g. ["fr", "de"])'
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
additionalProperties: false
|
|
@@ -29,10 +29,10 @@ export default {
|
|
|
29
29
|
return {
|
|
30
30
|
Program() {
|
|
31
31
|
const metaEntries = extractMetadata(context.sourceCode),
|
|
32
|
-
nameEntries = metaEntries.filter(
|
|
33
|
-
descEntries = metaEntries.filter(
|
|
34
|
-
nameLangs = new Set(nameEntries.map(
|
|
35
|
-
descLangs = new Set(descEntries.map(
|
|
32
|
+
nameEntries = metaEntries.filter(entry => entry.key.startsWith('name:')),
|
|
33
|
+
descEntries = metaEntries.filter(entry => entry.key.startsWith('description:')),
|
|
34
|
+
nameLangs = new Set(nameEntries.map(entry => entry.key.split(':')[1])),
|
|
35
|
+
descLangs = new Set(descEntries.map(entry => entry.key.split(':')[1]))
|
|
36
36
|
for (const lang of nameLangs)
|
|
37
37
|
if (!ignoreLanguages.includes(lang) && !descLangs.has(lang))
|
|
38
38
|
context.report({
|
|
@@ -43,7 +43,7 @@ export default {
|
|
|
43
43
|
for (const lang of descLangs)
|
|
44
44
|
if (!ignoreLanguages.includes(lang) && !nameLangs.has(lang))
|
|
45
45
|
context.report({
|
|
46
|
-
node: descEntries.find(
|
|
46
|
+
node: descEntries.find(entry => entry.key == `description:${lang}`).comment,
|
|
47
47
|
messageId: 'missingPair',
|
|
48
48
|
data: { missing: `name:${lang}`, present: `description:${lang}` }
|
|
49
49
|
})
|
|
@@ -14,7 +14,7 @@ export default {
|
|
|
14
14
|
fields: {
|
|
15
15
|
type: 'array',
|
|
16
16
|
items: { type: 'string' },
|
|
17
|
-
description: 'Required metadata fields (default: name,
|
|
17
|
+
description: 'Required metadata fields (default: name, version)'
|
|
18
18
|
}
|
|
19
19
|
},
|
|
20
20
|
additionalProperties: false
|
|
@@ -25,11 +25,11 @@ export default {
|
|
|
25
25
|
|
|
26
26
|
create(context) {
|
|
27
27
|
const options = context.options[0] || {},
|
|
28
|
-
requiredFields = options.fields || ['name', '
|
|
28
|
+
requiredFields = options.fields || ['name', 'version']
|
|
29
29
|
return {
|
|
30
30
|
Program(node) {
|
|
31
31
|
const metaEntries = extractMetadata(context.sourceCode),
|
|
32
|
-
keys = new Set(metaEntries.map(
|
|
32
|
+
keys = new Set(metaEntries.map(entry => entry.key))
|
|
33
33
|
for (const field of requiredFields)
|
|
34
34
|
if (!keys.has(field))
|
|
35
35
|
context.report({ node, messageId: 'missingField', data: { field }})
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { leadingWhitespace: /^\s*/ }
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'layout',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Require exactly one space between // and metadata keys',
|
|
10
|
+
category: 'Stylistic Issues',
|
|
11
|
+
recommended: true
|
|
12
|
+
},
|
|
13
|
+
fixable: 'whitespace',
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: { badPrefix: '`{{actual}}` should be `// @{{key}}`' }
|
|
16
|
+
},
|
|
17
|
+
|
|
18
|
+
create(context) {
|
|
19
|
+
return {
|
|
20
|
+
Program() {
|
|
21
|
+
const sourceCode = context.sourceCode,
|
|
22
|
+
metaEntries = extractMetadata(sourceCode)
|
|
23
|
+
for (const entry of metaEntries) {
|
|
24
|
+
const commentText = sourceCode.getText(entry.comment),
|
|
25
|
+
afterSlashes = commentText.slice(2)
|
|
26
|
+
if (afterSlashes.startsWith(' @')) continue
|
|
27
|
+
const rest = afterSlashes.replace(re.leadingWhitespace, ''),
|
|
28
|
+
keyIdx = commentText.indexOf(`@${entry.key}`),
|
|
29
|
+
actual = commentText.slice(0, keyIdx +1 + entry.key.length),
|
|
30
|
+
fixedText = `// ${rest}`
|
|
31
|
+
context.report({
|
|
32
|
+
node: entry.comment, messageId: 'badPrefix', data: { actual, key: entry.key },
|
|
33
|
+
fix(fixer) { return fixer.replaceText(entry.comment, fixedText) }
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Require @updateURL and @downloadURL to be used together',
|
|
8
|
+
category: 'Best Practices',
|
|
9
|
+
recommended: false
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: { missingPair: 'Missing @{{missing}} (required when @{{present}} is used)' }
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
Program() {
|
|
18
|
+
const metaEntries = extractMetadata(context.sourceCode),
|
|
19
|
+
updateEntry = metaEntries.find(entry => entry.key == 'updateURL'),
|
|
20
|
+
downloadEntry = metaEntries.find(entry => entry.key == 'downloadURL')
|
|
21
|
+
if (updateEntry && !downloadEntry)
|
|
22
|
+
context.report({
|
|
23
|
+
node: updateEntry.comment, messageId: 'missingPair',
|
|
24
|
+
data: { missing: 'downloadURL', present: 'updateURL' }})
|
|
25
|
+
else if (downloadEntry && !updateEntry)
|
|
26
|
+
context.report({
|
|
27
|
+
node: downloadEntry.comment, messageId: 'missingPair',
|
|
28
|
+
data: { missing: 'updateURL', present: 'downloadURL' }})
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -9,7 +9,7 @@ export default {
|
|
|
9
9
|
recommended: true
|
|
10
10
|
},
|
|
11
11
|
schema: [],
|
|
12
|
-
messages: {
|
|
12
|
+
messages: { requireUserJS: 'Userscript filename should end in ".user.js"' }
|
|
13
13
|
},
|
|
14
14
|
|
|
15
15
|
create(context) {
|
|
@@ -18,7 +18,7 @@ export default {
|
|
|
18
18
|
const metaEntries = extractMetadata(context.sourceCode)
|
|
19
19
|
if (!metaEntries.length) return
|
|
20
20
|
if (!context.filename.endsWith('.user.js'))
|
|
21
|
-
context.report({ node, messageId: '
|
|
21
|
+
context.report({ node, messageId: 'requireUserJS' })
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
}
|
|
@@ -3,10 +3,11 @@ import { extractMetadata } from '../utils.js'
|
|
|
3
3
|
const re = { /* eslint-disable security/detect-unsafe-regex */
|
|
4
4
|
semVer: /^\d+\.\d+\.\d+(?:-[\da-z.-]+)?(?:\+[\da-z.-]+)?$/i,
|
|
5
5
|
dateVer: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/,
|
|
6
|
-
urlSuffix: /url$/i,
|
|
7
6
|
whitespace: /\s+/
|
|
8
7
|
} /* eslint-enable security/detect-unsafe-regex */
|
|
9
8
|
|
|
9
|
+
const urlKeys = ['homepage', 'icon', 'icon64', 'require', 'source', 'website']
|
|
10
|
+
|
|
10
11
|
export default {
|
|
11
12
|
meta: {
|
|
12
13
|
type: 'problem',
|
|
@@ -42,36 +43,33 @@ export default {
|
|
|
42
43
|
create(context) {
|
|
43
44
|
const options = context.options[0] || {},
|
|
44
45
|
versionType = options.versionType || 'semver',
|
|
45
|
-
allowedGrants = options.allowedGrants || []
|
|
46
|
-
|
|
46
|
+
allowedGrants = options.allowedGrants || [],
|
|
47
|
+
example = versionType == 'semver' ? '1.2.3' : 'YYYY.M.D or YYYY.MM.DD'
|
|
47
48
|
return {
|
|
48
49
|
Program() {
|
|
49
50
|
const metaEntries = extractMetadata(context.sourceCode)
|
|
50
51
|
for (const entry of metaEntries) {
|
|
51
|
-
if (entry.key == '
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
context.report({
|
|
57
|
-
node: entry.comment, messageId: 'invalidVersion', data: { versionType, example }})
|
|
58
|
-
}
|
|
59
|
-
if (['homepage', 'icon', 'icon64', 'require', 'source', 'website'].includes(entry.key)
|
|
60
|
-
|| re.urlSuffix.test(entry.key)
|
|
61
|
-
)
|
|
62
|
-
try { new URL(entry.value) }
|
|
52
|
+
if (entry.key == 'grant' && allowedGrants.length && !allowedGrants.includes(entry.value))
|
|
53
|
+
context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
|
|
54
|
+
else if (entry.key == 'resource') {
|
|
55
|
+
const url = entry.value.trim().split(re.whitespace)[1]
|
|
56
|
+
if (url) try { new URL(url) }
|
|
63
57
|
catch {
|
|
64
58
|
context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
|
|
65
59
|
}
|
|
66
|
-
if (entry.key
|
|
67
|
-
|
|
68
|
-
if (url) try { new URL(url) }
|
|
60
|
+
} else if (urlKeys.includes(entry.key) || entry.key.toLowerCase().endsWith('url'))
|
|
61
|
+
try { new URL(entry.value) }
|
|
69
62
|
catch {
|
|
70
63
|
context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
|
|
71
64
|
}
|
|
65
|
+
else if (entry.key == 'version') {
|
|
66
|
+
const isValid = versionType == 'semver' ? re.semVer.test(entry.value)
|
|
67
|
+
: versionType == 'date' ? re.dateVer.test(entry.value)
|
|
68
|
+
: false
|
|
69
|
+
if (!isValid)
|
|
70
|
+
context.report({
|
|
71
|
+
node: entry.comment, messageId: 'invalidVersion', data: { versionType, example }})
|
|
72
72
|
}
|
|
73
|
-
if (entry.key == 'grant' && allowedGrants.length && !allowedGrants.includes(entry.value))
|
|
74
|
-
context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
|
|
75
73
|
}
|
|
76
74
|
}
|
|
77
75
|
}
|
|
@@ -1,32 +1,10 @@
|
|
|
1
1
|
import { extractMetadata } from '../utils.js'
|
|
2
|
+
import { allAPIs, isAPIValidFor } from '../data/apis.js'
|
|
2
3
|
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
'GM_removeValueChangeListener', 'GM_saveTab', 'GM_setClipboard', 'GM_setValue', 'GM_setValues',
|
|
8
|
-
'GM_unregisterMenuCommand', 'GM_webRequest', 'GM_xmlhttpRequest'
|
|
9
|
-
]) /* eslint-enable no-secrets/no-secrets */
|
|
10
|
-
|
|
11
|
-
const gm_modern_apis = new Set([
|
|
12
|
-
'GM.addElement', 'GM.addStyle', 'GM.addValueChangeListener', 'GM.deleteValue', 'GM.download',
|
|
13
|
-
'GM.getResourceText', 'GM.getResourceUrl', 'GM.getValue', 'GM.info', 'GM.listValues', 'GM.log',
|
|
14
|
-
'GM.notification', 'GM.openInTab', 'GM.registerMenuCommand', 'GM.removeValueChangeListener',
|
|
15
|
-
'GM.setClipboard', 'GM.setValue', 'GM.unregisterMenuCommand', 'GM.xmlHttpRequest'
|
|
16
|
-
])
|
|
17
|
-
|
|
18
|
-
const sc_apis = new Set([
|
|
19
|
-
'CAT.agent.conversation', 'CAT.agent.dom', 'CAT.agent.mcp', 'CAT.agent.model',
|
|
20
|
-
'CAT.agent.opfs', 'CAT.agent.skills', 'CAT.agent.task',
|
|
21
|
-
'CAT_fileStorage', 'CAT_scriptLoaded', 'CAT_userConfig'
|
|
22
|
-
])
|
|
23
|
-
|
|
24
|
-
const specialGrants = new Set([
|
|
25
|
-
'none', 'unsafeWindow', 'window.close', 'window.focus', 'window.onurlchange'
|
|
26
|
-
])
|
|
27
|
-
|
|
28
|
-
const grants = new Set([...gm_legacy_apis, ...gm_modern_apis, ...sc_apis, ...specialGrants]),
|
|
29
|
-
sortedGrants = new Map(Array.from(grants, grant => [grant.toLowerCase(), grant]))
|
|
4
|
+
const managers = ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat'],
|
|
5
|
+
specialGrants = new Set(['none', 'unsafeWindow', 'window.close', 'window.focus', 'window.onurlchange']),
|
|
6
|
+
allGrants = new Set([...allAPIs, ...specialGrants]),
|
|
7
|
+
sortedGrants = new Map(Array.from(allGrants, grant => [grant.toLowerCase(), grant]))
|
|
30
8
|
|
|
31
9
|
export default {
|
|
32
10
|
meta: {
|
|
@@ -39,6 +17,13 @@ export default {
|
|
|
39
17
|
schema: [{
|
|
40
18
|
type: 'object',
|
|
41
19
|
properties: {
|
|
20
|
+
target: {
|
|
21
|
+
anyOf: [
|
|
22
|
+
{ type: 'string', enum: ['all', ...managers] },
|
|
23
|
+
{ type: 'array', items: { type: 'string', enum: managers }, minItems: 1, uniqueItems: true }
|
|
24
|
+
],
|
|
25
|
+
description: 'Manager(s) whose grants to validate against (default: all)'
|
|
26
|
+
},
|
|
42
27
|
allowedGrants: {
|
|
43
28
|
type: 'array',
|
|
44
29
|
items: { type: 'string' },
|
|
@@ -50,28 +35,37 @@ export default {
|
|
|
50
35
|
defaultOptions: [{}],
|
|
51
36
|
messages: {
|
|
52
37
|
invalidGrant: 'Invalid @grant: "{{grant}}"',
|
|
38
|
+
unsupportedGrant: '"@{{grant}}" is not supported by {{target}} (only by {{supporting}})',
|
|
53
39
|
wrongCase: 'Invalid @grant: "{{grant}}" (did you mean "{{suggestion}}"?)'
|
|
54
40
|
}
|
|
55
41
|
},
|
|
56
42
|
|
|
57
43
|
create(context) {
|
|
58
44
|
const options = context.options[0] || {},
|
|
45
|
+
rawTarget = options.target ?? 'all',
|
|
46
|
+
targets = Array.isArray(rawTarget) ? rawTarget : [rawTarget],
|
|
59
47
|
allowedGrants = options.allowedGrants || []
|
|
60
48
|
return {
|
|
61
49
|
Program() {
|
|
62
50
|
const metaEntries = extractMetadata(context.sourceCode),
|
|
63
|
-
grantEntries = metaEntries.filter(
|
|
51
|
+
grantEntries = metaEntries.filter(entry => entry.key == 'grant')
|
|
64
52
|
for (const grant of grantEntries) {
|
|
65
53
|
const grantName = grant.value.trim()
|
|
66
|
-
if (
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
node: grant.comment, messageId: 'wrongCase',
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
54
|
+
if (specialGrants.has(grantName) || allowedGrants.includes(grantName)) continue
|
|
55
|
+
else if (!allGrants.has(grantName)) {
|
|
56
|
+
const suggestion = sortedGrants.get(grantName.toLowerCase())
|
|
57
|
+
if (suggestion && suggestion != grantName)
|
|
58
|
+
context.report({ node: grant.comment, messageId: 'wrongCase',
|
|
59
|
+
data: { grant: grantName, suggestion }})
|
|
60
|
+
else
|
|
61
|
+
context.report({ node: grant.comment, messageId: 'invalidGrant',
|
|
62
|
+
data: { grant: grantName }})
|
|
63
|
+
continue
|
|
64
|
+
} else if (!targets.includes('all') && !targets.some(m => isAPIValidFor(grantName, m))) {
|
|
65
|
+
const supporting = managers.filter(manager => isAPIValidFor(grantName, manager ))
|
|
66
|
+
context.report({ node: grant.comment, messageId: 'unsupportedGrant',
|
|
67
|
+
data: { grant: grantName, target: targets.join(', '), supporting: supporting.join(', ') }})
|
|
68
|
+
}
|
|
75
69
|
}
|
|
76
70
|
}
|
|
77
71
|
}
|
package/src/utils.js
CHANGED
|
@@ -4,7 +4,7 @@ export function extractMetadata(sourceCode) {
|
|
|
4
4
|
let inMetaBlock
|
|
5
5
|
for (const comment of comments) {
|
|
6
6
|
const text = comment.value.trim()
|
|
7
|
-
if (!inMetaBlock && text == '==UserScript==')
|
|
7
|
+
if (!inMetaBlock && text == '==UserScript==') inMetaBlock = true
|
|
8
8
|
else if (inMetaBlock && text == '==/UserScript==') break
|
|
9
9
|
else if (inMetaBlock && text.startsWith('@')) { // capture entry
|
|
10
10
|
const spaceIdx = text.indexOf(' ')
|
|
@@ -17,3 +17,22 @@ export function extractMetadata(sourceCode) {
|
|
|
17
17
|
}
|
|
18
18
|
return metaEntries
|
|
19
19
|
}
|
|
20
|
+
|
|
21
|
+
export function resolveTemplate(node) {
|
|
22
|
+
if (node.type == 'ConditionalExpression') {
|
|
23
|
+
const branches = [node.consequent, node.alternate].map(resolveTemplate)
|
|
24
|
+
return branches.every(Boolean) ? branches.flat() : null
|
|
25
|
+
} else if (node.type == 'Literal' && ['number', 'string'].includes(typeof node.value))
|
|
26
|
+
return [String(node.value)]
|
|
27
|
+
else if (node.type == 'TemplateLiteral') // build every combo of quasis + resolved expressions
|
|
28
|
+
return node.quasis.reduce((combos, quasi, idx) => {
|
|
29
|
+
if (!combos) return null
|
|
30
|
+
const withQuasi = combos.map(combo => combo + quasi.value.raw)
|
|
31
|
+
if (idx >= node.expressions.length) return withQuasi
|
|
32
|
+
const vals = // eslint-disable-next-line security/detect-object-injection -- AST array indices are bounded
|
|
33
|
+
resolveTemplate(node.expressions[idx])
|
|
34
|
+
return vals ? withQuasi.flatMap(combo => vals.map(val => combo + val)) : null
|
|
35
|
+
}, [''])
|
|
36
|
+
else
|
|
37
|
+
return null
|
|
38
|
+
}
|