eslint-plugin-greasemonkey 1.9.0 → 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 +481 -13
- package/package.json +11 -7
- package/src/data/apis.js +56 -53
- package/src/data/headers.js +45 -35
- package/src/data/regex.js +14 -0
- package/src/index.d.ts +1 -0
- package/src/rules/meta-order.js +2 -2
- package/src/rules/meta-spacing.js +9 -8
- package/src/rules/no-insecure-fetch.js +24 -25
- package/src/rules/no-insecure-messaging.js +2 -3
- package/src/rules/no-invalid-grants.js +10 -8
- package/src/rules/no-invalid-headers.js +9 -9
- package/src/rules/no-missing-grants.js +4 -6
- package/src/rules/no-missing-resources.js +6 -5
- package/src/rules/no-sensitive-grants.js +81 -0
- package/src/rules/no-unused-resources.js +6 -5
- package/src/rules/prefer-gm-add-style.js +12 -11
- package/src/rules/require-license-header.js +27 -0
- package/src/rules/require-space-before-meta-key.js +2 -3
- package/src/rules/valid-field-values.js +5 -5
- 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 +2 -2
- package/src/utils/init.js +23 -8
- package/src/utils/license.js +56 -0
- package/src/utils/metadata.js +2 -2
- package/src/utils/pkg.js +21 -0
- package/src/utils/template.js +1 -1
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import metadata from '../utils/metadata.js'
|
|
2
2
|
import template from '../utils/template.js'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
import regex from '../data/regex.js'
|
|
5
|
+
|
|
6
|
+
const resourceGetterNames = new Set(['GM_getResourceText', 'GM_getResourceURL'])
|
|
6
7
|
|
|
7
8
|
export default {
|
|
8
9
|
meta: {
|
|
@@ -29,8 +30,8 @@ export default {
|
|
|
29
30
|
const resolved = template.resolve(arg)
|
|
30
31
|
if (resolved)
|
|
31
32
|
for (const name of resolved) usedResources.add(name)
|
|
32
|
-
else usedPatterns.push(new RegExp( //
|
|
33
|
-
`^${arg.quasis.map(quasi => quasi.value.raw.replace(
|
|
33
|
+
else usedPatterns.push(new RegExp( // oxlint-disable-line security/detect-non-literal-regexp
|
|
34
|
+
`^${arg.quasis.map(quasi => quasi.value.raw.replace(regex.specialChars, '\\$&')).join('.*')}$`))
|
|
34
35
|
}
|
|
35
36
|
},
|
|
36
37
|
'Program:exit'() {
|
|
@@ -38,7 +39,7 @@ export default {
|
|
|
38
39
|
resources = metaEntries.filter(entry => entry.key == 'resource')
|
|
39
40
|
if (!resources.length) return
|
|
40
41
|
for (const res of resources) {
|
|
41
|
-
const name = res.value.trim().split(
|
|
42
|
+
const name = res.value.trim().split(regex.whitespace.all)[0]
|
|
42
43
|
if (usedResources.has(name) || usedPatterns.some(patt => patt.test(name))) continue
|
|
43
44
|
context.report({ node: res.comment, messageId: 'unusedResource', data: { name }})
|
|
44
45
|
}
|
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
const re = { appendMethod: /^append(?:Child)?$/ }
|
|
2
2
|
|
|
3
|
+
function isCreateStyle(node) {
|
|
4
|
+
return node?.type == 'CallExpression'
|
|
5
|
+
&& node.callee?.property?.name == 'createElement'
|
|
6
|
+
&& node.arguments[0]?.value == 'style'
|
|
7
|
+
}
|
|
8
|
+
function isObjectAssign(node) {
|
|
9
|
+
return node?.type == 'CallExpression'
|
|
10
|
+
&& node.callee?.type == 'MemberExpression'
|
|
11
|
+
&& node.callee.object?.name == 'Object'
|
|
12
|
+
&& node.callee.property?.name == 'assign'
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
export default {
|
|
4
16
|
meta: {
|
|
5
17
|
type: 'suggestion',
|
|
@@ -16,11 +28,6 @@ export default {
|
|
|
16
28
|
|
|
17
29
|
create(context) {
|
|
18
30
|
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
31
|
function isDocumentRef(node) {
|
|
25
32
|
return node?.type == 'Identifier'
|
|
26
33
|
&& (node.name == 'document' || docAliases.has(node.name))
|
|
@@ -28,12 +35,6 @@ export default {
|
|
|
28
35
|
function isDocumentMember(node) {
|
|
29
36
|
return node?.type == 'MemberExpression' && isDocumentRef(node.object)
|
|
30
37
|
}
|
|
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
38
|
function isStyleTarget(node) {
|
|
38
39
|
if (isCreateStyle(node)) return true
|
|
39
40
|
if (node?.type == 'Identifier' && styleElems.has(node.name)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import regex from '../data/regex.js'
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
meta: {
|
|
5
|
+
type: 'problem',
|
|
6
|
+
docs: {
|
|
7
|
+
description: 'Requires a `@license` metadata field',
|
|
8
|
+
category: 'Possible Errors',
|
|
9
|
+
recommended: false
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
missingLicense: 'No `@license` field found. Add one (e.g. `@license MIT`).'
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
create(context) {
|
|
17
|
+
const sourceCode = context.sourceCode ?? context.getSourceCode()
|
|
18
|
+
return {
|
|
19
|
+
Program() {
|
|
20
|
+
const match = sourceCode.text.match(regex.meta.block)
|
|
21
|
+
if (!match) return
|
|
22
|
+
if (!regex.meta.license.test(match[1]))
|
|
23
|
+
context.report({ loc: { line: 1, column: 0 }, messageId: 'missingLicense' })
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import metadata from '../utils/metadata.js'
|
|
2
|
-
|
|
3
|
-
const re = { leadingWhitespace: /^\s*/ }
|
|
2
|
+
import regex from '../data/regex.js'
|
|
4
3
|
|
|
5
4
|
export default {
|
|
6
5
|
meta: {
|
|
@@ -24,7 +23,7 @@ export default {
|
|
|
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,10 +1,10 @@
|
|
|
1
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
|
|
|
@@ -69,7 +69,7 @@ export default {
|
|
|
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] }}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export default {
|
|
2
|
-
initAlias(plugin, [oldName, alias]) {
|
|
2
|
+
initAlias(plugin, [oldName, alias]) { // oxlint-disable security/detect-object-injection
|
|
3
3
|
const routes = typeof alias == 'string' ? { [alias]: null } // 'old-rule': 'new-rule'
|
|
4
4
|
: Array.isArray(alias) ? Object.fromEntries(alias.map(entry =>
|
|
5
5
|
typeof entry == 'string' ? [entry, null] // ['new-rule-1', 'new-rule-2']
|
|
@@ -72,5 +72,5 @@ export default {
|
|
|
72
72
|
}))
|
|
73
73
|
}
|
|
74
74
|
}
|
|
75
|
-
}
|
|
75
|
+
} // oxlint-enable security/detect-object-injection
|
|
76
76
|
}
|
package/src/utils/init.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import deprecations from './deprecations.js'
|
|
2
|
+
import pkg from './pkg.js'
|
|
2
3
|
import rulesUtils from './rules.js'
|
|
3
4
|
|
|
5
|
+
import headers from '../data/headers.js'
|
|
4
6
|
import { rules as deprecationsData } from '../data/deprecations.js'
|
|
5
7
|
|
|
6
8
|
import metaOrder from '../rules/meta-order.js'
|
|
@@ -12,12 +14,14 @@ import noInvalidGrants from '../rules/no-invalid-grants.js'
|
|
|
12
14
|
import noInvalidHeaders from '../rules/no-invalid-headers.js'
|
|
13
15
|
import noMissingGrants from '../rules/no-missing-grants.js'
|
|
14
16
|
import noMissingResources from '../rules/no-missing-resources.js'
|
|
17
|
+
import noSensitiveGrants from '../rules/no-sensitive-grants.js'
|
|
15
18
|
import noUnusedGrants from '../rules/no-unused-grants.js'
|
|
16
19
|
import noUnusedResources from '../rules/no-unused-resources.js'
|
|
17
20
|
import preferGMaddStyle from '../rules/prefer-gm-add-style.js'
|
|
18
21
|
import preferMatch from '../rules/prefer-match.js'
|
|
19
22
|
import requireBlankLineAfterMetaBlock from '../rules/require-blank-line-after-meta-block.js'
|
|
20
23
|
import requireHomepagePair from '../rules/require-homepage-pair.js'
|
|
24
|
+
import requireLicenseHeader from '../rules/require-license-header.js'
|
|
21
25
|
import requireMatchingLocalizedFields from '../rules/require-matching-localized-fields.js'
|
|
22
26
|
import requireRequiredFields from '../rules/require-required-fields.js'
|
|
23
27
|
import requireSpaceBeforeMetaKey from '../rules/require-space-before-meta-key.js'
|
|
@@ -25,9 +29,13 @@ import requireSRIhashing from '../rules/require-sri-hashing.js'
|
|
|
25
29
|
import requireUpdateDownloadPair from '../rules/require-update-download-pair.js'
|
|
26
30
|
import requireUserJsFilename from '../rules/require-user-js-filename.js'
|
|
27
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'
|
|
28
36
|
|
|
29
|
-
const
|
|
30
|
-
|
|
37
|
+
const pkgName = pkg.getName(),
|
|
38
|
+
namespace = pkgName.replace(/^@[^/]+\//, '').replace(/^eslint-plugin-/, '')
|
|
31
39
|
|
|
32
40
|
export default {
|
|
33
41
|
configs(plugin) {
|
|
@@ -38,10 +46,11 @@ export default {
|
|
|
38
46
|
recommended: createConfig(rule => rule.meta.docs.recommended),
|
|
39
47
|
strict: createConfig(() => true),
|
|
40
48
|
problem: createConfig(rule => rule.meta.type == 'problem'),
|
|
41
|
-
security: createConfig(rule => rule.meta.docs.security)
|
|
49
|
+
security: createConfig(rule => rule.meta.docs.security),
|
|
50
|
+
stylistic: createConfig(rule => rule.meta.docs.category == 'Stylistic Issues')
|
|
42
51
|
}
|
|
43
|
-
for (const manager of managers)
|
|
44
|
-
plugin.configs[manager] = { //
|
|
52
|
+
for (const manager of headers.managers)
|
|
53
|
+
plugin.configs[manager] = { // oxlint-disable-line security/detect-object-injection
|
|
45
54
|
plugins,
|
|
46
55
|
rules: {
|
|
47
56
|
...plugin.configs.recommended.rules,
|
|
@@ -58,7 +67,7 @@ export default {
|
|
|
58
67
|
deprecations.initAlias(plugin, entry)
|
|
59
68
|
},
|
|
60
69
|
|
|
61
|
-
plugin() { return { rules: {}, configs: {} }},
|
|
70
|
+
plugin() { return { meta: { name: pkgName }, rules: {}, configs: {} }},
|
|
62
71
|
|
|
63
72
|
rules(plugin) {
|
|
64
73
|
plugin.rules = {
|
|
@@ -71,21 +80,27 @@ export default {
|
|
|
71
80
|
'no-invalid-headers': noInvalidHeaders,
|
|
72
81
|
'no-missing-grants': noMissingGrants,
|
|
73
82
|
'no-missing-resources': noMissingResources,
|
|
83
|
+
'no-sensitive-grants': noSensitiveGrants,
|
|
74
84
|
'no-unused-grants': noUnusedGrants,
|
|
75
85
|
'no-unused-resources': noUnusedResources,
|
|
76
86
|
'prefer-gm-add-style': preferGMaddStyle,
|
|
77
87
|
'prefer-match': preferMatch,
|
|
78
88
|
'require-blank-line-after-meta-block': requireBlankLineAfterMetaBlock,
|
|
79
89
|
'require-homepage-pair': requireHomepagePair,
|
|
90
|
+
'require-license-header': requireLicenseHeader,
|
|
80
91
|
'require-matching-localized-fields': requireMatchingLocalizedFields,
|
|
81
92
|
'require-required-fields': requireRequiredFields,
|
|
82
93
|
'require-space-before-meta-key': requireSpaceBeforeMetaKey,
|
|
83
94
|
'require-sri-hashing': requireSRIhashing,
|
|
84
95
|
'require-update-download-pair': requireUpdateDownloadPair,
|
|
85
96
|
'require-user-js-filename': requireUserJsFilename,
|
|
86
|
-
'valid-field-values': validFieldValues
|
|
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
|
|
87
102
|
}
|
|
88
103
|
for (const [ruleId, rule] of Object.entries(plugin.rules))
|
|
89
|
-
rule.meta.docs.url = `https://codeberg.org/adamlui
|
|
104
|
+
rule.meta.docs.url = `https://codeberg.org/adamlui/${pkgName}/#${ruleId}`
|
|
90
105
|
}
|
|
91
106
|
}
|