eslint-plugin-greasemonkey 1.0.0 → 1.2.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 +492 -37
- package/docs/LICENSE.md +9 -0
- package/docs/SECURITY.md +3 -0
- package/package.json +46 -12
- package/src/index.d.ts +6 -0
- package/src/index.js +42 -33
- package/src/rules/meta-spacing.js +51 -47
- package/src/rules/no-duplicate-keys.js +35 -0
- package/src/rules/no-unused-grants.js +33 -36
- package/src/rules/no-unused-resources.js +36 -40
- package/src/rules/require-matching-localized-fields.js +53 -53
- package/src/rules/require-required-fields.js +41 -41
- package/src/rules/require-user-js-filename.js +25 -0
- package/src/rules/valid-field-values.js +79 -60
- package/src/rules/valid-grants.js +79 -0
- package/src/utils.js +19 -19
|
@@ -1,60 +1,79 @@
|
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { /* eslint-disable security/detect-unsafe-regex */
|
|
4
|
+
semVer: /^\d+\.\d+\.\d+(?:-[\da-z.-]+)?(?:\+[\da-z.-]+)?$/i,
|
|
5
|
+
dateVer: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/,
|
|
6
|
+
urlSuffix: /url$/i,
|
|
7
|
+
whitespace: /\s+/
|
|
8
|
+
} /* eslint-enable security/detect-unsafe-regex */
|
|
9
|
+
|
|
10
|
+
export default {
|
|
11
|
+
meta: {
|
|
12
|
+
type: 'problem',
|
|
13
|
+
docs: {
|
|
14
|
+
description: 'Validate values of metadata fields',
|
|
15
|
+
category: 'Possible Errors',
|
|
16
|
+
recommended: true
|
|
17
|
+
},
|
|
18
|
+
schema: [{
|
|
19
|
+
type: 'object',
|
|
20
|
+
properties: {
|
|
21
|
+
versionType: {
|
|
22
|
+
type: 'string',
|
|
23
|
+
enum: ['semver', 'date'],
|
|
24
|
+
description: 'Version format to enforce (default: semver)'
|
|
25
|
+
},
|
|
26
|
+
allowedGrants: {
|
|
27
|
+
type: 'array',
|
|
28
|
+
items: { type: 'string' },
|
|
29
|
+
description: 'Allowed @grant values to check against; if empty, no grant validation'
|
|
30
|
+
}
|
|
31
|
+
},
|
|
32
|
+
additionalProperties: false
|
|
33
|
+
}],
|
|
34
|
+
defaultOptions: [{}],
|
|
35
|
+
messages: {
|
|
36
|
+
invalidVersion: 'Invalid @version: must be a valid {{versionType}} (expected format: {{example}})',
|
|
37
|
+
invalidURL: 'Invalid URL for @{{field}}',
|
|
38
|
+
invalidGrant: 'Unknown @grant: "{{grant}}"'
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
create(context) {
|
|
43
|
+
const options = context.options[0] || {},
|
|
44
|
+
versionType = options.versionType || 'semver',
|
|
45
|
+
allowedGrants = options.allowedGrants || []
|
|
46
|
+
const example = versionType == 'semver' ? '1.2.3' : 'YYYY.M.D or YYYY.MM.DD'
|
|
47
|
+
return {
|
|
48
|
+
Program() {
|
|
49
|
+
const metaEntries = extractMetadata(context.sourceCode)
|
|
50
|
+
for (const entry of metaEntries) {
|
|
51
|
+
if (entry.key == 'version') {
|
|
52
|
+
const isValid = versionType == 'semver' ? re.semVer.test(entry.value)
|
|
53
|
+
: versionType == 'date' ? re.dateVer.test(entry.value)
|
|
54
|
+
: false
|
|
55
|
+
if (!isValid)
|
|
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) }
|
|
63
|
+
catch {
|
|
64
|
+
context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
|
|
65
|
+
}
|
|
66
|
+
if (entry.key == 'resource') {
|
|
67
|
+
const url = entry.value.trim().split(re.whitespace)[1]
|
|
68
|
+
if (url) try { new URL(url) }
|
|
69
|
+
catch {
|
|
70
|
+
context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
|
|
71
|
+
}
|
|
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
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const gm_legacy_apis = new Set([ /* eslint-disable no-secrets/no-secrets */
|
|
4
|
+
'GM_addElement', 'GM_addStyle', 'GM_addValueChangeListener', 'GM_deleteValue', 'GM_download',
|
|
5
|
+
'GM_getResourceText', 'GM_getResourceURL', 'GM_getTab', 'GM_getTabs', 'GM_getValue', 'GM_getValues',
|
|
6
|
+
'GM_info', 'GM_listValues', 'GM_log', 'GM_notification', 'GM_openInTab', 'GM_registerMenuCommand',
|
|
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]))
|
|
30
|
+
|
|
31
|
+
export default {
|
|
32
|
+
meta: {
|
|
33
|
+
type: 'problem',
|
|
34
|
+
docs: {
|
|
35
|
+
description: 'Disallow invalid @grant values',
|
|
36
|
+
category: 'Possible Errors',
|
|
37
|
+
recommended: true
|
|
38
|
+
},
|
|
39
|
+
schema: [{
|
|
40
|
+
type: 'object',
|
|
41
|
+
properties: {
|
|
42
|
+
allowedGrants: {
|
|
43
|
+
type: 'array',
|
|
44
|
+
items: { type: 'string' },
|
|
45
|
+
description: 'Additional @grant values to allow beyond the built-in list'
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
additionalProperties: false
|
|
49
|
+
}],
|
|
50
|
+
defaultOptions: [{}],
|
|
51
|
+
messages: {
|
|
52
|
+
invalidGrant: 'Invalid @grant: "{{grant}}"',
|
|
53
|
+
wrongCase: 'Invalid @grant: "{{grant}}" (did you mean "{{suggestion}}"?)'
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
create(context) {
|
|
58
|
+
const options = context.options[0] || {},
|
|
59
|
+
allowedGrants = options.allowedGrants || []
|
|
60
|
+
return {
|
|
61
|
+
Program() {
|
|
62
|
+
const metaEntries = extractMetadata(context.sourceCode),
|
|
63
|
+
grantEntries = metaEntries.filter(e => e.key == 'grant')
|
|
64
|
+
for (const grant of grantEntries) {
|
|
65
|
+
const grantName = grant.value.trim()
|
|
66
|
+
if (grants.has(grantName) || allowedGrants.includes(grantName)) continue
|
|
67
|
+
const suggestion = sortedGrants.get(grantName.toLowerCase())
|
|
68
|
+
if (suggestion)
|
|
69
|
+
context.report({
|
|
70
|
+
node: grant.comment, messageId: 'wrongCase',
|
|
71
|
+
data: { grant: grantName, suggestion }})
|
|
72
|
+
else
|
|
73
|
+
context.report({
|
|
74
|
+
node: grant.comment, messageId: 'invalidGrant', data: { grant: grantName }})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
export function extractMetadata(sourceCode) {
|
|
2
|
-
const comments = sourceCode.getAllComments(),
|
|
3
|
-
metaEntries = []
|
|
4
|
-
let inMetaBlock
|
|
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) { //
|
|
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
|
|
5
|
+
for (const comment of comments) {
|
|
6
|
+
const text = comment.value.trim()
|
|
7
|
+
if (!inMetaBlock && text == '==UserScript==') { inMetaBlock = true ; continue }
|
|
8
|
+
else if (inMetaBlock && text == '==/UserScript==') break
|
|
9
|
+
else if (inMetaBlock && text.startsWith('@')) { // capture entry
|
|
10
|
+
const spaceIdx = text.indexOf(' ')
|
|
11
|
+
if (spaceIdx > 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
|
+
}
|