eslint-plugin-greasemonkey 1.8.4 → 1.10.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.
Files changed (45) hide show
  1. package/README.md +776 -68
  2. package/docs/LICENSE.md +1 -1
  3. package/package.json +53 -43
  4. package/src/data/apis.js +56 -53
  5. package/src/data/headers.js +65 -31
  6. package/src/data/regex.js +14 -0
  7. package/src/index.d.ts +34 -3
  8. package/src/index.js +5 -80
  9. package/src/rules/meta-order.js +5 -6
  10. package/src/rules/meta-spacing.js +11 -10
  11. package/src/rules/no-duplicate-keys.js +2 -2
  12. package/src/rules/no-insecure-fetch.js +108 -0
  13. package/src/rules/no-insecure-messaging.js +67 -0
  14. package/src/rules/no-invalid-grants.js +12 -10
  15. package/src/rules/no-invalid-headers.js +11 -11
  16. package/src/rules/no-missing-grants.js +6 -8
  17. package/src/rules/no-missing-resources.js +10 -8
  18. package/src/rules/no-sensitive-grants.js +81 -0
  19. package/src/rules/no-unused-grants.js +2 -2
  20. package/src/rules/no-unused-resources.js +10 -8
  21. package/src/rules/prefer-gm-add-style.js +67 -0
  22. package/src/rules/prefer-match.js +4 -3
  23. package/src/rules/require-homepage-pair.js +3 -4
  24. package/src/rules/require-license-header.js +27 -0
  25. package/src/rules/require-matching-localized-fields.js +2 -2
  26. package/src/rules/require-required-fields.js +2 -2
  27. package/src/rules/require-space-before-meta-key.js +4 -5
  28. package/src/rules/require-sri-hashing.js +3 -3
  29. package/src/rules/require-update-download-pair.js +3 -4
  30. package/src/rules/require-user-js-filename.js +2 -2
  31. package/src/rules/valid-field-values.js +7 -7
  32. package/src/rules/valid-icon-headers.js +74 -0
  33. package/src/rules/valid-license-header.js +73 -0
  34. package/src/rules/valid-url-headers.js +114 -0
  35. package/src/rules/valid-version-header.js +83 -0
  36. package/src/utils/api.js +11 -0
  37. package/src/utils/array.js +1 -0
  38. package/src/utils/deprecations.js +76 -0
  39. package/src/utils/init.js +106 -0
  40. package/src/utils/license.js +56 -0
  41. package/src/utils/metadata.js +31 -0
  42. package/src/utils/pkg.js +21 -0
  43. package/src/utils/rules.js +7 -0
  44. package/src/utils/template.js +21 -0
  45. package/src/utils.js +0 -119
@@ -0,0 +1,106 @@
1
+ import deprecations from './deprecations.js'
2
+ import pkg from './pkg.js'
3
+ import rulesUtils from './rules.js'
4
+
5
+ import headers from '../data/headers.js'
6
+ import { rules as deprecationsData } from '../data/deprecations.js'
7
+
8
+ import metaOrder from '../rules/meta-order.js'
9
+ import metaSpacing from '../rules/meta-spacing.js'
10
+ import noDuplicateKeys from '../rules/no-duplicate-keys.js'
11
+ import noInsecureFetch from '../rules/no-insecure-fetch.js'
12
+ import noInsecureMessaging from '../rules/no-insecure-messaging.js'
13
+ import noInvalidGrants from '../rules/no-invalid-grants.js'
14
+ import noInvalidHeaders from '../rules/no-invalid-headers.js'
15
+ import noMissingGrants from '../rules/no-missing-grants.js'
16
+ import noMissingResources from '../rules/no-missing-resources.js'
17
+ import noSensitiveGrants from '../rules/no-sensitive-grants.js'
18
+ import noUnusedGrants from '../rules/no-unused-grants.js'
19
+ import noUnusedResources from '../rules/no-unused-resources.js'
20
+ import preferGMaddStyle from '../rules/prefer-gm-add-style.js'
21
+ import preferMatch from '../rules/prefer-match.js'
22
+ import requireBlankLineAfterMetaBlock from '../rules/require-blank-line-after-meta-block.js'
23
+ import requireHomepagePair from '../rules/require-homepage-pair.js'
24
+ import requireLicenseHeader from '../rules/require-license-header.js'
25
+ import requireMatchingLocalizedFields from '../rules/require-matching-localized-fields.js'
26
+ import requireRequiredFields from '../rules/require-required-fields.js'
27
+ import requireSpaceBeforeMetaKey from '../rules/require-space-before-meta-key.js'
28
+ import requireSRIhashing from '../rules/require-sri-hashing.js'
29
+ import requireUpdateDownloadPair from '../rules/require-update-download-pair.js'
30
+ import requireUserJsFilename from '../rules/require-user-js-filename.js'
31
+ import validFieldValues from '../rules/valid-field-values.js'
32
+ import validIconHeaders from '../rules/valid-icon-headers.js'
33
+ import validLicenseHeader from '../rules/valid-license-header.js'
34
+ import validUrlHeaders from '../rules/valid-url-headers.js'
35
+ import validVersionHeader from '../rules/valid-version-header.js'
36
+
37
+ const pkgName = pkg.getName(),
38
+ namespace = pkgName.replace(/^@[^/]+\//, '').replace(/^eslint-plugin-/, '')
39
+
40
+ export default {
41
+ configs(plugin) {
42
+ const plugins = { [namespace]: plugin },
43
+ rules = plugin.rules,
44
+ createConfig = predicate => ({ plugins, rules: rulesUtils.bundle(rules, predicate, namespace) })
45
+ plugin.configs = {
46
+ recommended: createConfig(rule => rule.meta.docs.recommended),
47
+ strict: createConfig(() => true),
48
+ problem: createConfig(rule => rule.meta.type == 'problem'),
49
+ security: createConfig(rule => rule.meta.docs.security),
50
+ stylistic: createConfig(rule => rule.meta.docs.category == 'Stylistic Issues')
51
+ }
52
+ for (const manager of headers.managers)
53
+ plugin.configs[manager] = { // oxlint-disable-line security/detect-object-injection
54
+ plugins,
55
+ rules: {
56
+ ...plugin.configs.recommended.rules,
57
+ [`${namespace}/no-invalid-grants`]: ['error', { target: manager }],
58
+ [`${namespace}/no-invalid-headers`]: ['error', { target: manager }],
59
+ ...(['tampermonkey', 'scriptcat', 'adguard'].includes(manager) &&
60
+ { [`${namespace}/require-update-download-pair`]: 'error' })
61
+ }
62
+ }
63
+ },
64
+
65
+ deprecations(plugin) {
66
+ for (const entry of Object.entries(deprecationsData))
67
+ deprecations.initAlias(plugin, entry)
68
+ },
69
+
70
+ plugin() { return { meta: { name: pkgName }, rules: {}, configs: {} }},
71
+
72
+ rules(plugin) {
73
+ plugin.rules = {
74
+ 'meta-order': metaOrder,
75
+ 'meta-spacing': metaSpacing,
76
+ 'no-duplicate-keys': noDuplicateKeys,
77
+ 'no-insecure-fetch': noInsecureFetch,
78
+ 'no-insecure-messaging': noInsecureMessaging,
79
+ 'no-invalid-grants': noInvalidGrants,
80
+ 'no-invalid-headers': noInvalidHeaders,
81
+ 'no-missing-grants': noMissingGrants,
82
+ 'no-missing-resources': noMissingResources,
83
+ 'no-sensitive-grants': noSensitiveGrants,
84
+ 'no-unused-grants': noUnusedGrants,
85
+ 'no-unused-resources': noUnusedResources,
86
+ 'prefer-gm-add-style': preferGMaddStyle,
87
+ 'prefer-match': preferMatch,
88
+ 'require-blank-line-after-meta-block': requireBlankLineAfterMetaBlock,
89
+ 'require-homepage-pair': requireHomepagePair,
90
+ 'require-license-header': requireLicenseHeader,
91
+ 'require-matching-localized-fields': requireMatchingLocalizedFields,
92
+ 'require-required-fields': requireRequiredFields,
93
+ 'require-space-before-meta-key': requireSpaceBeforeMetaKey,
94
+ 'require-sri-hashing': requireSRIhashing,
95
+ 'require-update-download-pair': requireUpdateDownloadPair,
96
+ 'require-user-js-filename': requireUserJsFilename,
97
+ 'valid-field-values': validFieldValues,
98
+ 'valid-icon-headers': validIconHeaders,
99
+ 'valid-license-header': validLicenseHeader,
100
+ 'valid-url-headers': validUrlHeaders,
101
+ 'valid-version-header': validVersionHeader
102
+ }
103
+ for (const [ruleId, rule] of Object.entries(plugin.rules))
104
+ rule.meta.docs.url = `https://codeberg.org/adamlui/${pkgName}/#${ruleId}`
105
+ }
106
+ }
@@ -0,0 +1,56 @@
1
+ // oxlint-disable security/detect-non-literal-fs-filename
2
+
3
+ import { createRequire } from 'node:module'
4
+ import fs from 'node:fs'
5
+ import path from 'node:path'
6
+
7
+ import regex from '../data/regex.js'
8
+
9
+ const re = { spdxTag: /SPDX-License-Identifier:\s*([\w.-]+)/i },
10
+ require = createRequire(import.meta.url),
11
+ skipDirs = new Set(['node_modules']),
12
+ spdxIDs = new Set(require('spdx-license-ids')),
13
+ spdxNames = Object.entries(require('spdx-license-list'))
14
+ .map(([id, { name }]) => ({ id, name }))
15
+ .sort((a, b) => b.name.length - a.name.length)
16
+
17
+ function tryReaddir(dir) { try { return fs.readdirSync(dir, { withFileTypes: true }) } catch { return null }}
18
+
19
+ export default {
20
+ find(root) {
21
+ const queue = [root]
22
+ while (queue.length) {
23
+ const dir = queue.shift(), entries = tryReaddir(dir)
24
+ if (!entries) continue
25
+ for (const entry of entries) {
26
+ if (entry.name.startsWith('.') || skipDirs.has(entry.name)) continue
27
+ if (entry.isFile() && regex.filename.license.test(entry.name)) return path.join(dir, entry.name)
28
+ if (entry.isDirectory()) queue.push(path.join(dir, entry.name))
29
+ }
30
+ }
31
+ return null
32
+ },
33
+
34
+ spdx: {
35
+ appendTag(fixer, text, id) {
36
+ const nl = text.includes('\r\n') ? '\r\n' : '\n',
37
+ trailer = `${text.endsWith(nl) ? '' : nl}${nl}SPDX-License-Identifier: ${id}${nl}`
38
+ return fixer.insertTextAfterRange([0, text.length], trailer)
39
+ },
40
+
41
+ find(text) {
42
+ const normalizedTxt = text.replace(/\s+/g, ' ')
43
+ let best = null
44
+ for (const { id, name } of spdxNames) {
45
+ const idx = normalizedTxt.indexOf(name)
46
+ if (idx == -1) continue
47
+ if (!best || idx < best.idx || (idx == best.idx && name.length > best.name.length))
48
+ best = { id, idx, name }
49
+ }
50
+ return best?.id ?? null
51
+ },
52
+
53
+ isValid(id) { return spdxIDs.has(id) },
54
+ parse(text) { return text.match(re.spdxTag)?.[1] ?? null }
55
+ }
56
+ }
@@ -0,0 +1,31 @@
1
+ import regex from '../data/regex.js'
2
+
3
+ export default {
4
+ createBlock(...entries) {
5
+ const lines = ['// ==UserScript==']
6
+ for (const entry of entries)
7
+ lines.push(typeof entry == 'string'
8
+ ? `// ${entry}`
9
+ : `// @${entry.key}${entry.value ? ` ${entry.value}` : ''}`)
10
+ lines.push('// ==/UserScript==')
11
+ return lines.join('\n') + '\n'
12
+ },
13
+
14
+ extract(sourceCode) {
15
+ const comments = sourceCode.getAllComments(),
16
+ metaEntries = []
17
+ let inMetaBlock
18
+ for (const comment of comments) {
19
+ const text = comment.value.trim()
20
+ if (!inMetaBlock && text == '==UserScript==') inMetaBlock = true
21
+ else if (inMetaBlock && text == '==/UserScript==') break
22
+ else if (inMetaBlock && text.startsWith('@')) { // capture entry
23
+ const spaceIdx = text.search(regex.whitespace.all),
24
+ key = ( spaceIdx == -1 ? text.slice(1) : text.slice(1, spaceIdx) ).trim(),
25
+ value = spaceIdx == -1 ? '' : text.slice(spaceIdx +1).trim()
26
+ if (key) metaEntries.push({ key, value, comment, line: comment.loc.start.line })
27
+ }
28
+ }
29
+ return metaEntries
30
+ }
31
+ }
@@ -0,0 +1,21 @@
1
+ import { fileURLToPath } from 'node:url'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+
5
+ export default {
6
+ getName() { // oxlint-disable security/detect-non-literal-fs-filename
7
+ let current = path.dirname(fileURLToPath(import.meta.url))
8
+ while (current) {
9
+ const pkgPath = path.join(current, 'package.json')
10
+ if (fs.existsSync(pkgPath))
11
+ try {
12
+ const { name } = JSON.parse(fs.readFileSync(pkgPath, 'utf8'))
13
+ if (name) return name
14
+ } catch { /* malformed, keep walking */ }
15
+ const parent = path.dirname(current)
16
+ if (parent === current) return null
17
+ current = parent
18
+ }
19
+ return null
20
+ } // oxlint-enable security/detect-non-literal-fs-filename
21
+ }
@@ -0,0 +1,7 @@
1
+ export default {
2
+ bundle(rules, predicate, pluginName) {
3
+ return Object.fromEntries(Object.entries(rules)
4
+ .filter(([, rule]) => !rule.meta.deprecated && predicate(rule))
5
+ .map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
6
+ }
7
+ }
@@ -0,0 +1,21 @@
1
+ export default {
2
+ resolve(node) {
3
+ function walk(n) {
4
+ if (n.type == 'ConditionalExpression') {
5
+ const branches = [n.consequent, n.alternate].map(walk)
6
+ return branches.every(Boolean) ? branches.flat() : null
7
+ } else if (n.type == 'Literal' && ['number', 'string'].includes(typeof n.value))
8
+ return [String(n.value)]
9
+ else if (n.type == 'TemplateLiteral')
10
+ return n.quasis.reduce((combos, quasi, idx) => {
11
+ if (!combos) return null
12
+ const withQuasi = combos.map(combo => combo + quasi.value.raw)
13
+ if (idx >= n.expressions.length) return withQuasi
14
+ const vals = walk(n.expressions[idx]) // oxlint-disable-line security/detect-object-injection
15
+ return vals ? withQuasi.flatMap(combo => vals.map(val => combo + val)) : null
16
+ }, [''])
17
+ else return null
18
+ }
19
+ return walk(node)
20
+ }
21
+ }
package/src/utils.js DELETED
@@ -1,119 +0,0 @@
1
- const re = { whitespace: /\s/ }
2
-
3
- export function bundleRules(rules, predicate, pluginName) {
4
- return Object.fromEntries(Object.entries(rules)
5
- .filter(([, rule]) => predicate(rule))
6
- .map(([ruleId]) => [`${pluginName}/${ruleId}`, 'error']))
7
- }
8
-
9
- export function extractMetadata(sourceCode) {
10
- const comments = sourceCode.getAllComments(),
11
- metaEntries = []
12
- let inMetaBlock
13
- for (const comment of comments) {
14
- const text = comment.value.trim()
15
- if (!inMetaBlock && text == '==UserScript==') inMetaBlock = true
16
- else if (inMetaBlock && text == '==/UserScript==') break
17
- else if (inMetaBlock && text.startsWith('@')) { // capture entry
18
- const spaceIdx = text.search(re.whitespace),
19
- key = ( spaceIdx == -1 ? text.slice(1) : text.slice(1, spaceIdx) ).trim(),
20
- value = spaceIdx == -1 ? '' : text.slice(spaceIdx +1).trim()
21
- if (key) metaEntries.push({ key, value, comment, line: comment.loc.start.line })
22
- }
23
- }
24
- return metaEntries
25
- }
26
-
27
- export function initDeprecatedAlias(plugin, [oldName, alias]) { /* eslint-disable security/detect-object-injection */
28
- const routes = typeof alias == 'string' ? { [alias]: null } // 'old-rule': 'new-rule'
29
- : Array.isArray(alias) ? Object.fromEntries(alias.map(entry =>
30
- typeof entry == 'string' ? [entry, null] // ['new-rule-1', 'new-rule-2']
31
- : Object.entries(entry)[0])) // [{ 'new-rule': { 'old-opt': 'new-opt' }}]
32
- : alias // { 'new-rule': null | { 'new-opt': default } | { 'old-val': 'new-val' } | { ..., 'new-opt': default }}
33
- const targetNames = Object.keys(routes),
34
- isRename = !targetNames.includes(oldName),
35
- originals = Object.fromEntries(targetNames.map(name => [name, plugin.rules[name]])),
36
- source = originals[oldName] ?? originals[targetNames[0]]
37
- plugin.rules[oldName] = {
38
- meta: {
39
- ...source.meta,
40
- defaultOptions: [],
41
- schema: [{ type: ['object', 'array', 'string', 'number', 'boolean', 'null'] }],
42
- docs: { ...source.meta.docs, recommended: isRename ? false : source.meta.docs.recommended },
43
- messages: {
44
- ...source.meta.messages,
45
- deprecatedOption: `'{{old}}' is deprecated. Use '{{new}}' instead.`
46
- },
47
- ...(isRename ? { deprecated: {
48
- message: `Use ${targetNames.map(name => `\`${name}\``).join(', ')} instead.`,
49
- replacedBy: targetNames.map(name => ({ rule: { name }}))
50
- }} : {})
51
- },
52
- create(context) {
53
- const opts = context.options[0],
54
- isObj = opts?.constructor === Object
55
- return Object.assign({}, ...Object.entries(routes).map(([targetName, mapping]) => {
56
- let newOpts = opts, deprecation
57
- if (mapping) {
58
- const entries = Object.entries(mapping),
59
- namedRemap = entries.every(([, val]) => typeof val == 'string'),
60
- wrapKey = namedRemap && entries.length > 1 ? entries.at(-1)[0] : null,
61
- valMap = wrapKey ? Object.fromEntries(entries.slice(0, -1)) : mapping
62
- if (namedRemap) {
63
- if (typeof opts == 'string') { // bare old val → remap, optionally wrapped
64
- if (valMap[opts]) deprecation = { old: opts, new: valMap[opts] }
65
- const remapped = valMap[opts] ?? opts
66
- newOpts = wrapKey ? { [wrapKey]: remapped } : remapped
67
- } else if (wrapKey) { // object opts (or defaults) → remap inner val, warn if deprecated
68
- const raw = isObj ? (opts[wrapKey] ?? mapping[wrapKey]) : mapping[wrapKey],
69
- remapped = valMap[raw]
70
- if (remapped) deprecation = { old: raw, new: remapped }
71
- newOpts = { [wrapKey]: remapped ?? raw }
72
- } else { // object opts → old key → new key
73
- newOpts = { ...opts }
74
- for (const [oldKey, newKey] of entries)
75
- if (opts?.[oldKey] !== undefined) {
76
- newOpts[newKey] = opts[oldKey] ; delete newOpts[oldKey] }
77
- }
78
- } else { // { new-opt: default } — positional wrap
79
- const [newKey, defaultValue] = entries[0]
80
- if (opts !== undefined && !isObj)
81
- deprecation = { old: JSON.stringify(opts), new: `{ ${newKey}: ${JSON.stringify(opts)} }` }
82
- newOpts = { [newKey]: isObj ? (opts[newKey] ?? defaultValue) : (opts ?? defaultValue) }
83
- }
84
- }
85
- const aliasContext = Object.create(context)
86
- Object.defineProperty(aliasContext, 'options',
87
- { value: [newOpts], writable: true, configurable: true, enumerable: true })
88
- const visitors = { ...originals[targetName].create(aliasContext) }
89
- if (deprecation) {
90
- const ruleProgram = visitors.Program
91
- visitors.Program = function(node) {
92
- context.report({ node, messageId: 'deprecatedOption', data: deprecation })
93
- if (ruleProgram) ruleProgram.call(this, node)
94
- }
95
- }
96
- return visitors
97
- }))
98
- }
99
- }
100
- } /* eslint-enable security/detect-object-injection */
101
-
102
- export function resolveTemplate(node) {
103
- if (node.type == 'ConditionalExpression') {
104
- const branches = [node.consequent, node.alternate].map(resolveTemplate)
105
- return branches.every(Boolean) ? branches.flat() : null
106
- } else if (node.type == 'Literal' && ['number', 'string'].includes(typeof node.value))
107
- return [String(node.value)]
108
- else if (node.type == 'TemplateLiteral') // build every combo of quasis + resolved expressions
109
- return node.quasis.reduce((combos, quasi, idx) => {
110
- if (!combos) return null
111
- const withQuasi = combos.map(combo => combo + quasi.value.raw)
112
- if (idx >= node.expressions.length) return withQuasi
113
- const vals = // eslint-disable-next-line security/detect-object-injection -- AST array indices are bounded
114
- resolveTemplate(node.expressions[idx])
115
- return vals ? withQuasi.flatMap(combo => vals.map(val => combo + val)) : null
116
- }, [''])
117
- else
118
- return null
119
- }