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
@@ -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,6 +1,5 @@
1
- import { extractMetadata } from '../utils.js'
2
-
3
- const re = { leadingWhitespace: /^\s*/ }
1
+ import metadata from '../utils/metadata.js'
2
+ import regex from '../data/regex.js'
4
3
 
5
4
  export default {
6
5
  meta: {
@@ -19,12 +18,12 @@ export default {
19
18
  return {
20
19
  Program() {
21
20
  const sourceCode = context.sourceCode,
22
- metaEntries = extractMetadata(sourceCode)
21
+ metaEntries = metadata.extract(sourceCode)
23
22
  for (const entry of metaEntries) {
24
23
  const commentText = sourceCode.getText(entry.comment),
25
24
  afterSlashes = commentText.slice(2)
26
25
  if (afterSlashes.startsWith(' @')) continue
27
- const rest = afterSlashes.replace(re.leadingWhitespace, ''),
26
+ const rest = afterSlashes.replace(regex.whitespace.leading, ''),
28
27
  keyIdx = commentText.indexOf(`@${entry.key}`),
29
28
  actual = commentText.slice(0, keyIdx +1 + entry.key.length),
30
29
  fixedText = `// ${rest}`
@@ -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,10 +1,10 @@
1
- import { extractMetadata } from '../utils.js'
1
+ import metadata from '../utils/metadata.js'
2
+ import regex from '../data/regex.js'
2
3
 
3
- const re = { /* eslint-disable security/detect-unsafe-regex */
4
+ const re = { // oxlint-disable security/detect-unsafe-regex
4
5
  semVer: /^\d+\.\d+\.\d+(?:-[\da-z.-]+)?(?:\+[\da-z.-]+)?$/i,
5
- dateVer: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/,
6
- whitespace: /\s+/
7
- } /* eslint-enable security/detect-unsafe-regex */
6
+ dateVer: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/
7
+ } // oxlint-enable security/detect-unsafe-regex
8
8
 
9
9
  const urlKeys = ['homepage', 'icon', 'icon64', 'require', 'source', 'website']
10
10
 
@@ -64,12 +64,12 @@ 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 }})
71
71
  else if (entry.key == 'resource') {
72
- const url = entry.value.trim().split(re.whitespace)[1]
72
+ const url = entry.value.trim().split(regex.whitespace.all)[1]
73
73
  if (url) try { new URL(url) }
74
74
  catch {
75
75
  context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
@@ -0,0 +1,74 @@
1
+ import regex from '../data/regex.js'
2
+
3
+ const re = {
4
+ dataURI: /^data:([^;,]+)(;base64)?,(.*)$/i,
5
+ iconLine: /^[ \t]*\/\/[ \t]*@(icon|icon64|defaulticon)[ \t]+(\S.*)$/,
6
+ scheme: /^([a-z][a-z0-9+.-]*):/i
7
+ }
8
+
9
+ export default {
10
+ meta: {
11
+ type: 'suggestion',
12
+ docs: {
13
+ description: 'Validates `@icon`/`@icon64`/`@defaulticon` values against allowed schemes',
14
+ category: 'Possible Errors',
15
+ recommended: true
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ malformedURL: '`@{{header}}` is not a valid URL.',
20
+ malformedDataURI: '`@{{header}}` data URI is malformed (expected `data:<mime>[;base64],<payload>`).',
21
+ nonImageMIME: '`@{{header}}` data URI MIME type `{{mime}}` is not an image.',
22
+ disallowedScheme: '`@{{header}}` uses unsupported scheme `{{scheme}}:`'
23
+ + ' (use `http(s)`, `data:`, or a relative path).'
24
+ }
25
+ },
26
+
27
+ create(context) {
28
+ const sourceCode = context.sourceCode ?? context.getSourceCode()
29
+
30
+ function validate(value, header, line, col) {
31
+ const loc = { start: { line, column: col }, end: { line, column: col + value.length }}
32
+ const schemeMatch = value.match(re.scheme)
33
+ if (!schemeMatch) return // relative path — accepted
34
+ const scheme = schemeMatch[1].toLowerCase()
35
+ if (scheme == 'http' || scheme == 'https') {
36
+ try { new URL(value) }
37
+ catch { return context.report({ loc, messageId: 'malformedURL', data: { header }}) }
38
+ return
39
+ }
40
+ if (scheme == 'data') {
41
+ const dataMatch = value.match(re.dataURI)
42
+ if (!dataMatch)
43
+ return context.report({ loc, messageId: 'malformedDataURI', data: { header }})
44
+ const mime = dataMatch[1]
45
+ if (!mime.toLowerCase().startsWith('image/'))
46
+ return context.report({ loc, messageId: 'nonImageMIME', data: { header, mime }})
47
+ return
48
+ }
49
+ context.report({ loc, messageId: 'disallowedScheme', data: { header, scheme }})
50
+ }
51
+
52
+ return {
53
+ Program() {
54
+ const text = sourceCode.text,
55
+ metaMatch = text.match(regex.meta.block)
56
+ if (!metaMatch) return
57
+ const lines = metaMatch[1].split(regex.newline),
58
+ iconMatches = []
59
+ for (const [i, line] of lines.entries()) {
60
+ const m = line.match(re.iconLine)
61
+ if (m) iconMatches.push({ i, line, m })
62
+ }
63
+ if (!iconMatches.length) return
64
+ const baseLine = text.slice(0, metaMatch.index).split(regex.newline).length
65
+ for (const { i, line, m } of iconMatches) {
66
+ const header = m[1],
67
+ value = m[2].trim()
68
+ if (!value) continue
69
+ validate(value, header, baseLine + i, line.indexOf(value))
70
+ }
71
+ }
72
+ }
73
+ }
74
+ }
@@ -0,0 +1,73 @@
1
+ // oxlint-disable security/detect-object-injection
2
+
3
+ import array from '../utils/array.js'
4
+ import license from '../utils/license.js'
5
+
6
+ import regex from '../data/regex.js'
7
+
8
+ const re = { spdxExpr: /(?<=\s)(?:OR|AND)(?=\s)/i }
9
+
10
+ export default {
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Validates `@license` values against the SPDX license list',
15
+ category: 'Possible Errors',
16
+ recommended: true
17
+ },
18
+ defaultOptions: [{ allowedLicenses: [] }],
19
+ schema: [{
20
+ type: 'object',
21
+ properties: {
22
+ allowedLicenses: {
23
+ description: 'SPDX license IDs to restrict to',
24
+ oneOf: [
25
+ { type: 'string', description: 'Single SPDX license ID' },
26
+ { type: 'array', description: 'Array of SPDX license IDs', items: { type: 'string' }}
27
+ ]
28
+ }
29
+ },
30
+ additionalProperties: false
31
+ }],
32
+ messages: {
33
+ invalidSPDX: 'Invalid SPDX identifier `{{id}}` (see https://spdx.org/licenses/).',
34
+ disallowedSPDX: '`@license {{id}}` is not in allowed list: {{allowed}}.'
35
+ }
36
+ },
37
+
38
+ create(context) {
39
+ const opts = context.options[0] ?? {},
40
+ allowedLicenses = array.to(opts.allowedLicenses) ?? [],
41
+ sourceCode = context.sourceCode ?? context.getSourceCode()
42
+ return {
43
+ Program() {
44
+ const text = sourceCode.text,
45
+ metaMatch = text.match(regex.meta.block)
46
+ if (!metaMatch) return
47
+ const lines = text.split('\n'),
48
+ startLine = text.slice(0, metaMatch.index).split('\n').length -1,
49
+ endLine = startLine + metaMatch[0].split('\n').length -1
50
+ let licenseLineIdx = -1, licenseMatch = null
51
+ for (let i = startLine ; i < endLine ; i++) {
52
+ const match = lines[i].match(regex.meta.license)
53
+ if (match) { licenseLineIdx = i ; licenseMatch = match ; break }
54
+ }
55
+ if (!licenseMatch) return
56
+ const id = licenseMatch[1].trim() ; if (regex.url.test(id)) return
57
+ const parts = id.split(re.spdxExpr).map(part => part.trim()).filter(Boolean),
58
+ idCol = lines[licenseLineIdx].indexOf(id),
59
+ loc = { start: { line: licenseLineIdx +1, column: idCol },
60
+ end: { line: licenseLineIdx +1, column: idCol + id.length }}
61
+ for (const part of parts)
62
+ if (!license.spdx.isValid(part))
63
+ return context.report({ loc, messageId: 'invalidSPDX', data: { id: part }})
64
+ if (allowedLicenses.length) {
65
+ for (const part of parts)
66
+ if (!allowedLicenses.includes(part))
67
+ return context.report({ loc, messageId: 'disallowedSPDX',
68
+ data: { id: part, allowed: allowedLicenses.join(', ') }})
69
+ }
70
+ }
71
+ }
72
+ }
73
+ }
@@ -0,0 +1,114 @@
1
+ import metadata from '../utils/metadata.js'
2
+ import regex from '../data/regex.js'
3
+
4
+ const re = {
5
+ matchPattern: /^(\*|http\*?|https?|file|ftp):\/\/([^/]*)(\/.*)$/,
6
+ regexForm: /^\/.+\/[a-z]*$/i
7
+ }
8
+
9
+ const urlKeys = new Set(['homepage', 'require', 'require-css', 'source', 'website']),
10
+ matchKeys = new Set(['match', 'exclude-match']),
11
+ regexOrMatchKeys = new Set(['include', 'exclude'])
12
+
13
+ export default {
14
+ meta: {
15
+ type: 'problem',
16
+ docs: {
17
+ description: 'Validates URLs and match patterns in metadata headers',
18
+ category: 'Possible Errors',
19
+ recommended: true
20
+ },
21
+ defaultOptions: [{
22
+ checkURLfields: true,
23
+ checkMatchPatterns: true,
24
+ allowedSchemes: ['http', 'https', 'file', 'ftp', 'http*'],
25
+ allowBroadPatterns: false
26
+ }],
27
+ schema: [{
28
+ type: 'object',
29
+ properties: {
30
+ checkURLfields: {
31
+ type: 'boolean',
32
+ description: 'Validate URL fields (`@homepage`, `@require`, `@resource`, etc.)'
33
+ },
34
+ checkMatchPatterns: {
35
+ type: 'boolean',
36
+ description: 'Validate match patterns (`@match`, `@include`, `@exclude`, `@exclude-match`)'
37
+ },
38
+ allowedSchemes: {
39
+ type: 'array', items: { type: 'string' },
40
+ description: 'Permitted URL schemes for match patterns'
41
+ },
42
+ allowBroadPatterns: {
43
+ type: 'boolean',
44
+ description: 'Allow unrestricted patterns (`<all_urls>`, `*://*/*`, `http*://*/*`, `https://*/*`)'
45
+ }
46
+ },
47
+ additionalProperties: false
48
+ }],
49
+ messages: {
50
+ invalidURL: '`@{{field}}` has an invalid URL: "{{value}}".',
51
+ invalidMatchPattern:'`@{{field}}` has an invalid match pattern: "{{value}}".',
52
+ invalidScheme: '`@{{field}}` uses a disallowed scheme: "{{value}}".',
53
+ invalidFileHost: '`@{{field}}` uses a `file://` scheme w/ a non-empty host: "{{value}}".',
54
+ invalidHost: '`@{{field}}` has an invalid wildcard host: "{{value}}". Use `*.` prefix instead.',
55
+ allURLs: '`@{{field}}` uses the unrestricted pattern "{{value}}".'
56
+ }
57
+ },
58
+
59
+ create(context) {
60
+ const opts = context.options[0] ?? {},
61
+ checkURLfields = opts.checkURLfields !== false,
62
+ checkMatchPatterns = opts.checkMatchPatterns !== false,
63
+ allowedSchemes = new Set(opts.allowedSchemes ?? ['http', 'https', 'file', 'ftp', 'http*']),
64
+ allowBroadPatterns = opts.allowBroadPatterns === true
65
+ if (!checkURLfields && !checkMatchPatterns) return {}
66
+ function validateMatchPattern(value, field, node) {
67
+ if (value == '<all_urls>') {
68
+ if (!allowBroadPatterns)
69
+ context.report({ node, messageId: 'allURLs', data: { field, value }})
70
+ return
71
+ }
72
+ const m = value.match(re.matchPattern)
73
+ if (!m) {
74
+ if (regexOrMatchKeys.has(field) && re.regexForm.test(value)) return
75
+ context.report({ node, messageId: 'invalidMatchPattern', data: { field, value }})
76
+ return
77
+ }
78
+ const [, scheme, host, path] = m
79
+ if (scheme != '*' && !allowedSchemes.has(scheme))
80
+ context.report({ node, messageId: 'invalidScheme', data: { field, value }})
81
+ else if (scheme == 'file' && host != '')
82
+ context.report({ node, messageId: 'invalidFileHost', data: { field, value }})
83
+ else if (host.includes('*') && host != '*' && !host.startsWith('*.'))
84
+ context.report({ node, messageId: 'invalidHost', data: { field, value }})
85
+ else if (host == '*' && path == '/*' && !allowBroadPatterns)
86
+ context.report({ node, messageId: 'allURLs', data: { field, value }})
87
+ }
88
+ function validateURL(url, field, node) {
89
+ try { new URL(url) }
90
+ catch { context.report({ node, messageId: 'invalidURL', data: { field, value: url }}) }
91
+ }
92
+ return {
93
+ Program() {
94
+ for (const entry of metadata.extract(context.sourceCode)) {
95
+ const key = entry.key, lower = key.toLowerCase()
96
+ if (checkURLfields) {
97
+ if (key == 'resource') {
98
+ const tokens = entry.value.trim().split(regex.whitespace.all),
99
+ url = tokens[1]
100
+ if (url) validateURL(url, key, entry.comment)
101
+ } else if (urlKeys.has(key) || lower.endsWith('url')) {
102
+ const url = entry.value.trim()
103
+ if (url) validateURL(url, key, entry.comment)
104
+ }
105
+ }
106
+ if (checkMatchPatterns && (matchKeys.has(key) || regexOrMatchKeys.has(key))) {
107
+ const value = entry.value.trim()
108
+ if (value) validateMatchPattern(value, key, entry.comment)
109
+ }
110
+ }
111
+ }
112
+ }
113
+ }
114
+ }
@@ -0,0 +1,83 @@
1
+ import metadata from '../utils/metadata.js'
2
+
3
+ const re = { // oxlint-disable security/detect-unsafe-regex
4
+ date: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/,
5
+ semver: /^\d+\.\d+\.\d+(?:-[\da-z.-]+)?(?:\+[\da-z.-]+)?$/i
6
+ } // oxlint-enable security/detect-unsafe-regex
7
+
8
+ const examples = { date: 'YYYY.M.D or YYYY.M.D.N', semver: '1.2.3' }
9
+
10
+ export default {
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Validates the @version metadata field',
15
+ category: 'Possible Errors',
16
+ recommended: true
17
+ },
18
+ defaultOptions: [{ versionType: 'semver', allowFutureDate: false, timezone: '' }],
19
+ schema: [{
20
+ type: 'object',
21
+ properties: {
22
+ versionType: {
23
+ type: 'string',
24
+ enum: ['semver', 'date'],
25
+ description: 'Version format to enforce'
26
+ },
27
+ allowFutureDate: {
28
+ type: 'boolean',
29
+ description: 'Allow `@version` dates later than the current date'
30
+ },
31
+ timezone: {
32
+ type: 'string',
33
+ description: 'IANA timezone for determining the current date '
34
+ + '(when omitted, only year/month are checked)'
35
+ }
36
+ },
37
+ additionalProperties: false
38
+ }],
39
+ messages: {
40
+ invalidVersion: 'Invalid @version: must be a valid {{versionType}} version '
41
+ + '(expected format: {{example}}).',
42
+ futureVersion: 'Invalid @version: date cannot be in the future.'
43
+ }
44
+ },
45
+
46
+ create(context) {
47
+ const opts = context.options[0] ?? {},
48
+ versionType = opts.versionType ?? 'semver',
49
+ allowFutureDate = opts.allowFutureDate ?? false,
50
+ timezone = opts.timezone ?? '',
51
+ example = examples[versionType] // oxlint-disable-line security/detect-object-injection
52
+ const now = timezone
53
+ ? Object.fromEntries(new Intl.DateTimeFormat('en', {
54
+ timeZone: timezone, year: 'numeric', month: 'numeric', day: 'numeric'
55
+ }).formatToParts().filter(({ type }) => ['year', 'month', 'day'].includes(type))
56
+ .map(({ type, value }) => [type, Number(value)]))
57
+ : { year: new Date().getUTCFullYear(), month: new Date().getUTCMonth() + 1 }
58
+ function validate(node, version) {
59
+ if (typeof version != 'string') return
60
+ if (versionType == 'semver') {
61
+ if (!re.semver.test(version))
62
+ context.report({ node, messageId: 'invalidVersion', data: { versionType, example }})
63
+ return
64
+ }
65
+ if (versionType == 'date') {
66
+ if (!re.date.test(version))
67
+ return context.report({ node, messageId: 'invalidVersion', data: { versionType, example }})
68
+ if (allowFutureDate) return
69
+ const [year, month, day] = version.split('.').map(Number)
70
+ if (year > now.year
71
+ || year == now.year && month > now.month
72
+ || timezone && year == now.year && month == now.month && day > now.day
73
+ ) context.report({ node, messageId: 'futureVersion' })
74
+ }
75
+ }
76
+ return {
77
+ Program() {
78
+ for (const entry of metadata.extract(context.sourceCode))
79
+ if (entry.key == 'version') validate(entry.comment, entry.value)
80
+ }
81
+ }
82
+ }
83
+ }
@@ -0,0 +1,11 @@
1
+ import apis from '../data/apis.js'
2
+
3
+ export default {
4
+ supports({ manager, api }) {
5
+ return !apis.all.has(api) ? false
6
+ : api.startsWith('CAT') ? manager == 'scriptcat'
7
+ : apis.tm.has(api) ? manager == 'tampermonkey'
8
+ : manager == 'adguard' ? apis.ag.has(api)
9
+ : true
10
+ }
11
+ }
@@ -0,0 +1 @@
1
+ export default { to(val) { return Array.isArray(val) ? val : val == null ? [] : [val] }}
@@ -0,0 +1,76 @@
1
+ export default {
2
+ initAlias(plugin, [oldName, alias]) { // oxlint-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
+ } // oxlint-enable security/detect-object-injection
76
+ }