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
|
@@ -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,6 +1,5 @@
|
|
|
1
|
-
import
|
|
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 =
|
|
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(
|
|
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
|
|
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,10 +1,10 @@
|
|
|
1
|
-
import
|
|
1
|
+
import metadata from '../utils/metadata.js'
|
|
2
|
+
import regex from '../data/regex.js'
|
|
2
3
|
|
|
3
|
-
const re = {
|
|
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
|
-
|
|
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 =
|
|
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(
|
|
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
|
+
}
|
package/src/utils/api.js
ADDED
|
@@ -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
|
+
}
|