{"record":{"id":"232d3a4bc77f621f","repo":"usememos/memos","slug":"expression-is-not-an-identifier","errorCode":null,"errorMessage":"expression is not an identifier","messagePattern":"expression is not an identifier","errorType":"validation","errorClass":null,"httpStatus":400,"severity":"error","filePath":"internal/filter/parser.go","lineNumber":462,"sourceCode":"\t\treturn CompareNeq, nil\n\tcase \"_<_\":\n\t\treturn CompareLt, nil\n\tcase \"_>_\":\n\t\treturn CompareGt, nil\n\tcase \"_<=_\":\n\t\treturn CompareLte, nil\n\tcase \"_>=_\":\n\t\treturn CompareGte, nil\n\tdefault:\n\t\treturn \"\", errors.Errorf(\"unsupported comparison operator %q\", fn)\n\t}\n}\n\nfunc getIdentName(expr *exprv1.Expr) (string, error) {\n\tif ident := expr.GetIdentExpr(); ident != nil {\n\t\treturn ident.GetName(), nil\n\t}\n\treturn \"\", errors.New(\"expression is not an identifier\")\n}\n\nfunc getConstValue(expr *exprv1.Expr) (interface{}, error) {\n\tv, ok := expr.ExprKind.(*exprv1.Expr_ConstExpr)\n\tif !ok {\n\t\treturn nil, errors.New(\"expression is not a literal\")\n\t}\n\tswitch x := v.ConstExpr.ConstantKind.(type) {\n\tcase *exprv1.Constant_StringValue:\n\t\treturn v.ConstExpr.GetStringValue(), nil\n\tcase *exprv1.Constant_Int64Value:\n\t\treturn v.ConstExpr.GetInt64Value(), nil\n\tcase *exprv1.Constant_Uint64Value:\n\t\treturn int64(v.ConstExpr.GetUint64Value()), nil\n\tcase *exprv1.Constant_DoubleValue:\n\t\treturn v.ConstExpr.GetDoubleValue(), nil\n\tcase *exprv1.Constant_BoolValue:\n\t\treturn v.ConstExpr.GetBoolValue(), nil","sourceCodeStart":444,"sourceCodeEnd":480,"githubUrl":"https://github.com/usememos/memos/blob/14d757ce1fb31c78590f374bc042f8dbedbc20d7/internal/filter/parser.go#L444-L480","documentation":"getIdentName extracts the field name from an expression and only succeeds when the expression is an identifier (exprv1.Expr_IdentExpr). Callers use it for receivers of contains/matches, targets of set operations, and comprehension ranges; when the sub-expression is a call, literal, or select, this error surfaces.","triggerScenarios":"Filters where a field position holds a non-identifier: content.size().contains(\"x\") (method chained on a call), 5.contains(\"x\"), or (a || b).matches(\"x\").","commonSituations":"Chaining calls so the receiver is itself a call result; wrapping field names in parentheses or expressions; typos that turn an identifier into a function call.","solutions":["Put a bare schema field where a field is expected: content.contains(\"x\")","Apply size() inside comparisons only: size(tag) > 1, never as a text-match receiver","Simplify the left side to a single identifier before the dot"],"exampleFix":"// before\ncontent.size().contains(\"x\")\n\n// after\ncontent.contains(\"x\")","handlingStrategy":"validation","validationCode":"// Go (AST level): assert the receiver is an identifier\nif call.GetTarget().GetIdentExpr() == nil {\n    return errors.New(\"receiver must be a bare field name\")\n}","typeGuard":"func isIdent(e *exprv1.Expr) bool { return e.GetIdentExpr() != nil }","tryCatchPattern":null,"preventionTips":["Write a bare field before the dot in contains/matches calls","Never chain method calls in the filter DSL","If you build ASTs, validate receivers are identifiers before compiling"],"tags":["cel","filter","identifier","parser"],"backgroundTag":null,"analyzedSha":"14d757ce1fb31c78590f374bc042f8dbedbc20d7","analyzedAt":"2026-08-15T09:27:36.538Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}