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.
@@ -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 { extractMetadata } from '../utils.js'
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 extractMetadata(context.sourceCode))
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 { extractMetadata } from '../utils.js'
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 = extractMetadata(context.sourceCode),
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 { extractMetadata } from '../utils.js'
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 = extractMetadata(context.sourceCode),
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 { extractMetadata } from '../utils.js'
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 = extractMetadata(context.sourceCode),
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 { extractMetadata } from '../utils.js'
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 = extractMetadata(sourceCode)
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 { extractMetadata } from '../utils.js'
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
- recommended: false
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 extractMetadata(context.sourceCode)) {
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 { extractMetadata } from '../utils.js'
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 = extractMetadata(context.sourceCode),
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 { extractMetadata } from '../utils.js'
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 = extractMetadata(context.sourceCode)
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 { extractMetadata } from '../utils.js'
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 = extractMetadata(context.sourceCode)
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,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]) // 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
- }