fix: classify Linear raw GraphQL mutations safely (#59)

Credentialed four-epoch SkillOpt validation. Exact-head independent review passed; validate check green.
This commit is contained in:
Magnus Hedemark
2026-07-17 14:07:56 -04:00
committed by GitHub
parent 672f6190b0
commit b5e0eec43f
2 changed files with 66 additions and 2 deletions
+53 -2
View File
@@ -79,8 +79,59 @@ def parse_document_ref(ref):
def is_mutation(query):
stripped = re.sub(r"#[^\n]*", "", query)
return bool(re.search(r"\bmutation\b", stripped, re.I))
index = 0
braces = parens = brackets = 0
definition = None
while index < len(query):
if query.startswith('"""', index):
index += 3
while index < len(query):
if query.startswith('"""', index):
cursor = index - 1
while cursor >= 0 and query[cursor] == "\\":
cursor -= 1
if (index - cursor - 1) % 2 == 0:
index += 3
break
index += 1
continue
if query[index] == '"':
index += 1
while index < len(query) and query[index] != '"':
index += 2 if query[index] == "\\" else 1
index += 1
continue
if query[index] == "#":
newline = query.find("\n", index)
index = len(query) if newline < 0 else newline + 1
continue
match = re.match(r"[_A-Za-z][_0-9A-Za-z]*", query[index:])
if match:
token = match.group(0).lower()
if not (braces or parens or brackets) and definition is None:
if token == "mutation":
return True
if token in ("query", "subscription", "fragment"):
definition = token
index += len(match.group(0))
continue
char = query[index]
if char == "{":
braces += 1
elif char == "}":
braces = max(0, braces - 1)
if not (braces or parens or brackets):
definition = None
elif char == "(":
parens += 1
elif char == ")":
parens = max(0, parens - 1)
elif char == "[":
brackets += 1
elif char == "]":
brackets = max(0, brackets - 1)
index += 1
return False
class Client:
+13
View File
@@ -303,6 +303,19 @@ class LinearCliTests(unittest.TestCase):
)
)
def test_is_mutation_ignores_read_contexts(self):
self.assertFalse(cli.is_mutation('query { __type(name: "Mutation") { name } }'))
self.assertFalse(cli.is_mutation("query { mutation: viewer { id } }"))
self.assertFalse(cli.is_mutation("fragment mutation on Issue { id }"))
self.assertFalse(
cli.is_mutation(r'query { __type(name: """hello\""" mutation""") { name } }')
)
self.assertTrue(
cli.is_mutation(
r'fragment F on Mutation { issueArchive(id: """hello\\""") { success } } mutation { ...F }'
)
)
def test_raw_with_comment_mutation_exits_confirm(self):
code, _output, error = self.run_cli(
["raw", '# comment\nmutation { issueArchive(id: "x") { success } }']