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.
- package/README.md +776 -68
- package/docs/LICENSE.md +1 -1
- package/package.json +53 -43
- package/src/data/apis.js +56 -53
- package/src/data/headers.js +65 -31
- package/src/data/regex.js +14 -0
- package/src/index.d.ts +34 -3
- package/src/index.js +5 -80
- package/src/rules/meta-order.js +5 -6
- package/src/rules/meta-spacing.js +11 -10
- package/src/rules/no-duplicate-keys.js +2 -2
- package/src/rules/no-insecure-fetch.js +108 -0
- package/src/rules/no-insecure-messaging.js +67 -0
- package/src/rules/no-invalid-grants.js +12 -10
- package/src/rules/no-invalid-headers.js +11 -11
- package/src/rules/no-missing-grants.js +6 -8
- package/src/rules/no-missing-resources.js +10 -8
- package/src/rules/no-sensitive-grants.js +81 -0
- package/src/rules/no-unused-grants.js +2 -2
- package/src/rules/no-unused-resources.js +10 -8
- package/src/rules/prefer-gm-add-style.js +67 -0
- package/src/rules/prefer-match.js +4 -3
- package/src/rules/require-homepage-pair.js +3 -4
- package/src/rules/require-license-header.js +27 -0
- 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 +4 -5
- 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 +7 -7
- package/src/rules/valid-icon-headers.js +74 -0
- package/src/rules/valid-license-header.js +73 -0
- package/src/rules/valid-url-headers.js +114 -0
- package/src/rules/valid-version-header.js +83 -0
- package/src/utils/api.js +11 -0
- package/src/utils/array.js +1 -0
- package/src/utils/deprecations.js +76 -0
- package/src/utils/init.js +106 -0
- package/src/utils/license.js +56 -0
- package/src/utils/metadata.js +31 -0
- package/src/utils/pkg.js +21 -0
- package/src/utils/rules.js +7 -0
- package/src/utils/template.js +21 -0
- 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
|
+
}
|
package/src/utils/pkg.js
ADDED
|
@@ -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,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
|
-
}
|