eslint-plugin-greasemonkey 1.3.0 → 1.5.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 +757 -289
- package/package.json +8 -7
- package/src/{apis.js → data/apis.js} +19 -3
- package/src/data/deprecations.js +3 -0
- package/src/data/headers.js +29 -0
- package/src/index.js +45 -21
- package/src/rules/meta-order.js +76 -0
- package/src/rules/no-duplicate-keys.js +3 -3
- package/src/rules/no-invalid-headers.js +79 -0
- package/src/rules/no-missing-grants.js +2 -3
- package/src/rules/no-missing-resources.js +61 -0
- package/src/rules/{require-meta-newline.js → require-blank-line-after-meta-block.js} +6 -10
- package/src/rules/require-homepage-pair.js +6 -6
- package/src/rules/require-matching-localized-fields.js +6 -4
- package/src/rules/require-sri-hashing.js +57 -0
- package/src/rules/valid-grants.js +38 -16
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "eslint-plugin-greasemonkey",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "ESLint rules for Greasemonkey/Tampermonkey userscripts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -35,10 +35,10 @@
|
|
|
35
35
|
"types": "src/index.d.ts",
|
|
36
36
|
"scripts": {
|
|
37
37
|
"prepare": "husky",
|
|
38
|
-
"lint": "eslint . --cache",
|
|
39
|
-
"lint:all": "eslint .",
|
|
40
|
-
"lint:fix": "eslint . --fix --cache",
|
|
41
|
-
"lint:fix-all": "eslint . --fix",
|
|
38
|
+
"lint": "eslint . --cache && yamllint .",
|
|
39
|
+
"lint:all": "eslint . && yamllint .",
|
|
40
|
+
"lint:fix": "eslint . --fix --cache && yamllint .",
|
|
41
|
+
"lint:fix-all": "eslint . --fix && yamllint .",
|
|
42
42
|
"test": "node tests/run.js",
|
|
43
43
|
"demo": "node demo/run.js",
|
|
44
44
|
"bump:patch": "bash utils/bump.sh patch",
|
|
@@ -69,7 +69,7 @@
|
|
|
69
69
|
"@eslint/markdown": "^8.0.3",
|
|
70
70
|
"@stylistic/eslint-plugin": "^5.10.0",
|
|
71
71
|
"eslint": "^10.10.0",
|
|
72
|
-
"eslint-plugin-constructors": "^1.
|
|
72
|
+
"eslint-plugin-constructors": "^1.2.0",
|
|
73
73
|
"eslint-plugin-eslint-plugin": "^7.6.2",
|
|
74
74
|
"eslint-plugin-import-x": "^4.17.1",
|
|
75
75
|
"eslint-plugin-n": "^18.3.0",
|
|
@@ -79,6 +79,7 @@
|
|
|
79
79
|
"eslint-plugin-regexp": "^3.3.0",
|
|
80
80
|
"eslint-plugin-security": "^4.0.1",
|
|
81
81
|
"globals": "^17.12.0",
|
|
82
|
-
"husky": "^9.1.7"
|
|
82
|
+
"husky": "^9.1.7",
|
|
83
|
+
"yamllint-js": "^0.2.4"
|
|
83
84
|
}
|
|
84
85
|
}
|
|
@@ -8,9 +8,10 @@ export const gmLegacyAPIs = new Set([ /* eslint-disable no-secrets/no-secrets */
|
|
|
8
8
|
|
|
9
9
|
export const gmModernAPIs = new Set([
|
|
10
10
|
'GM.addElement', 'GM.addStyle', 'GM.addValueChangeListener', 'GM.deleteValue', 'GM.download',
|
|
11
|
-
'GM.getResourceText', 'GM.getResourceUrl', 'GM.
|
|
12
|
-
'GM.
|
|
13
|
-
'GM.
|
|
11
|
+
'GM.getResourceText', 'GM.getResourceUrl', 'GM.getTab', 'GM.getTabs', 'GM.getValue', 'GM.getValues',
|
|
12
|
+
'GM.info', 'GM.listValues', 'GM.log', 'GM.notification', 'GM.openInTab', 'GM.registerMenuCommand',
|
|
13
|
+
'GM.removeValueChangeListener', 'GM.saveTab', 'GM.setClipboard', 'GM.setValue', 'GM.setValues',
|
|
14
|
+
'GM.unregisterMenuCommand', 'GM.webRequest', 'GM.xmlHttpRequest'
|
|
14
15
|
])
|
|
15
16
|
|
|
16
17
|
export const scAPIs = new Set([
|
|
@@ -18,3 +19,18 @@ export const scAPIs = new Set([
|
|
|
18
19
|
'CAT.agent.opfs', 'CAT.agent.skills', 'CAT.agent.task',
|
|
19
20
|
'CAT_fileStorage', 'CAT_scriptLoaded', 'CAT_userConfig'
|
|
20
21
|
])
|
|
22
|
+
|
|
23
|
+
const tmAPIs = new Set([
|
|
24
|
+
'GM_getTab', 'GM_getTabs', 'GM_saveTab', 'GM_webRequest',
|
|
25
|
+
'GM.getTab', 'GM.getTabs', 'GM.saveTab', 'GM.webRequest'
|
|
26
|
+
])
|
|
27
|
+
|
|
28
|
+
export const allAPIs = new Set([...gmLegacyAPIs, ...gmModernAPIs, ...scAPIs])
|
|
29
|
+
|
|
30
|
+
export function supportsAPI({ manager, api }) {
|
|
31
|
+
return !allAPIs.has(api) ? false
|
|
32
|
+
: api.startsWith('CAT') ? manager == 'scriptcat'
|
|
33
|
+
: tmAPIs.has(api) ? manager == 'tampermonkey'
|
|
34
|
+
: true
|
|
35
|
+
|
|
36
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
export const universalHeaders = new Set([
|
|
2
|
+
'antifeature', 'author', 'compatible', 'connect', 'contributionAmount', 'contributionURL', 'copyright',
|
|
3
|
+
'defaulticon', 'description', 'downloadURL', 'exclude', 'exclude-match', 'grant', 'homepage', 'homepageURL',
|
|
4
|
+
'icon', 'icon64', 'include', 'license', 'match', 'name', 'namespace', 'noframes', 'require', 'resource',
|
|
5
|
+
'run-at', 'source', 'supportURL', 'unwrap', 'updateURL', 'version', 'website'
|
|
6
|
+
])
|
|
7
|
+
|
|
8
|
+
export const managerHeaders = new Map([
|
|
9
|
+
['inject-into', ['violentmonkey']],
|
|
10
|
+
['top-level-await', ['violentmonkey']],
|
|
11
|
+
['user-agent', ['tampermonkey']],
|
|
12
|
+
['run-in', ['tampermonkey']],
|
|
13
|
+
['sandbox', ['tampermonkey']],
|
|
14
|
+
['webRequest', ['tampermonkey']],
|
|
15
|
+
['nocompat', ['tampermonkey']]
|
|
16
|
+
])
|
|
17
|
+
|
|
18
|
+
export const localizedHeaders = ['name', 'description', 'antifeature']
|
|
19
|
+
export const managers = ['tampermonkey', 'violentmonkey', 'greasemonkey']
|
|
20
|
+
export const validHeaders = new Map()
|
|
21
|
+
|
|
22
|
+
validHeaders.set('all', new Set([...universalHeaders, ...managerHeaders.keys()]))
|
|
23
|
+
|
|
24
|
+
for (const manager of managers) {
|
|
25
|
+
const specific = [...managerHeaders]
|
|
26
|
+
.filter(([, supportedBy]) => supportedBy.includes(manager))
|
|
27
|
+
.map(([header]) => header)
|
|
28
|
+
validHeaders.set(manager, new Set([...universalHeaders, ...specific]))
|
|
29
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
|
+
import aliases from './data/deprecations.js'
|
|
2
|
+
|
|
3
|
+
import metaOrder from './rules/meta-order.js'
|
|
1
4
|
import metaSpacing from './rules/meta-spacing.js'
|
|
2
5
|
import noDuplicateKeys from './rules/no-duplicate-keys.js'
|
|
6
|
+
import noInvalidHeaders from './rules/no-invalid-headers.js'
|
|
3
7
|
import noMissingGrants from './rules/no-missing-grants.js'
|
|
8
|
+
import noMissingResources from './rules/no-missing-resources.js'
|
|
4
9
|
import noUnusedGrants from './rules/no-unused-grants.js'
|
|
5
10
|
import noUnusedResources from './rules/no-unused-resources.js'
|
|
6
11
|
import preferMatch from './rules/prefer-match.js'
|
|
12
|
+
import requireBlankLineAfterMetaBlock from './rules/require-blank-line-after-meta-block.js'
|
|
7
13
|
import requireHomepagePair from './rules/require-homepage-pair.js'
|
|
8
14
|
import requireMatchingLocalizedFields from './rules/require-matching-localized-fields.js'
|
|
9
|
-
import requireMetaNewline from './rules/require-meta-newline.js'
|
|
10
15
|
import requireRequiredFields from './rules/require-required-fields.js'
|
|
11
16
|
import requireSpaceBeforeMetaKey from './rules/require-space-before-meta-key.js'
|
|
17
|
+
import requireSRIhashing from './rules/require-sri-hashing.js'
|
|
12
18
|
import requireUpdateDownloadPair from './rules/require-update-download-pair.js'
|
|
13
19
|
import requireUserJsFilename from './rules/require-user-js-filename.js'
|
|
14
20
|
import validFieldValues from './rules/valid-field-values.js'
|
|
@@ -16,17 +22,21 @@ import validGrants from './rules/valid-grants.js'
|
|
|
16
22
|
|
|
17
23
|
const plugin = {
|
|
18
24
|
rules: {
|
|
25
|
+
'meta-order': metaOrder,
|
|
19
26
|
'meta-spacing': metaSpacing,
|
|
20
27
|
'no-duplicate-keys': noDuplicateKeys,
|
|
28
|
+
'no-invalid-headers': noInvalidHeaders,
|
|
21
29
|
'no-missing-grants': noMissingGrants,
|
|
22
30
|
'no-unused-grants': noUnusedGrants,
|
|
31
|
+
'no-missing-resources': noMissingResources,
|
|
23
32
|
'no-unused-resources': noUnusedResources,
|
|
24
33
|
'prefer-match': preferMatch,
|
|
34
|
+
'require-blank-line-after-meta-block': requireBlankLineAfterMetaBlock,
|
|
25
35
|
'require-homepage-pair': requireHomepagePair,
|
|
26
36
|
'require-matching-localized-fields': requireMatchingLocalizedFields,
|
|
27
|
-
'require-meta-newline': requireMetaNewline,
|
|
28
37
|
'require-required-fields': requireRequiredFields,
|
|
29
38
|
'require-space-before-meta-key': requireSpaceBeforeMetaKey,
|
|
39
|
+
'require-sri-hashing': requireSRIhashing,
|
|
30
40
|
'require-update-download-pair': requireUpdateDownloadPair,
|
|
31
41
|
'require-user-js-filename': requireUserJsFilename,
|
|
32
42
|
'valid-field-values': validFieldValues,
|
|
@@ -34,45 +44,59 @@ const plugin = {
|
|
|
34
44
|
}
|
|
35
45
|
}
|
|
36
46
|
|
|
47
|
+
const docsURL = 'https://codeberg.org/adamlui/eslint-plugin-greasemonkey/#'
|
|
48
|
+
for (const [ruleId, rule] of Object.entries(plugin.rules))
|
|
49
|
+
rule.meta.docs.url = docsURL + ruleId
|
|
50
|
+
|
|
51
|
+
// Generate deprecated aliases
|
|
52
|
+
for (const [oldName, newName] of Object.entries(aliases.rules)) { /* eslint-disable security/detect-object-injection */
|
|
53
|
+
const newRule = plugin.rules[newName]
|
|
54
|
+
plugin.rules[oldName] = {
|
|
55
|
+
...newRule,
|
|
56
|
+
meta: {
|
|
57
|
+
...newRule.meta,
|
|
58
|
+
docs: { ...newRule.meta.docs, recommended: false },
|
|
59
|
+
deprecated: { message: `Use \`${newName}\` instead.`, replacedBy: [{ rule: { name: newName }}] }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
} /* eslint-enable security/detect-object-injection */
|
|
63
|
+
|
|
37
64
|
plugin.configs = {
|
|
38
65
|
recommended: {
|
|
39
66
|
plugins: { greasemonkey: plugin },
|
|
40
67
|
rules: {
|
|
41
68
|
'greasemonkey/meta-spacing': 'error',
|
|
42
69
|
'greasemonkey/no-duplicate-keys': 'error',
|
|
70
|
+
'greasemonkey/no-invalid-headers': 'error',
|
|
43
71
|
'greasemonkey/no-missing-grants': 'error',
|
|
44
72
|
'greasemonkey/no-unused-grants': 'error',
|
|
73
|
+
'greasemonkey/no-missing-resources': 'error',
|
|
45
74
|
'greasemonkey/no-unused-resources': 'error',
|
|
46
75
|
'greasemonkey/prefer-match': 'error',
|
|
76
|
+
'greasemonkey/require-blank-line-after-meta-block': 'error',
|
|
47
77
|
'greasemonkey/require-matching-localized-fields': 'error',
|
|
48
|
-
'greasemonkey/require-meta-newline': 'error',
|
|
49
78
|
'greasemonkey/require-required-fields': 'error',
|
|
50
79
|
'greasemonkey/require-space-before-meta-key': 'error',
|
|
51
80
|
'greasemonkey/require-user-js-filename': 'error',
|
|
52
81
|
'greasemonkey/valid-field-values': 'error',
|
|
53
82
|
'greasemonkey/valid-grants': 'error'
|
|
54
83
|
}
|
|
55
|
-
}
|
|
56
|
-
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
plugin.configs.strict = {
|
|
88
|
+
plugins: { greasemonkey: plugin },
|
|
89
|
+
rules: Object.fromEntries(Object.keys(plugin.rules).map(ruleId => [`greasemonkey/${ruleId}`, 'error']))
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
for (const manager of ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat'])
|
|
93
|
+
plugin.configs[manager] = { // eslint-disable-line security/detect-object-injection
|
|
57
94
|
plugins: { greasemonkey: plugin },
|
|
58
95
|
rules: {
|
|
59
|
-
|
|
60
|
-
'greasemonkey/no-
|
|
61
|
-
'greasemonkey/
|
|
62
|
-
'greasemonkey/no-unused-grants': 'error',
|
|
63
|
-
'greasemonkey/no-unused-resources': 'error',
|
|
64
|
-
'greasemonkey/prefer-match': 'error',
|
|
65
|
-
'greasemonkey/require-homepage-pair': 'error',
|
|
66
|
-
'greasemonkey/require-matching-localized-fields': 'error',
|
|
67
|
-
'greasemonkey/require-meta-newline': 'error',
|
|
68
|
-
'greasemonkey/require-required-fields': 'error',
|
|
69
|
-
'greasemonkey/require-space-before-meta-key': 'error',
|
|
70
|
-
'greasemonkey/require-update-download-pair': 'error',
|
|
71
|
-
'greasemonkey/require-user-js-filename': 'error',
|
|
72
|
-
'greasemonkey/valid-field-values': 'error',
|
|
73
|
-
'greasemonkey/valid-grants': 'error'
|
|
96
|
+
...plugin.configs.recommended.rules,
|
|
97
|
+
'greasemonkey/no-invalid-headers': ['error', { target: manager }],
|
|
98
|
+
'greasemonkey/valid-grants': ['error', { target: manager }]
|
|
74
99
|
}
|
|
75
100
|
}
|
|
76
|
-
}
|
|
77
101
|
|
|
78
102
|
export { plugin as default, plugin as greasemonkey }
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const defaultOrder = [
|
|
4
|
+
'name', 'description', 'author', 'namespace', 'version', 'license', 'icon', 'icon64',
|
|
5
|
+
'compatible', 'match', 'exclude', 'include', 'connect', 'require', 'resource', 'grant',
|
|
6
|
+
'noframes', 'downloadURL', 'updateURL', 'homepageURL', 'supportURL', 'contributionURL'
|
|
7
|
+
]
|
|
8
|
+
|
|
9
|
+
function findViolation(entries, keys) {
|
|
10
|
+
const ranks = new Map(keys.map((key, index) => [key, index])),
|
|
11
|
+
known = entries.map(entry => {
|
|
12
|
+
const key = entry.key.split(':', 1)[0]
|
|
13
|
+
return { entry, key, rank: ranks.get(key) }
|
|
14
|
+
}).filter(item => item.rank !== undefined)
|
|
15
|
+
for (let i = 1; i < known.length; i++) // eslint-disable-next-line security/detect-object-injection
|
|
16
|
+
if (known[i - 1].rank > known[i].rank) return [known[i - 1], known[i]]
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function sortEntries(entries, keys) {
|
|
20
|
+
const ranks = new Map(keys.map((key, index) => [key, index]))
|
|
21
|
+
return entries.toSorted((a, b) => {
|
|
22
|
+
const aRank = ranks.get(a.key.split(':', 1)[0]),
|
|
23
|
+
bRank = ranks.get(b.key.split(':', 1)[0])
|
|
24
|
+
return (aRank ?? Infinity) - (bRank ?? Infinity)
|
|
25
|
+
})
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export default {
|
|
29
|
+
meta: {
|
|
30
|
+
type: 'layout',
|
|
31
|
+
docs: {
|
|
32
|
+
description: 'Enforce an order for metadata keys',
|
|
33
|
+
category: 'Stylistic Issues',
|
|
34
|
+
recommended: false
|
|
35
|
+
},
|
|
36
|
+
fixable: 'code',
|
|
37
|
+
schema: [{
|
|
38
|
+
type: 'array',
|
|
39
|
+
items: { type: 'string' },
|
|
40
|
+
description: 'Metadata keys in preferred order; unspecified keys are ignored'
|
|
41
|
+
}],
|
|
42
|
+
defaultOptions: [[]],
|
|
43
|
+
messages: { wrongOrder: '@{{first}} should appear before @{{second}}' }
|
|
44
|
+
},
|
|
45
|
+
|
|
46
|
+
create(context) {
|
|
47
|
+
const custom = context.options[0] ?? [],
|
|
48
|
+
master = custom.length ? defaultOrder.filter(key => !custom.includes(key)) : defaultOrder
|
|
49
|
+
return {
|
|
50
|
+
Program() {
|
|
51
|
+
const sourceCode = context.sourceCode,
|
|
52
|
+
entries = extractMetadata(sourceCode),
|
|
53
|
+
violation = findViolation(entries, master) ?? (custom.length && findViolation(entries, custom))
|
|
54
|
+
if (!violation) return
|
|
55
|
+
const [first, second] = violation
|
|
56
|
+
const sorted = sortEntries(sortEntries(entries, master), custom)
|
|
57
|
+
const replacements = // eslint-disable-next-line security/detect-object-injection
|
|
58
|
+
new Map(sorted.map((entry, idx) => [entries[idx], sourceCode.getText(entry.comment)]))
|
|
59
|
+
const start = entries[0].comment.range[0]
|
|
60
|
+
const end = entries.at(-1).comment.range[1]
|
|
61
|
+
let output = '', position = start
|
|
62
|
+
for (const entry of entries) {
|
|
63
|
+
output += sourceCode.text.slice(position, entry.comment.range[0])
|
|
64
|
+
output += replacements.get(entry)
|
|
65
|
+
position = entry.comment.range[1]
|
|
66
|
+
}
|
|
67
|
+
output += sourceCode.text.slice(position, end)
|
|
68
|
+
context.report({
|
|
69
|
+
node: second.entry.comment, messageId: 'wrongOrder',
|
|
70
|
+
data: { first: second.key, second: first.key },
|
|
71
|
+
fix(fixer) { return fixer.replaceTextRange([start, end], output) }
|
|
72
|
+
})
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -21,13 +21,13 @@ export default {
|
|
|
21
21
|
return {
|
|
22
22
|
Program() {
|
|
23
23
|
const metaEntries = extractMetadata(context.sourceCode),
|
|
24
|
-
|
|
24
|
+
seenEntries = new Set()
|
|
25
25
|
for (const entry of metaEntries) {
|
|
26
26
|
const baseKey = entry.key.split(':')[0]
|
|
27
27
|
if (!entry.key.includes(':') && multiValKeys.has(baseKey)) continue
|
|
28
|
-
if (
|
|
28
|
+
if (seenEntries.has(entry.key))
|
|
29
29
|
context.report({ node: entry.comment, messageId: 'dupeKey', data: { key: entry.key }})
|
|
30
|
-
else
|
|
30
|
+
else seenEntries.add(entry.key)
|
|
31
31
|
}
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
import { managerHeaders, managers, localizedHeaders, validHeaders } from '../data/headers.js'
|
|
3
|
+
|
|
4
|
+
function parseAllowedPattern(entry) {
|
|
5
|
+
if (typeof entry != 'string' || !entry.startsWith('/')) return null
|
|
6
|
+
const lastSlash = entry.lastIndexOf('/')
|
|
7
|
+
if (lastSlash == 0) return null
|
|
8
|
+
try { /* eslint-disable-next-line security/detect-non-literal-regexp */
|
|
9
|
+
return new RegExp(entry.slice(1, lastSlash), entry.slice(lastSlash +1))
|
|
10
|
+
} catch { return null }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export default {
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Disallow invalid metadata headers',
|
|
18
|
+
category: 'Possible Errors',
|
|
19
|
+
recommended: true
|
|
20
|
+
},
|
|
21
|
+
schema: [{
|
|
22
|
+
type: 'object',
|
|
23
|
+
properties: {
|
|
24
|
+
target: {
|
|
25
|
+
anyOf: [
|
|
26
|
+
{ type: 'string', enum: ['all', ...managers] },
|
|
27
|
+
{ type: 'array', items: { type: 'string', enum: managers }, minItems: 1, uniqueItems: true }
|
|
28
|
+
],
|
|
29
|
+
description: 'Manager(s) whose headers to validate against (default: all)'
|
|
30
|
+
},
|
|
31
|
+
allowedHeaders: {
|
|
32
|
+
anyOf: [{ type: 'string' }, { type: 'array' }, {}],
|
|
33
|
+
description: 'Additional headers: string, RegExp, or array mixing both'
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
additionalProperties: false
|
|
37
|
+
}],
|
|
38
|
+
defaultOptions: [{ allowedHeaders: [] }],
|
|
39
|
+
messages: {
|
|
40
|
+
invalidHeader: 'Invalid header: "@{{header}}"',
|
|
41
|
+
unsupportedHeader: '"@{{header}}" is not supported by {{target}} (only by {{supporting}})'
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
|
|
45
|
+
create(context) {
|
|
46
|
+
const options = context.options[0] || {},
|
|
47
|
+
rawTarget = options.target ?? 'all',
|
|
48
|
+
targets = Array.isArray(rawTarget) ? rawTarget : [rawTarget],
|
|
49
|
+
allowedList = [options.allowedHeaders ?? []].flat(),
|
|
50
|
+
allowedExact = new Set(),
|
|
51
|
+
allowedRegexes = [],
|
|
52
|
+
validSet = targets.includes('all') ? validHeaders.get('all')
|
|
53
|
+
: new Set(targets.flatMap(target => [...validHeaders.get(target)]))
|
|
54
|
+
for (const entry of allowedList) {
|
|
55
|
+
if (entry instanceof RegExp) allowedRegexes.push(entry)
|
|
56
|
+
else if (typeof entry == 'string') {
|
|
57
|
+
const parsed = parseAllowedPattern(entry)
|
|
58
|
+
if (parsed) allowedRegexes.push(parsed)
|
|
59
|
+
else allowedExact.add(entry)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
Program() {
|
|
64
|
+
const metaEntries = extractMetadata(context.sourceCode)
|
|
65
|
+
for (const entry of metaEntries) {
|
|
66
|
+
if (allowedExact.has(entry.key)) continue
|
|
67
|
+
if (allowedRegexes.some(re => re.test(entry.key))) continue
|
|
68
|
+
if (localizedHeaders.includes(entry.key.split(':')[0])) continue
|
|
69
|
+
if (validSet.has(entry.key)) continue
|
|
70
|
+
const supporting = managerHeaders.get(entry.key)
|
|
71
|
+
if (supporting)
|
|
72
|
+
context.report({ node: entry.comment, messageId: 'unsupportedHeader',
|
|
73
|
+
data: { header: entry.key, target: targets.join(', '), supporting: supporting.join(', ') }})
|
|
74
|
+
else context.report({ node: entry.comment, messageId: 'invalidHeader', data: { header: entry.key }})
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { extractMetadata } from '../utils.js'
|
|
2
|
-
import { gmLegacyAPIs, gmModernAPIs, scAPIs } from '../apis.js'
|
|
2
|
+
import { gmLegacyAPIs, gmModernAPIs, scAPIs } from '../data/apis.js'
|
|
3
3
|
|
|
4
4
|
const apis = new Set([...gmLegacyAPIs, ...gmModernAPIs, ...scAPIs]),
|
|
5
5
|
grantlessAPIs = new Set(['GM_info', 'GM.info']) // always available, no grant required
|
|
@@ -22,8 +22,7 @@ export default {
|
|
|
22
22
|
return {
|
|
23
23
|
Identifier(node) { record(node.name, node) },
|
|
24
24
|
MemberExpression(node) {
|
|
25
|
-
const path = []
|
|
26
|
-
let current = node
|
|
25
|
+
const path = [] ; let current = node
|
|
27
26
|
while (current.type == 'MemberExpression') {
|
|
28
27
|
if (current.property.type != 'Identifier') return
|
|
29
28
|
path.unshift(current.property.name)
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { extractMetadata, resolveTemplate } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const resourceGetterNames = new Set(['GM_getResourceText', 'GM_getResourceURL']),
|
|
4
|
+
re = { specialChars: /[.*+?^${}()|[\]\\]/g, whitespace: /\s+/ }
|
|
5
|
+
|
|
6
|
+
export default {
|
|
7
|
+
meta: {
|
|
8
|
+
type: 'problem',
|
|
9
|
+
docs: {
|
|
10
|
+
description: 'Disallow @resource references without a matching declaration',
|
|
11
|
+
category: 'Possible Errors',
|
|
12
|
+
recommended: true
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
missingResource: 'Missing @resource: "{{name}}"',
|
|
17
|
+
noMatchingResource: 'No declared @resource matches `{{pattern}}`'
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
const usedRefs = []
|
|
23
|
+
function record(node, arg) {
|
|
24
|
+
if (arg?.type == 'Literal' && typeof arg.value == 'string')
|
|
25
|
+
usedRefs.push({ names: [arg.value], pattern: null, node })
|
|
26
|
+
else if (arg?.type == 'TemplateLiteral') {
|
|
27
|
+
const resolved = resolveTemplate(arg)
|
|
28
|
+
if (resolved) usedRefs.push({ names: resolved, pattern: null, node })
|
|
29
|
+
else usedRefs.push({
|
|
30
|
+
names: null,
|
|
31
|
+
pattern: new RegExp( // eslint-disable-line security/detect-non-literal-regexp
|
|
32
|
+
`^${arg.quasis.map(q => q.value.raw.replace(re.specialChars, '\\$&')).join('.*')}$`),
|
|
33
|
+
node
|
|
34
|
+
})
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return {
|
|
38
|
+
CallExpression(node) {
|
|
39
|
+
if (!resourceGetterNames.has(node.callee.name)) return
|
|
40
|
+
record(node, node.arguments[0])
|
|
41
|
+
},
|
|
42
|
+
'Program:exit'() {
|
|
43
|
+
if (!usedRefs.length) return
|
|
44
|
+
const declared = new Set(
|
|
45
|
+
extractMetadata(context.sourceCode)
|
|
46
|
+
.filter(entry => entry.key == 'resource')
|
|
47
|
+
.map(entry => entry.value.trim().split(re.whitespace)[0])
|
|
48
|
+
)
|
|
49
|
+
for (const ref of usedRefs) {
|
|
50
|
+
if (ref.names) {
|
|
51
|
+
for (const name of ref.names)
|
|
52
|
+
if (!declared.has(name))
|
|
53
|
+
context.report({ node: ref.node, messageId: 'missingResource', data: { name }})
|
|
54
|
+
} else if (![...declared].some(name => ref.pattern.test(name)))
|
|
55
|
+
context.report({ node: ref.node, messageId: 'noMatchingResource',
|
|
56
|
+
data: { pattern: ref.pattern.source }})
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -1,16 +1,14 @@
|
|
|
1
|
-
import { extractMetadata } from '../utils.js'
|
|
2
|
-
|
|
3
1
|
export default {
|
|
4
2
|
meta: {
|
|
5
3
|
type: 'layout',
|
|
6
4
|
docs: {
|
|
7
|
-
description: 'Require a
|
|
5
|
+
description: 'Require a blank line between the metadata and code',
|
|
8
6
|
category: 'Stylistic Issues',
|
|
9
7
|
recommended: true
|
|
10
8
|
},
|
|
11
9
|
fixable: 'whitespace',
|
|
12
10
|
schema: [],
|
|
13
|
-
messages: {
|
|
11
|
+
messages: { missingBlankLine: 'Missing blank line after metadata block' }
|
|
14
12
|
},
|
|
15
13
|
|
|
16
14
|
create(context) {
|
|
@@ -18,16 +16,14 @@ export default {
|
|
|
18
16
|
Program() {
|
|
19
17
|
const sourceCode = context.sourceCode,
|
|
20
18
|
comments = sourceCode.getAllComments(),
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (!closingComment) return
|
|
19
|
+
hasOpening = comments.some(comment => comment.value.trim() == '==UserScript=='),
|
|
20
|
+
closingComment = comments.find(comment => comment.value.trim() == '==/UserScript==')
|
|
21
|
+
if (!hasOpening || !closingComment) return
|
|
25
22
|
const tokenAfter = sourceCode.getTokenAfter(closingComment)
|
|
26
23
|
if (!tokenAfter) return
|
|
27
24
|
if (tokenAfter.loc.start.line - closingComment.loc.end.line < 2)
|
|
28
25
|
context.report({
|
|
29
|
-
node: closingComment,
|
|
30
|
-
messageId: 'missingNewline',
|
|
26
|
+
node: closingComment, messageId: 'missingBlankLine',
|
|
31
27
|
fix(fixer) { return fixer.insertTextAfter(closingComment, '\n') }
|
|
32
28
|
})
|
|
33
29
|
}
|
|
@@ -18,8 +18,8 @@ export default {
|
|
|
18
18
|
Program() {
|
|
19
19
|
const metaEntries = extractMetadata(context.sourceCode),
|
|
20
20
|
homepageEntry = metaEntries.find(entry => entry.key == 'homepage'),
|
|
21
|
-
|
|
22
|
-
if (homepageEntry && !
|
|
21
|
+
homepageURLentry = metaEntries.find(entry => entry.key == 'homepageURL')
|
|
22
|
+
if (homepageEntry && !homepageURLentry)
|
|
23
23
|
context.report({
|
|
24
24
|
node: homepageEntry.comment, messageId: 'missingPair',
|
|
25
25
|
data: { missing: 'homepageURL', present: 'homepage' },
|
|
@@ -28,13 +28,13 @@ export default {
|
|
|
28
28
|
`\n// @homepageURL ${homepageEntry.value}`)
|
|
29
29
|
}
|
|
30
30
|
})
|
|
31
|
-
else if (
|
|
31
|
+
else if (homepageURLentry && !homepageEntry)
|
|
32
32
|
context.report({
|
|
33
|
-
node:
|
|
33
|
+
node: homepageURLentry.comment, messageId: 'missingPair',
|
|
34
34
|
data: { missing: 'homepage', present: 'homepageURL' },
|
|
35
35
|
fix(fixer) {
|
|
36
|
-
return fixer.
|
|
37
|
-
|
|
36
|
+
return fixer.insertTextBefore(homepageURLentry.comment,
|
|
37
|
+
`// @homepage ${homepageURLentry.value}\n`)
|
|
38
38
|
}
|
|
39
39
|
})
|
|
40
40
|
}
|
|
@@ -12,9 +12,11 @@ export default {
|
|
|
12
12
|
type: 'object',
|
|
13
13
|
properties: {
|
|
14
14
|
ignoreLanguages: {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
anyOf: [
|
|
16
|
+
{ type: 'string' },
|
|
17
|
+
{ type: 'array', items: { type: 'string' }, minItems: 1, uniqueItems: true }
|
|
18
|
+
],
|
|
19
|
+
description: 'Language code(s) to ignore'
|
|
18
20
|
}
|
|
19
21
|
},
|
|
20
22
|
additionalProperties: false
|
|
@@ -25,7 +27,7 @@ export default {
|
|
|
25
27
|
|
|
26
28
|
create(context) {
|
|
27
29
|
const options = context.options[0] || {},
|
|
28
|
-
ignoreLanguages = options.ignoreLanguages
|
|
30
|
+
ignoreLanguages = [options.ignoreLanguages ?? []].flat()
|
|
29
31
|
return {
|
|
30
32
|
Program() {
|
|
31
33
|
const metaEntries = extractMetadata(context.sourceCode),
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { extractMetadata } from '../utils.js'
|
|
2
|
+
|
|
3
|
+
const re = { hashFragment: /#([^#]+)$/, sriHash: /^(sha1|sha256|sha384|sha512|md5)-([a-z0-9+/]+={0,2})$/i }
|
|
4
|
+
|
|
5
|
+
export default {
|
|
6
|
+
meta: {
|
|
7
|
+
type: 'problem',
|
|
8
|
+
docs: {
|
|
9
|
+
description: 'Require SRI hashes on @require and @resource URLs',
|
|
10
|
+
category: 'Possible Errors',
|
|
11
|
+
recommended: false
|
|
12
|
+
},
|
|
13
|
+
schema: [{
|
|
14
|
+
type: 'object',
|
|
15
|
+
description: 'SRI hashing options',
|
|
16
|
+
properties: {
|
|
17
|
+
algorithm: {
|
|
18
|
+
type: 'string',
|
|
19
|
+
description: 'SRI hash algorithm to require',
|
|
20
|
+
enum: ['md5', 'sha1', 'sha256', 'sha384', 'sha512']
|
|
21
|
+
}
|
|
22
|
+
},
|
|
23
|
+
additionalProperties: false
|
|
24
|
+
}],
|
|
25
|
+
defaultOptions: [{ algorithm: 'sha256' }],
|
|
26
|
+
messages: {
|
|
27
|
+
missingHash: 'Missing SRI hash for @{{key}}.',
|
|
28
|
+
invalidHash: 'Invalid SRI hash for @{{key}}.{{detected}} Expected {{expected}}.'
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
create(context) {
|
|
33
|
+
const algorithm = (context.options[0]?.algorithm || 'sha256').toLowerCase()
|
|
34
|
+
return {
|
|
35
|
+
'Program:exit'() {
|
|
36
|
+
for (const entry of extractMetadata(context.sourceCode)) {
|
|
37
|
+
if (entry.key != 'require' && entry.key != 'resource') continue
|
|
38
|
+
const hash = entry.value.trim().match(re.hashFragment)?.[1]
|
|
39
|
+
if (!hash) {
|
|
40
|
+
context.report({ node: entry.comment, messageId: 'missingHash', data: { key: entry.key } })
|
|
41
|
+
continue
|
|
42
|
+
}
|
|
43
|
+
const match = hash.match(re.sriHash)
|
|
44
|
+
if (!match || match[1].toLowerCase() != algorithm)
|
|
45
|
+
context.report({
|
|
46
|
+
node: entry.comment, messageId: 'invalidHash',
|
|
47
|
+
data: {
|
|
48
|
+
key: entry.key,
|
|
49
|
+
detected: match ? ` Detected ${match[1].toLowerCase()};` : '',
|
|
50
|
+
expected: algorithm
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|