Skip to content

Commit d41c950

Browse files
committed
add --guard
1 parent c12be79 commit d41c950

23 files changed

Lines changed: 392 additions & 98 deletions

SKILL.md

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,13 @@ Run the scanner with a rule directory and an optional scan target directory or
1111
```bash
1212
moon runwasm moonbit-community/moongrep -- scan --rules path/to/rules path/to/src
1313
moon runwasm moonbit-community/moongrep -- scan --pattern 'target()' path/to/src
14+
moon runwasm moonbit-community/moongrep -- scan --pattern '$(callee:id)()' --guard '{$callee: "^safe_"}' path/to/src
1415
```
1516

1617
Synopsis:
1718

1819
```text
19-
moon runwasm moonbit-community/moongrep -- scan [--verbose] [--enable-builtin-rules] [--exclude-dir <dir>...] ((--rules <rules-root> | --rules=<rules-root> | -r <rules-root> | --rule <rule-file>) | --pattern <pattern>)... [scan-root]
20+
moon runwasm moonbit-community/moongrep -- scan [--verbose] [--enable-builtin-rules] [--exclude-dir <dir>...] ((--rules <rules-root> | --rules=<rules-root> | -r <rules-root> | --rule <rule-file>) | --pattern <pattern> [--guard <guard>])... [scan-root]
2021
```
2122

2223
The scanner is available through the `scan` subcommand. `--rules` / `-r` is
@@ -29,7 +30,9 @@ itself. `--enable-builtin-rules` loads the embedded builtin rules in addition
2930
to any rules and inline patterns supplied on the command line. One optional
3031
positional `scan-root` may appear in the `scan` argument list and defaults to
3132
`.`. If the rules or rule option appears multiple times, the last value wins.
32-
Repeated `--pattern` values are appended as separate anonymous rules.
33+
Repeated `--pattern` values are appended as separate anonymous rules. Use
34+
`--guard <guard>` after an anonymous `--pattern` to attach a YAML guard map with
35+
`$`-prefixed metavariable keys, using the same schema as rule-file `guard`.
3336

3437
Use `--exclude-dir <dir>...` to skip directory names or paths while recursively
3538
scanning the source tree. When passing multiple excluded directories after one
@@ -38,8 +41,9 @@ flag, put `scan-root` before `--exclude-dir`; repeated `--exclude-dir <dir>` and
3841

3942
Usage errors print a message and exit with code 2: missing `scan` command,
4043
missing all rule sources (`--rules`, `--rule`, `--pattern`, and
41-
`--enable-builtin-rules`), missing option value, unknown options, or more than
42-
one scan root. Non-usage errors, including unreadable paths, an empty rules
44+
`--enable-builtin-rules`), missing option value, misplaced or malformed
45+
`--guard`, unknown options, or more than one scan root. Non-usage errors,
46+
including unreadable paths, an empty rules
4347
directory, invalid YAML/schema/shape, or source read failures, abort the run;
4448
the CLI prints the error and exits with code 1.
4549

@@ -59,6 +63,7 @@ moon runwasm moonbit-community/moongrep -- scan --rules path/to/rules
5963
moon runwasm moonbit-community/moongrep -- scan -r path/to/rules
6064
moon runwasm moonbit-community/moongrep -- scan --rule path/to/rule.yaml
6165
moon runwasm moonbit-community/moongrep -- scan --pattern 'target()'
66+
moon runwasm moonbit-community/moongrep -- scan --pattern '$(callee:id)()' --guard '{$callee: "^safe_"}'
6267
moon runwasm moonbit-community/moongrep -- scan --enable-builtin-rules
6368
```
6469

cli_args.mbt

Lines changed: 118 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ priv suberror CliError {
99
priv struct CliOptions {
1010
rules_root : String?
1111
rule_file : String?
12-
patterns : Array[String]
12+
patterns : Array[@rule_model.RulePatternSpec]
1313
scan_root : String
1414
exclude_dirs : Array[String]
1515
verbose : Bool
@@ -59,6 +59,11 @@ let scan_command : @argparse.Command = @argparse.Command(
5959
action=@argparse.OptionAction::Append,
6060
about="Anonymous structural pattern to match.",
6161
),
62+
@argparse.OptionArg(
63+
"guard",
64+
action=@argparse.OptionAction::Append,
65+
about="YAML guard map for the preceding anonymous pattern.",
66+
),
6267
@argparse.OptionArg(
6368
"exclude-dir",
6469
action=@argparse.OptionAction::Append,
@@ -218,6 +223,7 @@ fn dump_cli_command(
218223
fn scan_cli_options(
219224
scan_matches : @argparse.Matches,
220225
env : Map[String, String],
226+
argv : Array[String],
221227
) -> CliOptions raise {
222228
let verbose = cli_flag_enabled(scan_matches.flags, "verbose")
223229
let enable_builtin_rules = cli_flag_enabled(
@@ -227,7 +233,7 @@ fn scan_cli_options(
227233
let no_color = no_color_enabled(env)
228234
let rules_root = last_cli_value(scan_matches.values, "rules")
229235
let rule_file = last_cli_value(scan_matches.values, "rule")
230-
let patterns = all_cli_values(scan_matches.values, "pattern")
236+
let patterns = parse_scan_pattern_specs(argv)
231237
let exclude_dirs = normalize_scan_exclude_dirs(
232238
all_cli_values(scan_matches.values, "exclude-dir"),
233239
)
@@ -258,6 +264,115 @@ fn scan_cli_options(
258264
}
259265
}
260266

267+
///|
268+
fn parse_scan_pattern_specs(
269+
argv : Array[String],
270+
) -> Array[@rule_model.RulePatternSpec] raise {
271+
let patterns : Array[@rule_model.RulePatternSpec] = []
272+
let has_guard : Array[Bool] = []
273+
let mut index = 1
274+
while index < argv.length() {
275+
let arg = argv[index]
276+
if arg == "--pattern" {
277+
if index + 1 >= argv.length() {
278+
raise CliError::Usage(
279+
message="missing value for --pattern",
280+
exit_code=2,
281+
)
282+
}
283+
patterns.push({ shape: argv[index + 1], guards: {} })
284+
has_guard.push(false)
285+
index += 2
286+
} else if arg.has_prefix("--pattern=") {
287+
patterns.push({
288+
shape: arg["--pattern=".length():].to_owned(),
289+
guards: {},
290+
})
291+
has_guard.push(false)
292+
index += 1
293+
} else if arg == "--guard" {
294+
if index + 1 >= argv.length() {
295+
raise CliError::Usage(message="missing value for --guard", exit_code=2)
296+
}
297+
attach_scan_guard(
298+
patterns,
299+
has_guard,
300+
parse_cli_guard_map(argv[index + 1]),
301+
)
302+
index += 2
303+
} else if arg.has_prefix("--guard=") {
304+
attach_scan_guard(
305+
patterns,
306+
has_guard,
307+
parse_cli_guard_map(arg["--guard=".length():].to_owned()),
308+
)
309+
index += 1
310+
} else {
311+
index += 1
312+
}
313+
}
314+
patterns
315+
}
316+
317+
///|
318+
fn attach_scan_guard(
319+
patterns : Array[@rule_model.RulePatternSpec],
320+
has_guard : Array[Bool],
321+
guards : Map[String, String],
322+
) -> Unit raise {
323+
let mut index = patterns.length()
324+
while index > 0 {
325+
index -= 1
326+
if !has_guard[index] {
327+
let shape = patterns[index].shape
328+
patterns[index] = { shape, guards }
329+
has_guard[index] = true
330+
return
331+
}
332+
}
333+
let message = if patterns.is_empty() {
334+
"--guard requires a preceding --pattern"
335+
} else {
336+
"duplicate --guard for anonymous --pattern"
337+
}
338+
raise CliError::Usage(message~, exit_code=2)
339+
}
340+
341+
///|
342+
fn parse_cli_guard_map(source : String) -> Map[String, String] raise {
343+
let docs = @yaml.Yaml::load_from_string(source) catch {
344+
err =>
345+
raise CliError::Usage(message="invalid --guard YAML: \{err}", exit_code=2)
346+
}
347+
if docs.length() != 1 {
348+
raise CliError::Usage(
349+
message="--guard must contain exactly one YAML document",
350+
exit_code=2,
351+
)
352+
}
353+
guard docs[0] is Map(map) else {
354+
raise CliError::Usage(message="--guard must be a YAML mapping", exit_code=2)
355+
}
356+
let guards : Map[String, String] = {}
357+
for key, value in map {
358+
if !key.has_prefix("$") || key.length() <= 1 {
359+
raise CliError::Usage(
360+
message="guard key \{key} must be a $-prefixed metavar name",
361+
exit_code=2,
362+
)
363+
}
364+
match value {
365+
String(regex_source) => guards[key] = regex_source
366+
_ =>
367+
raise CliError::Usage(
368+
message="--guard.\{key} must be a string",
369+
exit_code=2,
370+
)
371+
}
372+
}
373+
guards
374+
}
375+
261376
///|
262377
fn normalize_scan_exclude_dir_args(argv : Array[String]) -> Array[String] raise {
263378
if argv.length() == 0 || argv[0] != "scan" {
@@ -318,7 +433,7 @@ fn parse_cli_command(
318433
}
319434
match matches.subcommand {
320435
Some(("scan", scan_matches)) =>
321-
("scan", Some(scan_cli_options(scan_matches, env)), None)
436+
("scan", Some(scan_cli_options(scan_matches, env, normalized_argv)), None)
322437
Some(("docs", docs_matches)) => docs_cli_command(docs_matches)
323438
Some(("dump", dump_matches)) => dump_cli_command(dump_matches)
324439
_ => raise CliError::Usage(message="missing required command", exit_code=2)

0 commit comments

Comments
 (0)