eslint-plugin-greasemonkey 1.8.4 → 1.9.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 +304 -64
- package/docs/LICENSE.md +1 -1
- package/package.json +47 -41
- package/src/data/headers.js +24 -0
- package/src/index.d.ts +33 -3
- package/src/index.js +5 -80
- package/src/rules/meta-order.js +3 -4
- package/src/rules/meta-spacing.js +2 -2
- package/src/rules/no-duplicate-keys.js +2 -2
- package/src/rules/no-insecure-fetch.js +109 -0
- package/src/rules/no-insecure-messaging.js +68 -0
- package/src/rules/no-invalid-grants.js +3 -3
- package/src/rules/no-invalid-headers.js +3 -3
- package/src/rules/no-missing-grants.js +3 -3
- package/src/rules/no-missing-resources.js +4 -3
- package/src/rules/no-unused-grants.js +2 -2
- package/src/rules/no-unused-resources.js +6 -5
- package/src/rules/prefer-gm-add-style.js +66 -0
- package/src/rules/prefer-match.js +4 -3
- package/src/rules/require-homepage-pair.js +3 -4
- package/src/rules/require-matching-localized-fields.js +2 -2
- package/src/rules/require-required-fields.js +2 -2
- package/src/rules/require-space-before-meta-key.js +2 -2
- package/src/rules/require-sri-hashing.js +3 -3
- package/src/rules/require-update-download-pair.js +3 -4
- package/src/rules/require-user-js-filename.js +2 -2
- package/src/rules/valid-field-values.js +2 -2
- package/src/utils/deprecations.js +76 -0
- package/src/utils/init.js +91 -0
- package/src/utils/metadata.js +31 -0
- package/src/utils/rules.js +7 -0
- package/src/utils/template.js +21 -0
- package/src/utils.js +0 -119
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
const re = { appendMethod: /^append(?:Child)?$/ }
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'suggestion',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Prefers `GM_addStyle()` or a shadow root over manual `<style>` injection',
|
|
8
|
+
category: 'Best Practices'
|
|
9
|
+
},
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
preferGmAddStyle: 'Use `GM_addStyle()` or inject into a `shadowRoot` '
|
|
13
|
+
+ 'instead of manually appending a `<style>` element.'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
create(context) {
|
|
18
|
+
const styleElems = new Set(), docAliases = new Set()
|
|
19
|
+
function isCreateStyle(node) {
|
|
20
|
+
return node?.type == 'CallExpression'
|
|
21
|
+
&& node.callee?.property?.name == 'createElement'
|
|
22
|
+
&& node.arguments[0]?.value == 'style'
|
|
23
|
+
}
|
|
24
|
+
function isDocumentRef(node) {
|
|
25
|
+
return node?.type == 'Identifier'
|
|
26
|
+
&& (node.name == 'document' || docAliases.has(node.name))
|
|
27
|
+
}
|
|
28
|
+
function isDocumentMember(node) {
|
|
29
|
+
return node?.type == 'MemberExpression' && isDocumentRef(node.object)
|
|
30
|
+
}
|
|
31
|
+
function isObjectAssign(node) {
|
|
32
|
+
return node?.type == 'CallExpression'
|
|
33
|
+
&& node.callee?.type == 'MemberExpression'
|
|
34
|
+
&& node.callee.object?.name == 'Object'
|
|
35
|
+
&& node.callee.property?.name == 'assign'
|
|
36
|
+
}
|
|
37
|
+
function isStyleTarget(node) {
|
|
38
|
+
if (isCreateStyle(node)) return true
|
|
39
|
+
if (node?.type == 'Identifier' && styleElems.has(node.name)
|
|
40
|
+
|| isObjectAssign(node) && isCreateStyle(node.arguments[0])
|
|
41
|
+
|| isObjectAssign(node)
|
|
42
|
+
&& node.arguments[0]?.type == 'Identifier'
|
|
43
|
+
&& styleElems.has(node.arguments[0].name)
|
|
44
|
+
) return true
|
|
45
|
+
return false
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
VariableDeclarator(node) {
|
|
49
|
+
if (node.id.type != 'Identifier') return
|
|
50
|
+
if (isStyleTarget(node.init))
|
|
51
|
+
styleElems.add(node.id.name)
|
|
52
|
+
if (node.init?.type == 'Identifier' && isDocumentRef(node.init))
|
|
53
|
+
docAliases.add(node.id.name)
|
|
54
|
+
},
|
|
55
|
+
CallExpression(node) {
|
|
56
|
+
if (node.callee.type != 'MemberExpression') return
|
|
57
|
+
const method = node.callee.property.name
|
|
58
|
+
if (!re.appendMethod.test(method) || !isDocumentMember(node.callee.object))
|
|
59
|
+
return
|
|
60
|
+
for (const arg of node.arguments)
|
|
61
|
+
if (isStyleTarget(arg))
|
|
62
|
+
context.report({ node: arg, messageId: 'preferGmAddStyle' })
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
meta: {
|
|
@@ -6,7 +6,8 @@ export default {
|
|
|
6
6
|
docs: {
|
|
7
7
|
description: 'Prefer @match over @include (see https://developer.chrome.com/docs/extensions/develop/migrate/mv2-deprecation-timeline)',
|
|
8
8
|
category: 'Best Practices',
|
|
9
|
-
recommended: true
|
|
9
|
+
recommended: true,
|
|
10
|
+
security: true
|
|
10
11
|
},
|
|
11
12
|
schema: [],
|
|
12
13
|
messages: { preferMatch: 'Use @match instead of @include.' }
|
|
@@ -15,7 +16,7 @@ export default {
|
|
|
15
16
|
create(context) {
|
|
16
17
|
return {
|
|
17
18
|
Program() {
|
|
18
|
-
for (const entry of
|
|
19
|
+
for (const entry of metadata.extract(context.sourceCode))
|
|
19
20
|
if (entry.key == 'include')
|
|
20
21
|
context.report({ node: entry.comment, messageId: 'preferMatch' })
|
|
21
22
|
}
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
meta: {
|
|
5
5
|
type: 'problem',
|
|
6
6
|
docs: {
|
|
7
7
|
description: 'Require @homepage and @homepageURL to be used together',
|
|
8
|
-
category: 'Best Practices'
|
|
9
|
-
recommended: false
|
|
8
|
+
category: 'Best Practices'
|
|
10
9
|
},
|
|
11
10
|
fixable: 'code',
|
|
12
11
|
schema: [],
|
|
@@ -16,7 +15,7 @@ export default {
|
|
|
16
15
|
create(context) {
|
|
17
16
|
return {
|
|
18
17
|
Program() {
|
|
19
|
-
const metaEntries =
|
|
18
|
+
const metaEntries = metadata.extract(context.sourceCode),
|
|
20
19
|
homepageEntry = metaEntries.find(entry => entry.key == 'homepage'),
|
|
21
20
|
homepageURLentry = metaEntries.find(entry => entry.key == 'homepageURL')
|
|
22
21
|
if (homepageEntry && !homepageURLentry)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
meta: {
|
|
@@ -31,7 +31,7 @@ export default {
|
|
|
31
31
|
ignoreLangs = new Set(Array.isArray(rawLangs) ? rawLangs : [rawLangs])
|
|
32
32
|
return {
|
|
33
33
|
Program() {
|
|
34
|
-
const metaEntries =
|
|
34
|
+
const metaEntries = metadata.extract(context.sourceCode),
|
|
35
35
|
nameEntries = metaEntries.filter(entry => entry.key.startsWith('name:')),
|
|
36
36
|
descEntries = metaEntries.filter(entry => entry.key.startsWith('description:')),
|
|
37
37
|
nameLangs = new Set(nameEntries.map(entry => entry.key.split(':')[1])),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
meta: {
|
|
@@ -33,7 +33,7 @@ export default {
|
|
|
33
33
|
requireMatchOrInclude = options.requireMatchOrInclude ?? true
|
|
34
34
|
return {
|
|
35
35
|
Program(node) {
|
|
36
|
-
const metaEntries =
|
|
36
|
+
const metaEntries = metadata.extract(context.sourceCode),
|
|
37
37
|
keys = new Set(metaEntries.map(entry => entry.key))
|
|
38
38
|
for (const field of requiredFields)
|
|
39
39
|
if (!keys.has(field))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
const re = { leadingWhitespace: /^\s*/ }
|
|
4
4
|
|
|
@@ -19,7 +19,7 @@ export default {
|
|
|
19
19
|
return {
|
|
20
20
|
Program() {
|
|
21
21
|
const sourceCode = context.sourceCode,
|
|
22
|
-
metaEntries =
|
|
22
|
+
metaEntries = metadata.extract(sourceCode)
|
|
23
23
|
for (const entry of metaEntries) {
|
|
24
24
|
const commentText = sourceCode.getText(entry.comment),
|
|
25
25
|
afterSlashes = commentText.slice(2)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
const re = {
|
|
4
4
|
hashEntry: /^(sha1|sha256|sha384|sha512|md5)[=-]([a-z0-9+/]+={0,2})$/i, hashFragment: /#([^#]+)$/, hashSep: /[,;]/ }
|
|
@@ -9,7 +9,7 @@ export default {
|
|
|
9
9
|
docs: {
|
|
10
10
|
description: 'Require SRI hashes on @require and @resource URLs',
|
|
11
11
|
category: 'Possible Errors',
|
|
12
|
-
|
|
12
|
+
security: true
|
|
13
13
|
},
|
|
14
14
|
schema: [{
|
|
15
15
|
type: 'object',
|
|
@@ -42,7 +42,7 @@ export default {
|
|
|
42
42
|
expected = algorithms.join(' or ')
|
|
43
43
|
return {
|
|
44
44
|
'Program:exit'() {
|
|
45
|
-
for (const entry of
|
|
45
|
+
for (const entry of metadata.extract(context.sourceCode)) {
|
|
46
46
|
if (entry.key != 'require' && entry.key != 'resource') continue
|
|
47
47
|
const fragment = entry.value.trim().match(re.hashFragment)?.[1]
|
|
48
48
|
if (!fragment) {
|
|
@@ -1,12 +1,11 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
meta: {
|
|
5
5
|
type: 'problem',
|
|
6
6
|
docs: {
|
|
7
7
|
description: 'Require @updateURL and @downloadURL to be used together',
|
|
8
|
-
category: 'Best Practices'
|
|
9
|
-
recommended: false
|
|
8
|
+
category: 'Best Practices'
|
|
10
9
|
},
|
|
11
10
|
schema: [],
|
|
12
11
|
messages: { missingPair: 'Missing @{{missing}} (required when @{{present}} is used).' }
|
|
@@ -15,7 +14,7 @@ export default {
|
|
|
15
14
|
create(context) {
|
|
16
15
|
return {
|
|
17
16
|
Program() {
|
|
18
|
-
const metaEntries =
|
|
17
|
+
const metaEntries = metadata.extract(context.sourceCode),
|
|
19
18
|
updateEntry = metaEntries.find(entry => entry.key == 'updateURL'),
|
|
20
19
|
downloadEntry = metaEntries.find(entry => entry.key == 'downloadURL')
|
|
21
20
|
if (updateEntry && !downloadEntry)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
export default {
|
|
4
4
|
meta: {
|
|
@@ -15,7 +15,7 @@ export default {
|
|
|
15
15
|
create(context) {
|
|
16
16
|
return {
|
|
17
17
|
Program(node) {
|
|
18
|
-
const metaEntries =
|
|
18
|
+
const metaEntries = metadata.extract(context.sourceCode)
|
|
19
19
|
if (!metaEntries.length) return
|
|
20
20
|
if (!context.filename.endsWith('.user.js'))
|
|
21
21
|
context.report({ node, messageId: 'requireUserJS' })
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
2
|
|
|
3
3
|
const re = { /* eslint-disable security/detect-unsafe-regex */
|
|
4
4
|
semVer: /^\d+\.\d+\.\d+(?:-[\da-z.-]+)?(?:\+[\da-z.-]+)?$/i,
|
|
@@ -64,7 +64,7 @@ export default {
|
|
|
64
64
|
: { year: new Date().getUTCFullYear(), month: new Date().getUTCMonth() +1 }
|
|
65
65
|
return {
|
|
66
66
|
Program() {
|
|
67
|
-
const metaEntries =
|
|
67
|
+
const metaEntries = metadata.extract(context.sourceCode)
|
|
68
68
|
for (const entry of metaEntries) {
|
|
69
69
|
if (entry.key == 'grant' && allowedGrants.size && !allowedGrants.has(entry.value))
|
|
70
70
|
context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
initAlias(plugin, [oldName, alias]) { /* eslint-disable security/detect-object-injection */
|
|
3
|
+
const routes = typeof alias == 'string' ? { [alias]: null } // 'old-rule': 'new-rule'
|
|
4
|
+
: Array.isArray(alias) ? Object.fromEntries(alias.map(entry =>
|
|
5
|
+
typeof entry == 'string' ? [entry, null] // ['new-rule-1', 'new-rule-2']
|
|
6
|
+
: Object.entries(entry)[0])) // [{ 'new-rule': { 'old-opt': 'new-opt' }}]
|
|
7
|
+
: alias // { 'new-rule': null | { 'new-opt': default } | { 'old-val': 'new-val' } | { ..., 'new-opt': default }}
|
|
8
|
+
const targetNames = Object.keys(routes),
|
|
9
|
+
isRename = !targetNames.includes(oldName),
|
|
10
|
+
originals = Object.fromEntries(targetNames.map(name => [name, plugin.rules[name]])),
|
|
11
|
+
source = originals[oldName] ?? originals[targetNames[0]]
|
|
12
|
+
plugin.rules[oldName] = {
|
|
13
|
+
meta: {
|
|
14
|
+
...source.meta,
|
|
15
|
+
defaultOptions: [],
|
|
16
|
+
schema: [{ type: ['object', 'array', 'string', 'number', 'boolean', 'null'] }],
|
|
17
|
+
docs: { ...source.meta.docs, recommended: isRename ? false : source.meta.docs.recommended },
|
|
18
|
+
messages: {
|
|
19
|
+
...source.meta.messages,
|
|
20
|
+
deprecatedOption: `'{{old}}' is deprecated. Use '{{new}}' instead.`
|
|
21
|
+
},
|
|
22
|
+
...(isRename ? { deprecated: {
|
|
23
|
+
message: `Use ${targetNames.map(name => `\`${name}\``).join(', ')} instead.`,
|
|
24
|
+
replacedBy: targetNames.map(name => ({ rule: { name }}))
|
|
25
|
+
}} : {})
|
|
26
|
+
},
|
|
27
|
+
create(context) {
|
|
28
|
+
const opts = context.options[0],
|
|
29
|
+
isObj = opts?.constructor === Object
|
|
30
|
+
return Object.assign({}, ...Object.entries(routes).map(([targetName, mapping]) => {
|
|
31
|
+
let newOpts = opts, deprecation
|
|
32
|
+
if (mapping) {
|
|
33
|
+
const entries = Object.entries(mapping),
|
|
34
|
+
namedRemap = entries.every(([, val]) => typeof val == 'string'),
|
|
35
|
+
wrapKey = namedRemap && entries.length > 1 ? entries.at(-1)[0] : null,
|
|
36
|
+
valMap = wrapKey ? Object.fromEntries(entries.slice(0, -1)) : mapping
|
|
37
|
+
if (namedRemap) {
|
|
38
|
+
if (typeof opts == 'string') { // bare old val → remap, optionally wrapped
|
|
39
|
+
if (valMap[opts]) deprecation = { old: opts, new: valMap[opts] }
|
|
40
|
+
const remapped = valMap[opts] ?? opts
|
|
41
|
+
newOpts = wrapKey ? { [wrapKey]: remapped } : remapped
|
|
42
|
+
} else if (wrapKey) { // object opts (or defaults) → remap inner val, warn if deprecated
|
|
43
|
+
const raw = isObj ? (opts[wrapKey] ?? mapping[wrapKey]) : mapping[wrapKey],
|
|
44
|
+
remapped = valMap[raw]
|
|
45
|
+
if (remapped) deprecation = { old: raw, new: remapped }
|
|
46
|
+
newOpts = { [wrapKey]: remapped ?? raw }
|
|
47
|
+
} else { // object opts → old key → new key
|
|
48
|
+
newOpts = { ...opts }
|
|
49
|
+
for (const [oldKey, newKey] of entries)
|
|
50
|
+
if (opts?.[oldKey] !== undefined) {
|
|
51
|
+
newOpts[newKey] = opts[oldKey] ; delete newOpts[oldKey] }
|
|
52
|
+
}
|
|
53
|
+
} else { // { new-opt: default } — positional wrap
|
|
54
|
+
const [newKey, defaultValue] = entries[0]
|
|
55
|
+
if (opts !== undefined && !isObj)
|
|
56
|
+
deprecation = { old: JSON.stringify(opts), new: `{ ${newKey}: ${JSON.stringify(opts)} }` }
|
|
57
|
+
newOpts = { [newKey]: isObj ? (opts[newKey] ?? defaultValue) : (opts ?? defaultValue) }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const aliasContext = Object.create(context)
|
|
61
|
+
Object.defineProperty(aliasContext, 'options',
|
|
62
|
+
{ value: [newOpts], writable: true, configurable: true, enumerable: true })
|
|
63
|
+
const visitors = { ...originals[targetName].create(aliasContext) }
|
|
64
|
+
if (deprecation) {
|
|
65
|
+
const ruleProgram = visitors.Program
|
|
66
|
+
visitors.Program = function(node) {
|
|
67
|
+
context.report({ node, messageId: 'deprecatedOption', data: deprecation })
|
|
68
|
+
if (ruleProgram) ruleProgram.call(this, node)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
return visitors
|
|
72
|
+
}))
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
} /* eslint-enable security/detect-object-injection */
|
|
76
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import deprecations from './deprecations.js'
|
|
2
|
+
import rulesUtils from './rules.js'
|
|
3
|
+
|
|
4
|
+
import { rules as deprecationsData } from '../data/deprecations.js'
|
|
5
|
+
|
|
6
|
+
import metaOrder from '../rules/meta-order.js'
|
|
7
|
+
import metaSpacing from '../rules/meta-spacing.js'
|
|
8
|
+
import noDuplicateKeys from '../rules/no-duplicate-keys.js'
|
|
9
|
+
import noInsecureFetch from '../rules/no-insecure-fetch.js'
|
|
10
|
+
import noInsecureMessaging from '../rules/no-insecure-messaging.js'
|
|
11
|
+
import noInvalidGrants from '../rules/no-invalid-grants.js'
|
|
12
|
+
import noInvalidHeaders from '../rules/no-invalid-headers.js'
|
|
13
|
+
import noMissingGrants from '../rules/no-missing-grants.js'
|
|
14
|
+
import noMissingResources from '../rules/no-missing-resources.js'
|
|
15
|
+
import noUnusedGrants from '../rules/no-unused-grants.js'
|
|
16
|
+
import noUnusedResources from '../rules/no-unused-resources.js'
|
|
17
|
+
import preferGMaddStyle from '../rules/prefer-gm-add-style.js'
|
|
18
|
+
import preferMatch from '../rules/prefer-match.js'
|
|
19
|
+
import requireBlankLineAfterMetaBlock from '../rules/require-blank-line-after-meta-block.js'
|
|
20
|
+
import requireHomepagePair from '../rules/require-homepage-pair.js'
|
|
21
|
+
import requireMatchingLocalizedFields from '../rules/require-matching-localized-fields.js'
|
|
22
|
+
import requireRequiredFields from '../rules/require-required-fields.js'
|
|
23
|
+
import requireSpaceBeforeMetaKey from '../rules/require-space-before-meta-key.js'
|
|
24
|
+
import requireSRIhashing from '../rules/require-sri-hashing.js'
|
|
25
|
+
import requireUpdateDownloadPair from '../rules/require-update-download-pair.js'
|
|
26
|
+
import requireUserJsFilename from '../rules/require-user-js-filename.js'
|
|
27
|
+
import validFieldValues from '../rules/valid-field-values.js'
|
|
28
|
+
|
|
29
|
+
const namespace = 'greasemonkey',
|
|
30
|
+
managers = ['tampermonkey', 'violentmonkey', 'greasemonkey', 'scriptcat', 'adguard']
|
|
31
|
+
|
|
32
|
+
export default {
|
|
33
|
+
configs(plugin) {
|
|
34
|
+
const plugins = { [namespace]: plugin },
|
|
35
|
+
rules = plugin.rules,
|
|
36
|
+
createConfig = predicate => ({ plugins, rules: rulesUtils.bundle(rules, predicate, namespace) })
|
|
37
|
+
plugin.configs = {
|
|
38
|
+
recommended: createConfig(rule => rule.meta.docs.recommended),
|
|
39
|
+
strict: createConfig(() => true),
|
|
40
|
+
problem: createConfig(rule => rule.meta.type == 'problem'),
|
|
41
|
+
security: createConfig(rule => rule.meta.docs.security)
|
|
42
|
+
}
|
|
43
|
+
for (const manager of managers)
|
|
44
|
+
plugin.configs[manager] = { // eslint-disable-line security/detect-object-injection
|
|
45
|
+
plugins,
|
|
46
|
+
rules: {
|
|
47
|
+
...plugin.configs.recommended.rules,
|
|
48
|
+
[`${namespace}/no-invalid-grants`]: ['error', { target: manager }],
|
|
49
|
+
[`${namespace}/no-invalid-headers`]: ['error', { target: manager }],
|
|
50
|
+
...(['tampermonkey', 'scriptcat', 'adguard'].includes(manager) &&
|
|
51
|
+
{ [`${namespace}/require-update-download-pair`]: 'error' })
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
deprecations(plugin) {
|
|
57
|
+
for (const entry of Object.entries(deprecationsData))
|
|
58
|
+
deprecations.initAlias(plugin, entry)
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
plugin() { return { rules: {}, configs: {} }},
|
|
62
|
+
|
|
63
|
+
rules(plugin) {
|
|
64
|
+
plugin.rules = {
|
|
65
|
+
'meta-order': metaOrder,
|
|
66
|
+
'meta-spacing': metaSpacing,
|
|
67
|
+
'no-duplicate-keys': noDuplicateKeys,
|
|
68
|
+
'no-insecure-fetch': noInsecureFetch,
|
|
69
|
+
'no-insecure-messaging': noInsecureMessaging,
|
|
70
|
+
'no-invalid-grants': noInvalidGrants,
|
|
71
|
+
'no-invalid-headers': noInvalidHeaders,
|
|
72
|
+
'no-missing-grants': noMissingGrants,
|
|
73
|
+
'no-missing-resources': noMissingResources,
|
|
74
|
+
'no-unused-grants': noUnusedGrants,
|
|
75
|
+
'no-unused-resources': noUnusedResources,
|
|
76
|
+
'prefer-gm-add-style': preferGMaddStyle,
|
|
77
|
+
'prefer-match': preferMatch,
|
|
78
|
+
'require-blank-line-after-meta-block': requireBlankLineAfterMetaBlock,
|
|
79
|
+
'require-homepage-pair': requireHomepagePair,
|
|
80
|
+
'require-matching-localized-fields': requireMatchingLocalizedFields,
|
|
81
|
+
'require-required-fields': requireRequiredFields,
|
|
82
|
+
'require-space-before-meta-key': requireSpaceBeforeMetaKey,
|
|
83
|
+
'require-sri-hashing': requireSRIhashing,
|
|
84
|
+
'require-update-download-pair': requireUpdateDownloadPair,
|
|
85
|
+
'require-user-js-filename': requireUserJsFilename,
|
|
86
|
+
'valid-field-values': validFieldValues
|
|
87
|
+
}
|
|
88
|
+
for (const [ruleId, rule] of Object.entries(plugin.rules))
|
|
89
|
+
rule.meta.docs.url = `https://codeberg.org/adamlui/eslint-plugin-${namespace}/#${ruleId}`
|
|
90
|
+
}
|
|
91
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const re = { whitespace: /\s/ }
|
|
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(re.whitespace),
|
|
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
|
+
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]) // eslint-disable-line security/detect-object-injection -- AST array indices are bounded
|
|
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
|
-
}
|