eslint-plugin-greasemonkey 1.1.0 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,25 @@
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ export default {
4
+ meta: {
5
+ type: 'problem',
6
+ docs: {
7
+ description: 'Require that userscript filenames end in ".user.js"',
8
+ category: 'Possible Errors',
9
+ recommended: true
10
+ },
11
+ schema: [],
12
+ messages: { requireUserJS: 'Userscript filename should end in ".user.js"' }
13
+ },
14
+
15
+ create(context) {
16
+ return {
17
+ Program(node) {
18
+ const metaEntries = extractMetadata(context.sourceCode)
19
+ if (!metaEntries.length) return
20
+ if (!context.filename.endsWith('.user.js'))
21
+ context.report({ node, messageId: 'requireUserJS' })
22
+ }
23
+ }
24
+ }
25
+ }
@@ -1,9 +1,12 @@
1
1
  import { extractMetadata } from '../utils.js'
2
2
 
3
- const re = {
4
- semVer: /^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i,
5
- dateVer: /^\d{4}\.(?:0?[1-9]|1[0-2])\.(?:0?[1-9]|[12]\d|3[01])(?:\.\d+)?$/
6
- }
3
+ const re = { /* eslint-disable security/detect-unsafe-regex */
4
+ 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 */
8
+
9
+ const urlKeys = ['homepage', 'icon', 'icon64', 'require', 'source', 'website']
7
10
 
8
11
  export default {
9
12
  meta: {
@@ -40,13 +43,26 @@ export default {
40
43
  create(context) {
41
44
  const options = context.options[0] || {},
42
45
  versionType = options.versionType || 'semver',
43
- allowedGrants = options.allowedGrants || []
44
- const example = versionType == 'semver' ? '1.2.3' : 'YYYY.M.D or YYYY.MM.DD'
46
+ allowedGrants = options.allowedGrants || [],
47
+ example = versionType == 'semver' ? '1.2.3' : 'YYYY.M.D or YYYY.MM.DD'
45
48
  return {
46
49
  Program() {
47
50
  const metaEntries = extractMetadata(context.sourceCode)
48
51
  for (const entry of metaEntries) {
49
- if (entry.key == 'version') {
52
+ if (entry.key == 'grant' && allowedGrants.length && !allowedGrants.includes(entry.value))
53
+ context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
54
+ else if (entry.key == 'resource') {
55
+ const url = entry.value.trim().split(re.whitespace)[1]
56
+ if (url) try { new URL(url) }
57
+ catch {
58
+ context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
59
+ }
60
+ } else if (urlKeys.includes(entry.key) || entry.key.toLowerCase().endsWith('url'))
61
+ try { new URL(entry.value) }
62
+ catch {
63
+ context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
64
+ }
65
+ else if (entry.key == 'version') {
50
66
  const isValid = versionType == 'semver' ? re.semVer.test(entry.value)
51
67
  : versionType == 'date' ? re.dateVer.test(entry.value)
52
68
  : false
@@ -54,13 +70,6 @@ export default {
54
70
  context.report({
55
71
  node: entry.comment, messageId: 'invalidVersion', data: { versionType, example }})
56
72
  }
57
- if (['homepage', 'icon', 'updateURL', 'downloadURL', 'supportURL'].includes(entry.key))
58
- try { new URL(entry.value) }
59
- catch {
60
- context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
61
- }
62
- if (entry.key == 'grant' && allowedGrants.length && !allowedGrants.includes(entry.value))
63
- context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
64
73
  }
65
74
  }
66
75
  }
@@ -0,0 +1,56 @@
1
+ import { extractMetadata } from '../utils.js'
2
+ import { gmLegacyAPIs, gmModernAPIs, scAPIs } from '../apis.js'
3
+
4
+ const specialGrants = new Set([
5
+ 'none', 'unsafeWindow', 'window.close', 'window.focus', 'window.onurlchange'
6
+ ])
7
+
8
+ const grants = new Set([...gmLegacyAPIs, ...gmModernAPIs, ...scAPIs, ...specialGrants]),
9
+ sortedGrants = new Map(Array.from(grants, grant => [grant.toLowerCase(), grant]))
10
+
11
+ export default {
12
+ meta: {
13
+ type: 'problem',
14
+ docs: {
15
+ description: 'Disallow invalid @grant values',
16
+ category: 'Possible Errors',
17
+ recommended: true
18
+ },
19
+ schema: [{
20
+ type: 'object',
21
+ properties: {
22
+ allowedGrants: {
23
+ type: 'array',
24
+ items: { type: 'string' },
25
+ description: 'Additional @grant values to allow beyond the built-in list'
26
+ }
27
+ },
28
+ additionalProperties: false
29
+ }],
30
+ defaultOptions: [{}],
31
+ messages: {
32
+ invalidGrant: 'Invalid @grant: "{{grant}}"',
33
+ wrongCase: 'Invalid @grant: "{{grant}}" (did you mean "{{suggestion}}"?)'
34
+ }
35
+ },
36
+
37
+ create(context) {
38
+ const options = context.options[0] || {},
39
+ allowedGrants = options.allowedGrants || []
40
+ return {
41
+ Program() {
42
+ const metaEntries = extractMetadata(context.sourceCode),
43
+ grantEntries = metaEntries.filter(entry => entry.key == 'grant')
44
+ for (const grant of grantEntries) {
45
+ const grantName = grant.value.trim()
46
+ if (grants.has(grantName) || allowedGrants.includes(grantName)) continue
47
+ const suggestion = sortedGrants.get(grantName.toLowerCase())
48
+ context.report(suggestion
49
+ ? { node: grant.comment, messageId: 'wrongCase', data: { grant: grantName, suggestion }}
50
+ : { node: grant.comment, messageId: 'invalidGrant', data: { grant: grantName }}
51
+ )
52
+ }
53
+ }
54
+ }
55
+ }
56
+ }
package/src/utils.js CHANGED
@@ -1,14 +1,14 @@
1
1
  export function extractMetadata(sourceCode) {
2
2
  const comments = sourceCode.getAllComments(),
3
3
  metaEntries = []
4
- let inMetaBlock = false
4
+ let inMetaBlock
5
5
  for (const comment of comments) {
6
6
  const text = comment.value.trim()
7
- if (!inMetaBlock && text == '==UserScript==') { inMetaBlock = true ; continue }
8
- if (inMetaBlock && text == '==/UserScript==') break
9
- if (inMetaBlock && text.startsWith('@')) {
7
+ if (!inMetaBlock && text == '==UserScript==') inMetaBlock = true
8
+ else if (inMetaBlock && text == '==/UserScript==') break
9
+ else if (inMetaBlock && text.startsWith('@')) { // capture entry
10
10
  const spaceIdx = text.indexOf(' ')
11
- if (spaceIdx > 1) { // '@' at 0, key starts at 1, need space after key
11
+ if (spaceIdx > 1) { // need space after key
12
12
  const key = text.slice(1, spaceIdx).trim(),
13
13
  value = text.slice(spaceIdx +1).trim()
14
14
  metaEntries.push({ key, value, comment, line: comment.loc.start.line })
@@ -17,3 +17,22 @@ export function extractMetadata(sourceCode) {
17
17
  }
18
18
  return metaEntries
19
19
  }
20
+
21
+ export function resolveTemplate(node) {
22
+ if (node.type == 'ConditionalExpression') {
23
+ const branches = [node.consequent, node.alternate].map(resolveTemplate)
24
+ return branches.every(Boolean) ? branches.flat() : null
25
+ } else if (node.type == 'Literal' && ['number', 'string'].includes(typeof node.value))
26
+ return [String(node.value)]
27
+ else if (node.type == 'TemplateLiteral') // build every combo of quasis + resolved expressions
28
+ return node.quasis.reduce((combos, quasi, idx) => {
29
+ if (!combos) return null
30
+ const withQuasi = combos.map(combo => combo + quasi.value.raw)
31
+ if (idx >= node.expressions.length) return withQuasi
32
+ const vals = // eslint-disable-next-line security/detect-object-injection -- AST array indices are bounded
33
+ resolveTemplate(node.expressions[idx])
34
+ return vals ? withQuasi.flatMap(combo => vals.map(val => combo + val)) : null
35
+ }, [''])
36
+ else
37
+ return null
38
+ }