{"record":{"id":"51d5e7452231cf5a","repo":"astral-sh/ruff","slug":"must-be-at-the-end-rule-51d5e7","errorCode":null,"errorMessage":"`?` must be at the end: {rule}","messagePattern":"`\\?` must be at the end: (.+?)","errorType":"validation","errorClass":"ValueError","httpStatus":null,"severity":"error","filePath":"crates/ruff_python_ast/generate.py","lineNumber":274,"sourceCode":"            return sequence_kind, rule[len(prefix) : -len(suffix)]\n\n    return None, rule\n\n\n@dataclass\nclass FieldType:\n    rule: str\n    name: str\n    inner: str\n    sequence_kind: SequenceKind | None = None\n    optional: bool = False\n\n    def __init__(self, rule: str) -> None:\n        self.rule = rule\n        self.optional = False\n        if \"?\" in rule:\n            if not rule.endswith(\"?\") or rule.count(\"?\") != 1:\n                raise ValueError(f\"`?` must be at the end: {rule}\")\n            self.optional = True\n            rule = rule[:-1]\n\n        self.sequence_kind, self.name = split_sequence_type(rule)\n        if self.optional and self.sequence_kind is not None:\n            raise ValueError(f\"optional field cannot be sequence or slice: {self.rule}\")\n        if self.sequence_kind is not None and (\n            not self.name or any(ch in self.name for ch in \"?*&[]<>\")\n        ):\n            raise ValueError(f\"Invalid collection element type: {rule}\")\n\n        self.inner = extract_type_argument(self.name)\n\n\n# ------------------------------------------------------------------------------\n# Preamble\n\n","sourceCodeStart":256,"sourceCodeEnd":292,"githubUrl":"https://github.com/astral-sh/ruff/blob/d1087a4b9e03d253a88703f34e0869ee4b805456/crates/ruff_python_ast/generate.py#L256-L292","documentation":"A trailing '?' marks an optional field (generated as Option<T>). FieldType.__init__ accepts exactly one '?' and only at the end of the rule; shapes like '?Expr', 'Expr??', or 'Vec<X>?' with the marker misplaced raise this ValueError.","triggerScenarios":"Adding an optional field where the '?' is not the last character or appears more than once, then running the AST generator.","commonSituations":"Copy-paste typos; prefix-style optional markers borrowed from regex-like DSLs; editing rules in a hurry without the shapes cheatsheet.","solutions":["Place a single '?' at the very end: 'Expr?'.","Remove any duplicate '?' characters.","Re-run the generator to confirm parsing succeeds."],"exampleFix":"# before\nvalue: ?Expr\n\n# after\nvalue: Expr?","handlingStrategy":"validation","validationCode":"def rule_ok(rule: str) -> bool:\n    return '?' not in rule or (rule.endswith('?') and rule.count('?') == 1)","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Optional marker '?' is always a single trailing character.","Copy the shapes of existing optional fields when adding new ones."],"tags":["codegen","rust-ast","optional-fields","developer-tooling"],"backgroundTag":"codegen-dsl-parse-error","analyzedSha":"d1087a4b9e03d253a88703f34e0869ee4b805456","analyzedAt":"2026-08-20T16:33:49.445Z","contentChangedAt":"2026-08-20T16:33:49.445Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}