1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
/// A parsed search query.
#[derive(Debug, Clone)]
pub enum Query {
/// Functions whose return type contains a given type name.
/// `returns Maybe`, `returns Result`
ReturnsType(String),
/// Functions whose signature mentions a given type anywhere.
/// `type Html`, `type Decoder`
UsesType(String),
/// Case expressions matching on a given constructor/type.
/// `case-on Result`, `case-on Maybe`
CaseOn(String),
/// Record updates touching a given field.
/// `update .name`, `update .count`
RecordUpdateField(String),
/// All qualified calls to a module.
/// `calls Http`, `calls Json.Decode`
CallsTo(String),
/// Functions with arguments that are never used in the body.
/// `unused-args`
UnusedArgs,
/// Lambdas with at least N arguments.
/// `lambda 3` (lambdas with 3+ args)
LambdaArity(usize),
/// Functions that use a specific function/value name.
/// `uses map`, `uses andThen`
Uses(String),
/// Top-level functions matching a name pattern (substring).
/// `def update`, `def view`
Defined(String),
/// Expressions of a specific shape.
/// `expr let` (let expressions), `expr if` (if expressions)
ExprKind(ExprKindQuery),
}
#[derive(Debug, Clone)]
pub enum ExprKindQuery {
Let,
Case,
If,
Lambda,
Record,
List,
Tuple,
}
/// Parse a query string into a Query.
pub fn parse_query(input: &str) -> Result<Query, String> {
let parts: Vec<&str> = input.splitn(2, ' ').collect();
let cmd = parts[0].to_lowercase();
let arg = parts.get(1).map(|s| s.trim()).unwrap_or("");
match cmd.as_str() {
"returns" => {
if arg.is_empty() {
return Err("'returns' requires a type name, e.g. 'returns Maybe'".into());
}
Ok(Query::ReturnsType(arg.to_string()))
}
"type" => {
if arg.is_empty() {
return Err("'type' requires a type name, e.g. 'type Html'".into());
}
Ok(Query::UsesType(arg.to_string()))
}
"case-on" | "case" => {
if arg.is_empty() {
return Err("'case-on' requires a type/constructor name".into());
}
Ok(Query::CaseOn(arg.to_string()))
}
"update" => {
let field = arg.trim_start_matches('.');
if field.is_empty() {
return Err("'update' requires a field name, e.g. 'update .name'".into());
}
Ok(Query::RecordUpdateField(field.to_string()))
}
"calls" => {
if arg.is_empty() {
return Err("'calls' requires a module name, e.g. 'calls Http'".into());
}
Ok(Query::CallsTo(arg.to_string()))
}
"unused-args" => Ok(Query::UnusedArgs),
"lambda" => {
let n: usize = arg
.parse()
.map_err(|_| "'lambda' requires a number, e.g. 'lambda 3'")?;
Ok(Query::LambdaArity(n))
}
"uses" => {
if arg.is_empty() {
return Err("'uses' requires a name, e.g. 'uses map'".into());
}
Ok(Query::Uses(arg.to_string()))
}
"def" => {
if arg.is_empty() {
return Err("'def' requires a name pattern, e.g. 'def update'".into());
}
Ok(Query::Defined(arg.to_string()))
}
"expr" => {
let kind = match arg.to_lowercase().as_str() {
"let" => ExprKindQuery::Let,
"case" => ExprKindQuery::Case,
"if" => ExprKindQuery::If,
"lambda" => ExprKindQuery::Lambda,
"record" => ExprKindQuery::Record,
"list" => ExprKindQuery::List,
"tuple" => ExprKindQuery::Tuple,
_ => {
return Err(format!(
"Unknown expr kind: {arg}. Try: let, case, if, lambda, record, list, tuple"
));
}
};
Ok(Query::ExprKind(kind))
}
_ => Err(format!(
"Unknown query: '{cmd}'. Available: returns, type, case-on, update, calls, unused-args, lambda, uses, def, expr"
)),
}
}