eslint-plugin-greasemonkey 1.1.0 → 1.3.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 +673 -10
- package/package.json +30 -10
- package/src/apis.js +20 -0
- package/src/index.js +49 -4
- package/src/rules/meta-spacing.js +10 -19
- package/src/rules/no-duplicate-keys.js +35 -0
- package/src/rules/no-missing-grants.js +51 -0
- package/src/rules/no-unused-grants.js +16 -4
- package/src/rules/no-unused-resources.js +24 -14
- package/src/rules/prefer-match.js +24 -0
- package/src/rules/require-homepage-pair.js +43 -0
- package/src/rules/require-matching-localized-fields.js +6 -6
- package/src/rules/require-meta-newline.js +36 -0
- 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 +25 -0
- package/src/rules/valid-field-values.js +23 -14
- package/src/rules/valid-grants.js +56 -0
- package/src/utils.js +24 -5
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Require that userscript filenames end in ".user.js"',
|
|
8
|
+
category: 'Possible Errors',
|
|
9
|
+
recommended: true
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: { requireUserJS: 'Userscript filename should end in ".user.js"' }
|
|
13
|
+
},
|
|
14
|
+
|
|
15
|
+
create(context) {
|
|
16
|
+
return {
|
|
17
|
+
Program(node) {
|
|
18
|
+
const metaEntries = extractMetadata(context.sourceCode)
|
|
19
|
+
if (!metaEntries.length) return
|
|
20
|
+
if (!context.filename.endsWith('.user.js'))
|
|
21
|
+
context.report({ node, messageId: 'requireUserJS' })
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import { extractMetadata } from '../utils.js'
|
|
2
2
|
|
|
3
|
-
const re = {
|
|
4
|
-
semVer: /^\d+\.\d+\.\d+(?:-[
|
|
5
|
-
dateVer: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)
|
|
6
|
-
|
|
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
|
+
whitespace: /\s+/
|
|
7
|
+
} /* eslint-enable security/detect-unsafe-regex */
|
|
8
|
+
|
|
9
|
+
const urlKeys = ['homepage', 'icon', 'icon64', 'require', 'source', 'website']
|
|
7
10
|
|
|
8
11
|
export default {
|
|
9
12
|
meta: {
|
|
@@ -40,13 +43,26 @@ export default {
|
|
|
40
43
|
create(context) {
|
|
41
44
|
const options = context.options[0] || {},
|
|
42
45
|
versionType = options.versionType || 'semver',
|
|
43
|
-
allowedGrants = options.allowedGrants || []
|
|
44
|
-
|
|
46
|
+
allowedGrants = options.allowedGrants || [],
|
|
47
|
+
example = versionType == 'semver' ? '1.2.3' : 'YYYY.M.D or YYYY.MM.DD'
|
|
45
48
|
return {
|
|
46
49
|
Program() {
|
|
47
50
|
const metaEntries = extractMetadata(context.sourceCode)
|
|
48
51
|
for (const entry of metaEntries) {
|
|
49
|
-
if (entry.key == '
|
|
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) }
|
|
57
|
+
catch {
|
|
58
|
+
context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
|
|
59
|
+
}
|
|
60
|
+
} else if (urlKeys.includes(entry.key) || entry.key.toLowerCase().endsWith('url'))
|
|
61
|
+
try { new URL(entry.value) }
|
|
62
|
+
catch {
|
|
63
|
+
context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
|
|
64
|
+
}
|
|
65
|
+
else if (entry.key == 'version') {
|
|
50
66
|
const isValid = versionType == 'semver' ? re.semVer.test(entry.value)
|
|
51
67
|
: versionType == 'date' ? re.dateVer.test(entry.value)
|
|
52
68
|
: false
|
|
@@ -54,13 +70,6 @@ export default {
|
|
|
54
70
|
context.report({
|
|
55
71
|
node: entry.comment, messageId: 'invalidVersion', data: { versionType, example }})
|
|
56
72
|
}
|
|
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
73
|
}
|
|
65
74
|
}
|
|
66
75
|
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
import { gmLegacyAPIs, gmModernAPIs, scAPIs } from '../apis.js'
|
|
3
|
+
|
|
4
|
+
const specialGrants = new Set([
|
|
5
|
+
'none', 'unsafeWindow', 'window.close', 'window.focus', 'window.onurlchange'
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
const grants = new Set([...gmLegacyAPIs, ...gmModernAPIs, ...scAPIs, ...specialGrants]),
|
|
9
|
+
sortedGrants = new Map(Array.from(grants, grant => [grant.toLowerCase(), grant]))
|
|
10
|
+
|
|
11
|
+
export default {
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Disallow invalid @grant values',
|
|
16
|
+
category: 'Possible Errors',
|
|
17
|
+
recommended: true
|
|
18
|
+
},
|
|
19
|
+
schema: [{
|
|
20
|
+
type: 'object',
|
|
21
|
+
properties: {
|
|
22
|
+
allowedGrants: {
|
|
23
|
+
type: 'array',
|
|
24
|
+
items: { type: 'string' },
|
|
25
|
+
description: 'Additional @grant values to allow beyond the built-in list'
|
|
26
|
+
}
|
|
27
|
+
},
|
|
28
|
+
additionalProperties: false
|
|
29
|
+
}],
|
|
30
|
+
defaultOptions: [{}],
|
|
31
|
+
messages: {
|
|
32
|
+
invalidGrant: 'Invalid @grant: "{{grant}}"',
|
|
33
|
+
wrongCase: 'Invalid @grant: "{{grant}}" (did you mean "{{suggestion}}"?)'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
|
|
37
|
+
create(context) {
|
|
38
|
+
const options = context.options[0] || {},
|
|
39
|
+
allowedGrants = options.allowedGrants || []
|
|
40
|
+
return {
|
|
41
|
+
Program() {
|
|
42
|
+
const metaEntries = extractMetadata(context.sourceCode),
|
|
43
|
+
grantEntries = metaEntries.filter(entry => entry.key == 'grant')
|
|
44
|
+
for (const grant of grantEntries) {
|
|
45
|
+
const grantName = grant.value.trim()
|
|
46
|
+
if (grants.has(grantName) || allowedGrants.includes(grantName)) continue
|
|
47
|
+
const suggestion = sortedGrants.get(grantName.toLowerCase())
|
|
48
|
+
context.report(suggestion
|
|
49
|
+
? { node: grant.comment, messageId: 'wrongCase', data: { grant: grantName, suggestion }}
|
|
50
|
+
: { node: grant.comment, messageId: 'invalidGrant', data: { grant: grantName }}
|
|
51
|
+
)
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/utils.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export function extractMetadata(sourceCode) {
|
|
2
2
|
const comments = sourceCode.getAllComments(),
|
|
3
3
|
metaEntries = []
|
|
4
|
-
let inMetaBlock
|
|
4
|
+
let inMetaBlock
|
|
5
5
|
for (const comment of comments) {
|
|
6
6
|
const text = comment.value.trim()
|
|
7
|
-
if (!inMetaBlock && text == '==UserScript==')
|
|
8
|
-
if (inMetaBlock && text == '==/UserScript==') break
|
|
9
|
-
if (inMetaBlock && text.startsWith('@')) {
|
|
7
|
+
if (!inMetaBlock && text == '==UserScript==') inMetaBlock = true
|
|
8
|
+
else if (inMetaBlock && text == '==/UserScript==') break
|
|
9
|
+
else if (inMetaBlock && text.startsWith('@')) { // capture entry
|
|
10
10
|
const spaceIdx = text.indexOf(' ')
|
|
11
|
-
if (spaceIdx > 1) { //
|
|
11
|
+
if (spaceIdx > 1) { // need space after key
|
|
12
12
|
const key = text.slice(1, spaceIdx).trim(),
|
|
13
13
|
value = text.slice(spaceIdx +1).trim()
|
|
14
14
|
metaEntries.push({ key, value, comment, line: comment.loc.start.line })
|
|
@@ -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
|
+
}
|