{"record":{"id":"f6208f74bbb2a7c5","repo":"jdx/mise","slug":"expected-closing-parenthesis","errorCode":null,"errorMessage":"expected closing parenthesis","messagePattern":"expected closing parenthesis","errorType":"validation","errorClass":"eyre::Report","httpStatus":null,"severity":"error","filePath":"crates/aqua-registry/src/template.rs","lineNumber":244,"sourceCode":"    Ok(left)\n}\n\n/// Parse primary expressions\nfn parse_primary(tokens: &mut std::iter::Peekable<std::slice::Iter<Token>>) -> Result<Expr> {\n    skip_whitespace(tokens);\n\n    let token = tokens.next().wrap_err(\"unexpected end of expression\")?;\n\n    let expr = match token {\n        Token::Key(k) => Expr::Var(k.to_string()),\n        Token::String(s) => Expr::Literal(s.to_string()),\n        Token::LParen => {\n            // Parenthesized expression: (func arg)\n            skip_whitespace(tokens);\n            let inner = parse_pipe(tokens)?;\n            skip_whitespace(tokens);\n            if !matches!(tokens.next(), Some(Token::RParen)) {\n                bail!(\"expected closing parenthesis\");\n            }\n            inner\n        }\n        Token::Func(f) => {\n            // Function call: func arg1 arg2\n            let func_name = f.to_string();\n            let mut args = Vec::new();\n\n            // Collect arguments until we hit pipe, rparen, or end\n            loop {\n                skip_whitespace(tokens);\n\n                match tokens.peek() {\n                    None | Some(Token::Pipe) | Some(Token::RParen) => break,\n                    Some(Token::Dot) | Some(Token::Ident(_)) => break, // Stop before property access\n                    _ => {\n                        args.push(parse_arg(tokens)?);\n                    }","sourceCodeStart":226,"sourceCodeEnd":262,"githubUrl":"https://github.com/jdx/mise/blob/9dcfcaa0dc8747a2577d3270b69bb9d8313b2807/crates/aqua-registry/src/template.rs#L226-L262","documentation":"mise requires that any cask containing pkg artifacts also declares pkgutil receipt ids in its uninstall metadata, mirroring Homebrew's own rule — without pkgutil ids there is no way to track or uninstall an installed pkg. After dedup, if pkgs is non-empty but pkg_ids is empty, the install aborts.","triggerScenarios":"Cask JSON with a 'pkg' artifact but no uninstall: {pkgutil: [...]} entry; uninstall metadata listing only delete/quit entries while a pkg stanza exists.","commonSituations":"Hand-written casks missing the uninstall stanza; third-party taps with incomplete casks; upstream cask lint rules normally catch this, so it usually means non-canonical metadata.","solutions":["If you maintain the cask, add the pkg's receipt id to the uninstall stanza (get it via pkgutil --pkgs or the pkg's Distribution file)","Install with upstream brew, which may have looser handling, and report the cask to its tap","Update mise in case newer metadata from the API resolves the mismatch"],"exampleFix":"# cask JSON\n# before\n{\"artifacts\": [{\"pkg\": [\"Installer.pkg\"]}], \"uninstall\": {}}\n# after\n{\"artifacts\": [{\"pkg\": [\"Installer.pkg\"]}], \"uninstall\": {\"pkgutil\": [\"com.example.installer\"]}}","handlingStrategy":"validation","validationCode":"let has_pkg = artifacts.iter().any(|a| artifact_type(a) == \"pkg\");\nlet has_pkgutil = cask.uninstall.pkgutil.is_some_and(|ids| !ids.is_empty());\nif has_pkg && !has_pkgutil { /* reject the cask before install */ }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pair pkg stanzas with uninstall pkgutil ids when authoring casks","Run brew audit / cask linters on third-party casks before use"],"tags":["rust","mise","brew-cask","pkg-artifact","metadata-validation","macos"],"backgroundTag":"missing-required-metadata","analyzedSha":"9dcfcaa0dc8747a2577d3270b69bb9d8313b2807","analyzedAt":"2026-08-17T14:28:50.624Z","schemaVersion":2},"datasetVersion":"2026-08-21T18:17:14.833Z"}