eslint-plugin-greasemonkey 1.0.0 → 1.2.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.
@@ -1,60 +1,79 @@
1
- import { extractMetadata } from '../utils.js'
2
-
3
- const RE_SEMVER = /^\d+\.\d+\.\d+(?:-[0-9a-z.-]+)?(?:\+[0-9a-z.-]+)?$/i
4
-
5
- export default {
6
- meta: {
7
- type: 'problem',
8
- docs: {
9
- description: 'Validate values of metadata fields',
10
- category: 'Possible Errors',
11
- recommended: true
12
- },
13
- schema: [{
14
- type: 'object',
15
- properties: {
16
- reVer: {
17
- type: 'string',
18
- description: 'Regular expression pattern for @version validation (default: ^\\d+\\.\\d+\\.\\d+$)'
19
- },
20
- allowedGrants: {
21
- type: 'array',
22
- items: { type: 'string' },
23
- description: 'Allowed @grant values to check against; if empty, no grant validation'
24
- }
25
- },
26
- additionalProperties: false
27
- }],
28
- defaultOptions: [{}],
29
- messages: {
30
- invalidVersion: 'Invalid @version: must match pattern "{{pattern}}"',
31
- invalidURL: 'Invalid URL for @{{field}}',
32
- invalidGrant: 'Unknown @grant: "{{grant}}"'
33
- }
34
- },
35
-
36
- create(context) {
37
- const options = context.options[0] || {},
38
- reVer = options.reVer || '^\\d+\\.\\d+\\.\\d+$',
39
- allowedGrants = options.allowedGrants || []
40
- return {
41
- Program() {
42
- const metaEntries = extractMetadata(context.sourceCode)
43
- for (const entry of metaEntries) {
44
- if (entry.key == 'version') {
45
- if (!RE_SEMVER.test(entry.value) && !(new RegExp(reVer).test(entry.value)))
46
- context.report({
47
- node: entry.comment, messageId: 'invalidVersion', data: { pattern: reVer }})
48
- }
49
- if (['homepage', 'icon', 'updateURL', 'downloadURL', 'supportURL'].includes(entry.key))
50
- try { new URL(entry.value) }
51
- catch {
52
- context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
53
- }
54
- if (entry.key == 'grant' && allowedGrants.length && !allowedGrants.includes(entry.value))
55
- context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
56
- }
57
- }
58
- }
59
- }
60
- }
1
+ import { extractMetadata } from '../utils.js'
2
+
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
+ urlSuffix: /url$/i,
7
+ whitespace: /\s+/
8
+ } /* eslint-enable security/detect-unsafe-regex */
9
+
10
+ export default {
11
+ meta: {
12
+ type: 'problem',
13
+ docs: {
14
+ description: 'Validate values of metadata fields',
15
+ category: 'Possible Errors',
16
+ recommended: true
17
+ },
18
+ schema: [{
19
+ type: 'object',
20
+ properties: {
21
+ versionType: {
22
+ type: 'string',
23
+ enum: ['semver', 'date'],
24
+ description: 'Version format to enforce (default: semver)'
25
+ },
26
+ allowedGrants: {
27
+ type: 'array',
28
+ items: { type: 'string' },
29
+ description: 'Allowed @grant values to check against; if empty, no grant validation'
30
+ }
31
+ },
32
+ additionalProperties: false
33
+ }],
34
+ defaultOptions: [{}],
35
+ messages: {
36
+ invalidVersion: 'Invalid @version: must be a valid {{versionType}} (expected format: {{example}})',
37
+ invalidURL: 'Invalid URL for @{{field}}',
38
+ invalidGrant: 'Unknown @grant: "{{grant}}"'
39
+ }
40
+ },
41
+
42
+ create(context) {
43
+ const options = context.options[0] || {},
44
+ versionType = options.versionType || 'semver',
45
+ allowedGrants = options.allowedGrants || []
46
+ const example = versionType == 'semver' ? '1.2.3' : 'YYYY.M.D or YYYY.MM.DD'
47
+ return {
48
+ Program() {
49
+ const metaEntries = extractMetadata(context.sourceCode)
50
+ for (const entry of metaEntries) {
51
+ if (entry.key == 'version') {
52
+ const isValid = versionType == 'semver' ? re.semVer.test(entry.value)
53
+ : versionType == 'date' ? re.dateVer.test(entry.value)
54
+ : false
55
+ if (!isValid)
56
+ context.report({
57
+ node: entry.comment, messageId: 'invalidVersion', data: { versionType, example }})
58
+ }
59
+ if (['homepage', 'icon', 'icon64', 'require', 'source', 'website'].includes(entry.key)
60
+ || re.urlSuffix.test(entry.key)
61
+ )
62
+ try { new URL(entry.value) }
63
+ catch {
64
+ context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
65
+ }
66
+ if (entry.key == 'resource') {
67
+ const url = entry.value.trim().split(re.whitespace)[1]
68
+ if (url) try { new URL(url) }
69
+ catch {
70
+ context.report({ node: entry.comment, messageId: 'invalidURL', data: { field: entry.key }})
71
+ }
72
+ }
73
+ if (entry.key == 'grant' && allowedGrants.length && !allowedGrants.includes(entry.value))
74
+ context.report({ node: entry.comment, messageId: 'invalidGrant', data: { grant: entry.value }})
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
@@ -0,0 +1,79 @@
1
+ import { extractMetadata } from '../utils.js'
2
+
3
+ const gm_legacy_apis = new Set([ /* eslint-disable no-secrets/no-secrets */
4
+ 'GM_addElement', 'GM_addStyle', 'GM_addValueChangeListener', 'GM_deleteValue', 'GM_download',
5
+ 'GM_getResourceText', 'GM_getResourceURL', 'GM_getTab', 'GM_getTabs', 'GM_getValue', 'GM_getValues',
6
+ 'GM_info', 'GM_listValues', 'GM_log', 'GM_notification', 'GM_openInTab', 'GM_registerMenuCommand',
7
+ 'GM_removeValueChangeListener', 'GM_saveTab', 'GM_setClipboard', 'GM_setValue', 'GM_setValues',
8
+ 'GM_unregisterMenuCommand', 'GM_webRequest', 'GM_xmlhttpRequest'
9
+ ]) /* eslint-enable no-secrets/no-secrets */
10
+
11
+ const gm_modern_apis = new Set([
12
+ 'GM.addElement', 'GM.addStyle', 'GM.addValueChangeListener', 'GM.deleteValue', 'GM.download',
13
+ 'GM.getResourceText', 'GM.getResourceUrl', 'GM.getValue', 'GM.info', 'GM.listValues', 'GM.log',
14
+ 'GM.notification', 'GM.openInTab', 'GM.registerMenuCommand', 'GM.removeValueChangeListener',
15
+ 'GM.setClipboard', 'GM.setValue', 'GM.unregisterMenuCommand', 'GM.xmlHttpRequest'
16
+ ])
17
+
18
+ const sc_apis = new Set([
19
+ 'CAT.agent.conversation', 'CAT.agent.dom', 'CAT.agent.mcp', 'CAT.agent.model',
20
+ 'CAT.agent.opfs', 'CAT.agent.skills', 'CAT.agent.task',
21
+ 'CAT_fileStorage', 'CAT_scriptLoaded', 'CAT_userConfig'
22
+ ])
23
+
24
+ const specialGrants = new Set([
25
+ 'none', 'unsafeWindow', 'window.close', 'window.focus', 'window.onurlchange'
26
+ ])
27
+
28
+ const grants = new Set([...gm_legacy_apis, ...gm_modern_apis, ...sc_apis, ...specialGrants]),
29
+ sortedGrants = new Map(Array.from(grants, grant => [grant.toLowerCase(), grant]))
30
+
31
+ export default {
32
+ meta: {
33
+ type: 'problem',
34
+ docs: {
35
+ description: 'Disallow invalid @grant values',
36
+ category: 'Possible Errors',
37
+ recommended: true
38
+ },
39
+ schema: [{
40
+ type: 'object',
41
+ properties: {
42
+ allowedGrants: {
43
+ type: 'array',
44
+ items: { type: 'string' },
45
+ description: 'Additional @grant values to allow beyond the built-in list'
46
+ }
47
+ },
48
+ additionalProperties: false
49
+ }],
50
+ defaultOptions: [{}],
51
+ messages: {
52
+ invalidGrant: 'Invalid @grant: "{{grant}}"',
53
+ wrongCase: 'Invalid @grant: "{{grant}}" (did you mean "{{suggestion}}"?)'
54
+ }
55
+ },
56
+
57
+ create(context) {
58
+ const options = context.options[0] || {},
59
+ allowedGrants = options.allowedGrants || []
60
+ return {
61
+ Program() {
62
+ const metaEntries = extractMetadata(context.sourceCode),
63
+ grantEntries = metaEntries.filter(e => e.key == 'grant')
64
+ for (const grant of grantEntries) {
65
+ const grantName = grant.value.trim()
66
+ if (grants.has(grantName) || allowedGrants.includes(grantName)) continue
67
+ const suggestion = sortedGrants.get(grantName.toLowerCase())
68
+ if (suggestion)
69
+ context.report({
70
+ node: grant.comment, messageId: 'wrongCase',
71
+ data: { grant: grantName, suggestion }})
72
+ else
73
+ context.report({
74
+ node: grant.comment, messageId: 'invalidGrant', data: { grant: grantName }})
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
package/src/utils.js CHANGED
@@ -1,19 +1,19 @@
1
- export function extractMetadata(sourceCode) {
2
- const comments = sourceCode.getAllComments(),
3
- metaEntries = []
4
- let inMetaBlock = false
5
- for (const comment of comments) {
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('@')) {
10
- const spaceIdx = text.indexOf(' ')
11
- if (spaceIdx > 1) { // '@' at 0, key starts at 1, need space after key
12
- const key = text.slice(1, spaceIdx).trim(),
13
- value = text.slice(spaceIdx +1).trim()
14
- metaEntries.push({ key, value, comment, line: comment.loc.start.line })
15
- }
16
- }
17
- }
18
- return metaEntries
19
- }
1
+ export function extractMetadata(sourceCode) {
2
+ const comments = sourceCode.getAllComments(),
3
+ metaEntries = []
4
+ let inMetaBlock
5
+ for (const comment of comments) {
6
+ const text = comment.value.trim()
7
+ if (!inMetaBlock && text == '==UserScript==') { inMetaBlock = true ; continue }
8
+ else if (inMetaBlock && text == '==/UserScript==') break
9
+ else if (inMetaBlock && text.startsWith('@')) { // capture entry
10
+ const spaceIdx = text.indexOf(' ')
11
+ if (spaceIdx > 1) { // need space after key
12
+ const key = text.slice(1, spaceIdx).trim(),
13
+ value = text.slice(spaceIdx +1).trim()
14
+ metaEntries.push({ key, value, comment, line: comment.loc.start.line })
15
+ }
16
+ }
17
+ }
18
+ return metaEntries
19
+ }