diff --git a/linear/scripts/linear b/linear/scripts/linear index 08b86cf..26a3775 100755 --- a/linear/scripts/linear +++ b/linear/scripts/linear @@ -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: diff --git a/linear/tests/test_linear.py b/linear/tests/test_linear.py index f782fa1..9be1ec8 100644 --- a/linear/tests/test_linear.py +++ b/linear/tests/test_linear.py @@ -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 } }']