{"record":{"id":"859a5b803cb5f0d0","repo":"jdx/mise","slug":"each-version-token-has-one-matching-kind","errorCode":null,"errorMessage":"each version token has one matching kind","messagePattern":"each version token has one matching kind","errorType":"panic","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/packages/brew/cask/app_version.rs","lineNumber":257,"sourceCode":"        TokenKind::Pre,\n        TokenKind::Rc,\n        TokenKind::Patch,\n        TokenKind::Post,\n        TokenKind::Numeric,\n        TokenKind::Text,\n    ];\n    PATTERN\n        .captures_iter(version)\n        .map(|captures| {\n            let (kind, text) = kinds\n                .iter()\n                .enumerate()\n                .find_map(|(index, kind)| {\n                    captures\n                        .get(index + 1)\n                        .map(|matched| (*kind, matched.as_str()))\n                })\n                .expect(\"each version token has one matching kind\");\n            Token { kind, text }\n        })\n        .collect()\n}\n\n/// Compares bundle versions using Homebrew token ordering rather than semver.\n///\n/// Returns `None` for blank inputs or unequal dot-separated component counts,\n/// ignoring trailing dots. Homebrew treats those forms as incomparable rather\n/// than padding their components. Comparable `HEAD` versions sort above releases.\npub(super) fn compare_app_versions(first: &str, second: &str) -> Option<Ordering> {\n    let dot_count = |value: &str| {\n        let value = value.trim_end_matches('.');\n        if value.is_empty() {\n            0\n        } else {\n            value.split('.').count()\n        }","sourceCodeStart":239,"sourceCodeEnd":275,"githubUrl":"https://github.com/jdx/mise/blob/533346cc374382b41ec5ff70536252b2e96e725c/src/system/packages/brew/cask/app_version.rs#L239-L275","documentation":"In `tokens()`, after the version-token regex matches, the code finds the first capture group that participated and expects one to exist (`.expect(\"each version token has one matching kind\")`). Since the whole pattern is a single alternation, exactly one alternative matches; a panic means a match was produced with zero participating groups — a logic error in the regex structure.","triggerScenarios":"`tokens()` matching a version substring where `captures.get(index + 1)` returns None for all kind groups (src/system/packages/brew/cask/app_version.rs:257) — possible only if the regex is edited so a match can occur without any alternative group, or the kinds array is misaligned with the group count.","commonSituations":"Refactoring the regex (adding/removing groups) without keeping the `kinds` array in sync; a nested/optional group that matches empty and shadows the real alternative.","solutions":["Keep the `kinds` array length and order aligned with the regex capture groups","Add a test asserting group-count == kinds.len() so desync is caught in CI","Ensure the outer group wraps the entire alternation so every match populates one kind group"],"exampleFix":"// before\nlet kinds = [Alpha, Beta, Pre]; // fewer kinds than groups\n// after\nlet kinds = [Alpha, Beta, Pre, Rc, Patch, Post, Number, Word]; // matches 8 groups","handlingStrategy":"validation","validationCode":"assert_eq!(PATTERN.captures_len() - 1, kinds.len(), \"regex groups must match kinds\");","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Update the kinds array whenever the regex alternation changes","Add a test that tokenizes sample versions (1.2.3, 1.0b1, 2.0rc1)","Avoid optional inner groups that can match without a kind"],"tags":["panic","invariant","regex","homebrew"],"backgroundTag":"internal-invariant-violation","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}