eslint-plugin-no-mistakes 0.91.3 → 0.92.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 CHANGED
@@ -34,3 +34,19 @@ test and Vitest config files selected by your flat-config globs. It defaults to
34
34
  5,000 ms for configured test and hook timeouts and 30,000 ms for per-test and
35
35
  hook overrides. See the [rule documentation](../../docs/eslint-rules/vitest-timeout-cap.md)
36
36
  for options and examples.
37
+
38
+ `gh-api-argument-safety` is an opt-in rule for structured endpoint fields and explicit API methods.
39
+ See [options and examples](../../docs/eslint-rules/gh-api-argument-safety.md).
40
+
41
+ `no-argv-main-guard` is an opt-in rule for symlink-sensitive entrypoint comparisons.
42
+ See [options and examples](../../docs/eslint-rules/no-argv-main-guard.md).
43
+
44
+ `no-comment-phrases` is an opt-in rule for configured phrases in line and block comments.
45
+ See [options and examples](../../docs/eslint-rules/no-comment-phrases.md).
46
+
47
+ `forbidden-member-chains` is an opt-in rule for configured static member-access chains.
48
+ See [options and examples](../../docs/eslint-rules/forbidden-member-chains.md).
49
+
50
+ `require-options-on-imported-call` requires a static options object on configured
51
+ imports, globals with no local binding, and terminal methods with a matching receiver shape.
52
+ See [options and examples](../../docs/eslint-rules/require-options-on-imported-call.md).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "eslint-plugin-no-mistakes",
3
- "version": "0.91.3",
3
+ "version": "0.92.0",
4
4
  "description": "ESLint and Oxlint rules that keep code static enough for no-mistakes analyzers and coding agents",
5
5
  "license": "MIT",
6
6
  "repository": {
package/src/index.js CHANGED
@@ -1,6 +1,10 @@
1
1
  "use strict";
2
2
 
3
3
  const rules = {
4
+ "forbidden-member-chains": require("./rules/forbidden-member-chains"),
5
+ "gh-api-argument-safety": require("./rules/gh-api-argument-safety"),
6
+ "no-argv-main-guard": require("./rules/no-argv-main-guard"),
7
+ "no-comment-phrases": require("./rules/no-comment-phrases"),
4
8
  "async-call-disposition": require("./rules/async-call-disposition"),
5
9
  "async-try-catch-return-await": require("./rules/async-try-catch-return-await"),
6
10
  "await-array-methods": require("./rules/await-array-methods"),
@@ -1,11 +1,13 @@
1
1
  "use strict";
2
2
 
3
3
  // Parse actual comments once; directive-looking string literals never suppress.
4
- function ruleSuppression(context, ruleId) {
4
+ // requireReason is opt-in. Callers that omit it keep today's directive match.
5
+ function ruleSuppression(context, ruleId, suppressionOptions) {
5
6
  const lines = new Set();
6
7
  let file = false;
7
8
  const source = context.sourceCode;
8
9
  const first = source.ast.tokens[0];
10
+ const requireReason = suppressionOptions?.requireReason === true;
9
11
  for (const comment of source.getAllComments()) {
10
12
  for (const [offset, text] of comment.value.split(/\r?\n/).entries()) {
11
13
  const match = /^\s*\*?\s*no-mistakes-disable-(file|line|next-line)\b(.*)$/.exec(text);
@@ -20,6 +22,7 @@ function ruleSuppression(context, ruleId) {
20
22
  !names.some((name) => name === ruleId || name === `no-mistakes/${ruleId}`)
21
23
  )
22
24
  continue;
25
+ if (requireReason && !directiveReason(match[2])) continue;
23
26
  if (match[1] === "file") {
24
27
  if (!first || comment.range[1] <= first.range[0]) file = true;
25
28
  } else {
@@ -29,4 +32,11 @@ function ruleSuppression(context, ruleId) {
29
32
  }
30
33
  return (node) => file || lines.has(node.loc.start.line);
31
34
  }
35
+
36
+ function directiveReason(tail) {
37
+ const marker = tail.indexOf("--");
38
+ if (marker === -1) return "";
39
+ return tail.slice(marker + 2).trim();
40
+ }
41
+
32
42
  module.exports = { ruleSuppression };
@@ -0,0 +1,114 @@
1
+ "use strict";
2
+ const { unwrapExpression } = require("./async-ast");
3
+ const { resolveVariable, memberPropertyName, isLocalRequire } = require("./async-target-bindings");
4
+ const { literalString } = require("./module-mock-helpers");
5
+
6
+ function globalName(node, name, context) {
7
+ if (node?.type !== "Identifier" || node.name !== name) return false;
8
+ return !resolveVariable(node, context)?.defs.length;
9
+ }
10
+ function nodeProcess(node, context) {
11
+ if (globalName(node, "process", context)) return true;
12
+ if (node?.type !== "Identifier") return false;
13
+ const variable = resolveVariable(node, context);
14
+ const def = variable?.defs.length === 1 ? variable.defs[0] : null;
15
+ if (def?.type === "ImportBinding") {
16
+ return (
17
+ ["ImportDefaultSpecifier", "ImportNamespaceSpecifier"].includes(def.node.type) &&
18
+ ["process", "node:process"].includes(def.parent.source.value)
19
+ );
20
+ }
21
+ const init = unwrapExpression(def?.node.init);
22
+ return (
23
+ def?.parent?.kind === "const" &&
24
+ def.node.id.type === "Identifier" &&
25
+ init?.type === "CallExpression" &&
26
+ init.callee.type === "Identifier" &&
27
+ init.callee.name === "require" &&
28
+ !isLocalRequire(init.callee, context) &&
29
+ ["process", "node:process"].includes(literalString(init.arguments[0]))
30
+ );
31
+ }
32
+ const ARGV = 1;
33
+ const MAIN = 2;
34
+ const PROCESS = 3;
35
+ const ARGUMENTS = 4;
36
+ function createClassifier(context, wrappers) {
37
+ const stable = new WeakMap();
38
+ const aliases = new WeakMap();
39
+ function classify(input, depth = 0) {
40
+ const node = unwrapExpression(input);
41
+ if (!node || depth > 32) return 0;
42
+ if (node.type === "Identifier") {
43
+ const variable = resolveVariable(node, context);
44
+ if (!variable) {
45
+ if (nodeProcess(node, context)) return PROCESS;
46
+ return globalName(node, "__filename", context) ? MAIN : 0;
47
+ }
48
+ if (!stable.has(variable))
49
+ stable.set(variable, !variable.references.some((r) => r.isWrite() && !r.init));
50
+ if (!stable.get(variable)) return 0;
51
+ if (nodeProcess(node, context)) return PROCESS;
52
+ if (globalName(node, "__filename", context)) return MAIN;
53
+ const def = variable.defs.length === 1 ? variable.defs[0] : null;
54
+ if (
55
+ def?.type !== "Variable" ||
56
+ def.parent.kind !== "const" ||
57
+ def.node.id.type !== "Identifier" ||
58
+ def.node.range[1] > node.range[0]
59
+ )
60
+ return 0;
61
+ if (!aliases.has(variable)) {
62
+ aliases.set(variable, 0);
63
+ aliases.set(variable, classify(def.node.init, depth + 1));
64
+ }
65
+ return aliases.get(variable);
66
+ }
67
+ if (node.type === "MemberExpression") {
68
+ const name = memberPropertyName(node);
69
+ if (
70
+ node.object.type === "MetaProperty" &&
71
+ node.object.meta.name === "import" &&
72
+ node.object.property.name === "meta" &&
73
+ ["url", "filename"].includes(name)
74
+ )
75
+ return MAIN;
76
+ const object = classify(node.object, depth + 1);
77
+ if (object === PROCESS && name === "argv") return ARGUMENTS;
78
+ if (
79
+ object === ARGUMENTS &&
80
+ node.computed &&
81
+ node.property.type === "Literal" &&
82
+ node.property.value === 1
83
+ )
84
+ return ARGV;
85
+ // A URL conversion's href preserves the same module/argv identity.
86
+ if (
87
+ name === "href" &&
88
+ unwrapExpression(node.object).type === "CallExpression" &&
89
+ wrappers.resolveCallTarget(unwrapExpression(node.object))?.calleeName === "pathToFileURL"
90
+ )
91
+ return object;
92
+ }
93
+ if (node.type === "CallExpression") {
94
+ const callee = unwrapExpression(node.callee);
95
+ if (
96
+ callee?.type === "MemberExpression" &&
97
+ memberPropertyName(callee) === "at" &&
98
+ classify(callee.object, depth + 1) === ARGUMENTS &&
99
+ node.arguments.length === 1 &&
100
+ node.arguments[0].type === "Literal" &&
101
+ node.arguments[0].value === 1
102
+ )
103
+ return ARGV;
104
+ if (node.arguments.length !== 1 || node.arguments[0].type === "SpreadElement") return 0;
105
+ if (globalName(callee, "Boolean", context) || wrappers.resolveCallTarget(node)) {
106
+ const value = classify(node.arguments[0], depth + 1);
107
+ return value === ARGV || value === MAIN ? value : 0;
108
+ }
109
+ }
110
+ return 0;
111
+ }
112
+ return (node) => classify(node);
113
+ }
114
+ module.exports = { createClassifier, ARGV, MAIN };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ const { matches, expressionInfo } = require("./forbidden-member-chains-match");
4
+
5
+ const PARAM_PARENTS = new Set([
6
+ "FunctionDeclaration",
7
+ "FunctionExpression",
8
+ "ArrowFunctionExpression",
9
+ ]);
10
+
11
+ function patternKey(prop) {
12
+ const key = prop.key;
13
+ if (key.type === "Identifier") return key.name;
14
+ if (key.type === "Literal" && (typeof key.value === "string" || typeof key.value === "number")) {
15
+ return String(key.value);
16
+ }
17
+ return null;
18
+ }
19
+
20
+ function objectPattern(node) {
21
+ let current = node;
22
+ while (current?.type === "AssignmentPattern") current = current.left;
23
+ return current?.type === "ObjectPattern" ? current : null;
24
+ }
25
+
26
+ function destructureVisitors({ compiled, context, suppressed, info }) {
27
+ function report(node, chain) {
28
+ if (suppressed(node)) return;
29
+ context.report({ node, messageId: "forbiddenChain", data: { message: chain.message } });
30
+ }
31
+
32
+ // Computed keys are unproven, including string and numeric literals.
33
+ // A chain already matched by an outer property is not reported again.
34
+ function walk(pattern, chainInfo, blocked) {
35
+ for (const prop of pattern.properties) {
36
+ if (prop.type !== "Property" || prop.computed) continue;
37
+ const name = patternKey(prop);
38
+ if (name == null) continue;
39
+ const next = { segments: chainInfo.segments.concat(name), rooted: chainInfo.rooted };
40
+ const blockedHere = new Set(blocked);
41
+ for (const chain of compiled) {
42
+ if (blockedHere.has(chain) || !matches(next, chain)) continue;
43
+ report(prop, chain);
44
+ blockedHere.add(chain);
45
+ }
46
+ const nested = objectPattern(prop.value);
47
+ if (nested) walk(nested, next, blockedHere);
48
+ }
49
+ }
50
+
51
+ function check(pattern, receiver) {
52
+ const chainInfo = expressionInfo(receiver, info);
53
+ if (!chainInfo) return;
54
+ walk(pattern, chainInfo, new Set());
55
+ }
56
+
57
+ return {
58
+ VariableDeclarator(node) {
59
+ const pattern = objectPattern(node.id);
60
+ if (pattern && node.init) check(pattern, node.init);
61
+ },
62
+ AssignmentExpression(node) {
63
+ const pattern = objectPattern(node.left);
64
+ if (pattern) check(pattern, node.right);
65
+ },
66
+ AssignmentPattern(node) {
67
+ if (!PARAM_PARENTS.has(node.parent?.type)) return;
68
+ const pattern = objectPattern(node.left);
69
+ if (pattern) check(pattern, node.right);
70
+ },
71
+ };
72
+ }
73
+
74
+ module.exports = { destructureVisitors };
@@ -0,0 +1,120 @@
1
+ "use strict";
2
+
3
+ const MAX_WALK = 32;
4
+ const TRANSPARENT = new Set([
5
+ "ChainExpression",
6
+ "ParenthesizedExpression",
7
+ "TSAsExpression",
8
+ "TSSatisfiesExpression",
9
+ "TSNonNullExpression",
10
+ "TSTypeAssertion",
11
+ "TSInstantiationExpression",
12
+ "TypeCastExpression",
13
+ ]);
14
+
15
+ function unwrap(node) {
16
+ let current = node;
17
+ while (current && TRANSPARENT.has(current.type)) current = current.expression;
18
+ return current;
19
+ }
20
+
21
+ function literalKey(node) {
22
+ const value = unwrap(node);
23
+ if (
24
+ value?.type === "Literal" &&
25
+ (typeof value.value === "string" || typeof value.value === "number")
26
+ ) {
27
+ return String(value.value);
28
+ }
29
+ if (value?.type === "TemplateLiteral" && value.expressions.length === 0) {
30
+ return value.quasis.map((quasi) => quasi.value.cooked).join("");
31
+ }
32
+ return null;
33
+ }
34
+
35
+ function staticProperty(member) {
36
+ if (member.computed) return literalKey(member.property);
37
+ const property = member.property;
38
+ return property.type === "PrivateIdentifier" ? `#${property.name}` : property.name;
39
+ }
40
+
41
+ function rooted(segments, node) {
42
+ if (node?.type === "Identifier") segments.unshift(node.name);
43
+ else if (node?.type === "ThisExpression") segments.unshift("this");
44
+ else if (node?.type === "Super") segments.unshift("super");
45
+ else if (node?.type === "MetaProperty") segments.unshift(node.meta.name, node.property.name);
46
+ else return false;
47
+ return true;
48
+ }
49
+
50
+ function computeChain(member) {
51
+ const segments = [];
52
+ let current = member;
53
+ for (let steps = 0; current?.type === "MemberExpression"; steps += 1) {
54
+ if (steps >= MAX_WALK) return null;
55
+ const name = staticProperty(current);
56
+ if (name == null) return { segments, rooted: false };
57
+ segments.unshift(name);
58
+ current = unwrap(current.object);
59
+ }
60
+ return { segments, rooted: rooted(segments, current) };
61
+ }
62
+
63
+ function matches(info, chain) {
64
+ if (!info) return false;
65
+ const { segments, rooted: fromRoot } = info;
66
+ const { parts } = chain;
67
+ if (segments.length < parts.length) return false;
68
+ const start = segments.length - parts.length;
69
+ for (let index = 0; index < parts.length; index += 1) {
70
+ if (segments[start + index] !== parts[index]) return false;
71
+ }
72
+ // A wildcard needs a receiver. An opaque receiver is not one of the segments.
73
+ if (!chain.wildcard) return fromRoot && start === 0;
74
+ return start > 0 || !fromRoot;
75
+ }
76
+
77
+ function surface(node) {
78
+ let current = node;
79
+ while (
80
+ current.parent &&
81
+ TRANSPARENT.has(current.parent.type) &&
82
+ unwrap(current.parent) === node
83
+ ) {
84
+ current = current.parent;
85
+ }
86
+ return current;
87
+ }
88
+
89
+ function hasMatchingAncestor(node, chain, info) {
90
+ let current = node;
91
+ for (;;) {
92
+ const parent = surface(current).parent;
93
+ if (!parent || parent.type !== "MemberExpression" || unwrap(parent.object) !== current) {
94
+ return false;
95
+ }
96
+ if (matches(info(parent), chain)) return true;
97
+ current = parent;
98
+ }
99
+ }
100
+
101
+ function invocationOf(member) {
102
+ const parent = surface(member).parent;
103
+ if (parent.type !== "CallExpression" && parent.type !== "NewExpression") return null;
104
+ return unwrap(parent.callee) === member ? parent : null;
105
+ }
106
+
107
+ function expressionInfo(node, memberInfo) {
108
+ const value = unwrap(node);
109
+ if (value.type === "MemberExpression") return memberInfo(value);
110
+ const segments = [];
111
+ return { segments, rooted: rooted(segments, value) };
112
+ }
113
+
114
+ module.exports = {
115
+ matches,
116
+ computeChain,
117
+ hasMatchingAncestor,
118
+ invocationOf,
119
+ expressionInfo,
120
+ };
@@ -0,0 +1,104 @@
1
+ "use strict";
2
+
3
+ const MAX_SEGMENTS = 16;
4
+ // A segment is non-empty and is not exactly "*". The star is only a leading receiver.
5
+ const SEGMENT = "(?!\\*(?:\\.|$))[^.]+";
6
+ const OPEN_PATH = `^(?:\\*(?:\\.${SEGMENT}){1,15}|${SEGMENT}(?:\\.${SEGMENT}){0,15})$`;
7
+ // A named receiver replaces a leading "*" and still counts toward 16 segments.
8
+ const NAMED_PATH = `^(?:\\*|\\*(?:\\.${SEGMENT}){1,15}|${SEGMENT}(?:\\.${SEGMENT}){0,14})$`;
9
+
10
+ function text(maxLength, pattern) {
11
+ return pattern
12
+ ? { type: "string", minLength: 1, maxLength, pattern }
13
+ : { type: "string", minLength: 1, maxLength };
14
+ }
15
+
16
+ function nonStar() {
17
+ return text(128, "^(?!\\*$)[\\s\\S]+$");
18
+ }
19
+
20
+ function pathString(pattern) {
21
+ return text(256, pattern);
22
+ }
23
+
24
+ function segments(maxItems) {
25
+ return { type: "array", minItems: 1, maxItems, items: nonStar() };
26
+ }
27
+
28
+ function starThen(minItems, maxItems) {
29
+ return {
30
+ type: "array",
31
+ minItems,
32
+ maxItems,
33
+ items: [{ enum: ["*"] }],
34
+ additionalItems: nonStar(),
35
+ };
36
+ }
37
+
38
+ function pathSchema(pattern, starMin, plainMax, starMax) {
39
+ return {
40
+ anyOf: [pathString(pattern), segments(plainMax), starThen(starMin, starMax)],
41
+ };
42
+ }
43
+
44
+ function chain(properties, required) {
45
+ return {
46
+ type: "object",
47
+ additionalProperties: false,
48
+ required,
49
+ properties: { message: text(512), ...properties },
50
+ };
51
+ }
52
+
53
+ const schema = [
54
+ {
55
+ type: "object",
56
+ additionalProperties: false,
57
+ properties: {
58
+ chains: {
59
+ type: "array",
60
+ maxItems: 64,
61
+ items: {
62
+ anyOf: [
63
+ chain({ path: pathSchema(OPEN_PATH, 2, MAX_SEGMENTS, MAX_SEGMENTS) }, [
64
+ "path",
65
+ "message",
66
+ ]),
67
+ chain(
68
+ {
69
+ path: pathSchema(OPEN_PATH, 2, MAX_SEGMENTS, MAX_SEGMENTS),
70
+ receiver: { type: "string", enum: ["*"] },
71
+ },
72
+ ["path", "message", "receiver"],
73
+ ),
74
+ chain(
75
+ {
76
+ path: pathSchema(NAMED_PATH, 1, MAX_SEGMENTS - 1, MAX_SEGMENTS),
77
+ receiver: nonStar(),
78
+ },
79
+ ["path", "message", "receiver"],
80
+ ),
81
+ ],
82
+ },
83
+ },
84
+ },
85
+ },
86
+ ];
87
+
88
+ function compile(entry) {
89
+ const parts = Array.isArray(entry.path) ? entry.path.slice() : entry.path.split(".");
90
+ let wildcard = false;
91
+ let body = parts;
92
+ if (body[0] === "*") {
93
+ wildcard = true;
94
+ body = body.slice(1);
95
+ }
96
+ if (entry.receiver === "*") wildcard = true;
97
+ else if (typeof entry.receiver === "string") {
98
+ body = [entry.receiver, ...body];
99
+ wildcard = false;
100
+ }
101
+ return { wildcard, parts: body, message: entry.message };
102
+ }
103
+
104
+ module.exports = { schema, compile };
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+
3
+ const { ruleSuppression } = require("../rule-suppression");
4
+ const { schema, compile } = require("./forbidden-member-chains-schema");
5
+ const {
6
+ matches,
7
+ computeChain,
8
+ hasMatchingAncestor,
9
+ invocationOf,
10
+ } = require("./forbidden-member-chains-match");
11
+ const { destructureVisitors } = require("./forbidden-member-chains-destructure");
12
+
13
+ module.exports = {
14
+ meta: {
15
+ type: "problem",
16
+ docs: { description: "ban configured static member-access chains", recommended: false },
17
+ schema,
18
+ messages: { forbiddenChain: "{{message}}" },
19
+ },
20
+ create(context) {
21
+ const compiled = [];
22
+ const entries = context.options[0]?.chains;
23
+ if (Array.isArray(entries)) {
24
+ for (const entry of entries) compiled.push(compile(entry));
25
+ }
26
+ if (compiled.length === 0) return {};
27
+ const suppressed = ruleSuppression(context, "forbidden-member-chains");
28
+ const cache = new WeakMap();
29
+ const info = (member) => {
30
+ if (cache.has(member)) return cache.get(member);
31
+ const value = computeChain(member);
32
+ cache.set(member, value);
33
+ return value;
34
+ };
35
+ const report = (node, chain) => {
36
+ if (suppressed(node)) return;
37
+ context.report({ node, messageId: "forbiddenChain", data: { message: chain.message } });
38
+ };
39
+ return {
40
+ MemberExpression(node) {
41
+ for (const chain of compiled) {
42
+ if (!matches(info(node), chain) || hasMatchingAncestor(node, chain, info)) continue;
43
+ report(invocationOf(node) || node, chain);
44
+ }
45
+ },
46
+ ...destructureVisitors({ compiled, context, suppressed, info }),
47
+ };
48
+ },
49
+ };
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ const { createTargetMatcher } = require("./async-targets");
3
+ const { unwrapExpression } = require("./async-ast");
4
+ const { resolveVariable, memberPropertyName } = require("./async-target-bindings");
5
+ const { ruleSuppression } = require("../rule-suppression");
6
+ const { inspectArgv, token } = require("./gh-api-argv");
7
+
8
+ function wrapperName(node, context, stable) {
9
+ node = unwrapExpression(node);
10
+ const names = [];
11
+ while (node?.type === "MemberExpression") {
12
+ const name = memberPropertyName(node);
13
+ if (!name) return null;
14
+ names.unshift(name);
15
+ node = unwrapExpression(node.object);
16
+ }
17
+ if (node?.type !== "Identifier") return null;
18
+ const variable = resolveVariable(node, context);
19
+ // A configured module wrapper never means a same-spelled nested parameter.
20
+ if (variable && !["module", "global"].includes(variable.scope.type)) return null;
21
+ if (variable) {
22
+ if (!stable.has(variable))
23
+ stable.set(
24
+ variable,
25
+ !variable.references.some((reference) => reference.isWrite() && !reference.init),
26
+ );
27
+ if (!stable.get(variable)) return null;
28
+ }
29
+ return [node.name, ...names].join(".");
30
+ }
31
+ module.exports = {
32
+ meta: {
33
+ type: "problem",
34
+ docs: {
35
+ description: "require structured endpoint fields and explicit gh api methods",
36
+ recommended: false,
37
+ },
38
+ schema: [
39
+ {
40
+ type: "object",
41
+ properties: {
42
+ wrappers: {
43
+ type: "array",
44
+ maxItems: 64,
45
+ items: {
46
+ type: "object",
47
+ properties: {
48
+ callee: { type: "string", minLength: 1 },
49
+ argumentPosition: { type: "integer", minimum: 1, maximum: 32 },
50
+ includesApi: { type: "boolean" },
51
+ },
52
+ required: ["callee", "argumentPosition"],
53
+ additionalProperties: false,
54
+ },
55
+ },
56
+ endpointMessage: { type: "string", minLength: 1 },
57
+ methodMessage: { type: "string", minLength: 1 },
58
+ },
59
+ additionalProperties: false,
60
+ },
61
+ ],
62
+ messages: {
63
+ endpointQuery: "{{message}}",
64
+ missingMethod: "{{message}}",
65
+ },
66
+ },
67
+ create(context) {
68
+ const options = context.options[0] || {};
69
+ const stable = new WeakMap();
70
+ const matcherContext = Object.create(context);
71
+ Object.defineProperty(matcherContext, "options", {
72
+ value: [
73
+ {
74
+ targets: [
75
+ {
76
+ sourceSpecifierPatterns: ["node:child_process", "child_process"],
77
+ calleeNamePatterns: ["execFile", "execFileSync", "spawn", "spawnSync"],
78
+ },
79
+ ],
80
+ },
81
+ ],
82
+ });
83
+ const matcher = createTargetMatcher(matcherContext);
84
+ const suppressed = ruleSuppression(context, "gh-api-argument-safety");
85
+ return {
86
+ ...matcher.visitors,
87
+ CallExpression(node) {
88
+ let array;
89
+ let includesApi = true;
90
+ if (matcher.resolveCallTarget(node) && token(node.arguments[0]) === "gh") {
91
+ array = unwrapExpression(node.arguments[1]);
92
+ } else {
93
+ const name = wrapperName(node.callee, context, stable);
94
+ const wrapper = (options.wrappers || []).find((entry) => entry.callee === name);
95
+ if (!wrapper) return;
96
+ array = unwrapExpression(node.arguments[wrapper.argumentPosition - 1]);
97
+ includesApi = wrapper.includesApi !== false;
98
+ }
99
+ if (suppressed(node)) return;
100
+ const result = inspectArgv(array, includesApi);
101
+ if (!result) return;
102
+ if (result.endpoint && !suppressed(result.endpoint))
103
+ context.report({
104
+ node: result.endpoint,
105
+ messageId: "endpointQuery",
106
+ data: {
107
+ message:
108
+ options.endpointMessage ||
109
+ "Use structured -f/-F fields instead of an endpoint query string.",
110
+ },
111
+ });
112
+ if (result.missingMethod)
113
+ context.report({
114
+ node,
115
+ messageId: "missingMethod",
116
+ data: {
117
+ message:
118
+ options.methodMessage ||
119
+ "Set an explicit -X/--method when gh api uses field flags (for example -X GET).",
120
+ },
121
+ });
122
+ },
123
+ };
124
+ },
125
+ };