Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 15 additions & 5 deletions SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,21 @@ MoonBit.

## Quick Start

The simplest way to use `moongrep` is to run the `scan` command from the root
of a MoonBit project. By default, it scans recursively and skips directories
generated by Git and the MoonBit toolchain. Specify an *expression pattern* to
match with the `--pattern` option. For example, the following command matches a
typical expression that uses `match` on an `Option` value:
To check the current MoonBit project with the embedded builtin rules, run:

```bash
moongrep lint
```

`lint` is equivalent to `scan --enable-builtin-rules`. It defaults to the
current directory and accepts the other `scan` options for adding custom rules,
filtering findings, or changing the output format.

For structural search, run the `scan` command from the root of a MoonBit
project. By default, it scans recursively and skips directories generated by
Git and the MoonBit toolchain. Specify an *expression pattern* to match with
the `--pattern` option. For example, the following command matches a typical
expression that uses `match` on an `Option` value:

```bash
moongrep scan --pattern 'match $(value:exp) { Some($(some:id)) => $(some_body:exp); None => $(none_body:exp) }'
Expand Down
14 changes: 13 additions & 1 deletion SKILL_CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,19 @@ description: 使用 moongrep 对 MoonBit 源码进行结构化搜索和污点分

## 快速入门

`moongrep`的最简单使用方法是在一个MoonBit项目根目录运行scan命令(默认递归扫描,同时绕过Git和MoonBit工具链生成的目录), 并使用`--pattern`选项指定一个*表达式模式*进行匹配。例如,下面这条命令匹配典型的对Option类型的值进行`match`的表达式。
要使用内置规则检查当前 MoonBit 项目,运行:

```bash
moongrep lint
```

`lint` 等价于 `scan --enable-builtin-rules`。它默认扫描当前目录,并接受
`scan` 的其他选项,用于添加自定义规则、过滤结果或调整输出格式。

要进行结构化搜索,可以在 MoonBit 项目根目录运行 `scan` 命令。默认情况下,
它会递归扫描,并跳过 Git 和 MoonBit 工具链生成的目录。使用 `--pattern`
选项指定一个*表达式模式*进行匹配。例如,下面这条命令匹配典型的对 Option
类型的值进行 `match` 的表达式。

```bash
moongrep scan --pattern 'match $(value:exp) { Some($(some:id)) => $(some_body:exp); None => $(none_body:exp) }'
Expand Down
176 changes: 121 additions & 55 deletions cli/cli_args.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -29,57 +29,91 @@ pub fn runtime_cli_args(argv : Array[String]) -> Array[String] {
}

///|
let scan_command : @argparse.Command = Command(
"scan",
about="Scan MoonBit source files.",
arg_required_else_help=true,
flags=[
let default_rules_root : String = "./.moongrep/rules"

///|
fn scan_like_command(
name : StringView,
about : StringView,
include_enable_builtin_rules : Bool,
default_rules_root_enabled : Bool,
arg_required_else_help : Bool,
) -> @argparse.Command {
let flags : Array[@argparse.FlagArg] = [
FlagArg(
"verbose",
long="verbose",
about="Write loaded rule ids and traversal progress to stderr.",
),
FlagArg(
"enable-builtin-rules",
long="enable-builtin-rules",
about="Enable embedded builtin rules.",
),
]
if include_enable_builtin_rules {
flags.push(
FlagArg(
"enable-builtin-rules",
long="enable-builtin-rules",
about="Enable embedded builtin rules.",
),
)
}
flags.push(
FlagArg(
"output-json",
long="output-json",
about="Write each match as one JSON record to stdout.",
),
],
options=[
OptionArg(
"rules",
short='r',
action=Append,
about="Directory containing YAML rules.",
),
OptionArg("rule", action=Append, about="Single YAML rule file."),
OptionArg(
"pattern",
action=Append,
about="Anonymous structural pattern to match.",
),
OptionArg(
"guard",
action=Append,
about="YAML guard map for the preceding anonymous pattern.",
),
OptionArg(
"exclude-dir",
action=Append,
about="Directory name or path to skip while recursively scanning. May be repeated.",
),
OptionArg(
"exclude-rule",
action=Append,
about="Rule id to disable after loading rules. May be repeated.",
),
],
positionals=[PositionArg("scan-root", about="Directory to scan.")],
)
Command(
name,
about~,
arg_required_else_help~,
flags~,
options=[
OptionArg(
"rules",
short='r',
action=Append,
default_values=if default_rules_root_enabled {
[default_rules_root]
} else {
[]
},
about="Directory containing YAML rules.",
),
OptionArg("rule", action=Append, about="Single YAML rule file."),
OptionArg(
"pattern",
action=Append,
about="Anonymous structural pattern to match.",
),
OptionArg(
"guard",
action=Append,
about="YAML guard map for the preceding anonymous pattern.",
),
OptionArg(
"exclude-dir",
action=Append,
about="Directory name or path to skip while recursively scanning. May be repeated.",
),
OptionArg(
"exclude-rule",
action=Append,
about="Rule id to disable after loading rules. May be repeated.",
),
],
positionals=[PositionArg("scan-root", about="Directory to scan.")],
)
}

///|
let scan_command : @argparse.Command = scan_like_command(
"scan", "Scan MoonBit source files.", true, true, false,
)

///|
let lint_command : @argparse.Command = scan_like_command(
"lint", "Scan MoonBit source files with embedded builtin rules.", false, false,
false,
)

///|
Expand Down Expand Up @@ -118,7 +152,7 @@ let dump_command : @argparse.Command = Command(
let moongrep_command : @argparse.Command = Command(
"moongrep",
about="Scan MoonBit source files with structural and taint rules.",
subcommands=[scan_command, docs_command, dump_command],
subcommands=[scan_command, lint_command, docs_command, dump_command],
subcommand_required=true,
)

Expand Down Expand Up @@ -231,17 +265,22 @@ fn scan_cli_options(
scan_matches : @argparse.Matches,
env : Map[String, String],
argv : Array[String],
enable_builtin_rules_by_default~ : Bool,
) -> CliOptions raise {
let verbose = cli_flag_enabled(scan_matches.flags, "verbose")
let enable_builtin_rules = cli_flag_enabled(
scan_matches.flags,
"enable-builtin-rules",
)
let enable_builtin_rules = enable_builtin_rules_by_default ||
cli_flag_enabled(scan_matches.flags, "enable-builtin-rules")
let output_json = cli_flag_enabled(scan_matches.flags, "output-json")
let no_color = no_color_enabled(env)
let rules_root = last_cli_value(scan_matches.values, "rules")
let parsed_rules_root = last_cli_value(scan_matches.values, "rules")
let rule_file = last_cli_value(scan_matches.values, "rule")
let patterns = parse_scan_pattern_specs(argv)
let rules_root = if scan_matches.sources.get("rules") is Some(Default) &&
(rule_file is Some(_) || !patterns.is_empty() || enable_builtin_rules) {
None
} else {
parsed_rules_root
}
let exclude_dirs = normalize_scan_exclude_dirs(
all_cli_values(scan_matches.values, "exclude-dir"),
)
Expand Down Expand Up @@ -396,10 +435,13 @@ fn parse_cli_guard_map(source : String) -> Map[String, String] raise {
/// Parses command-line arguments after the executable name has been removed.
///
/// A supported subcommand is required after the executable name. Within `scan`,
/// `--rules <rules-root>`, `--rules=<rules-root>`, `-r <rules-root>`,
/// `--rule <rule-file>`, at least one `--pattern <pattern>`, or
/// `--enable-builtin-rules` is required. `--verbose` and `--output-json` are
/// optional.
/// `--rules <rules-root>`, `--rules=<rules-root>`, and `-r <rules-root>` select
/// a rules directory. When no rule source is given, the rules directory
/// defaults to `./.moongrep/rules`. An explicit `--rule <rule-file>`, at least
/// one `--pattern <pattern>`, or `--enable-builtin-rules` suppresses that
/// default. `--verbose` and `--output-json` are optional.
/// `lint` accepts the other scan options, enables builtin rules automatically,
/// and defaults to scanning the current directory when no arguments are given.
/// Each `--exclude-dir <dir>` skips one matching child directory name or path
/// during recursive source scanning. Each `--exclude-rule <rule-id>` disables
/// one loaded rule by exact rule id before matching. Both options may be
Expand All @@ -408,9 +450,9 @@ fn parse_cli_guard_map(source : String) -> Map[String, String] raise {
/// rules option or single rule option appears multiple times, the last value
/// wins. Repeated patterns, exclude directories, and excluded rules are
/// appended in order.
/// Invoking `scan`, `docs`, or `dump` without any arguments prints that
/// subcommand's help and exits successfully. Missing subcommands, other missing
/// required arguments, unknown options, or more than one scan root raise
/// Invoking `docs` or `dump` without any arguments prints that subcommand's
/// help and exits successfully. Missing subcommands, other missing required
/// arguments, unknown options, or more than one scan root raise
/// `CliError::Usage` with exit code 2.
pub fn parse_cli_command(
argv : Array[String],
Expand All @@ -424,7 +466,31 @@ pub fn parse_cli_command(
}
match matches.subcommand {
Some(("scan", scan_matches)) =>
("scan", Some(scan_cli_options(scan_matches, env, argv)), None)
(
"scan",
Some(
scan_cli_options(
scan_matches,
env,
argv,
enable_builtin_rules_by_default=false,
),
),
None,
)
Some(("lint", lint_matches)) =>
(
"scan",
Some(
scan_cli_options(
lint_matches,
env,
argv,
enable_builtin_rules_by_default=true,
),
),
None,
)
Some(("docs", docs_matches)) => docs_cli_command(docs_matches)
Some(("dump", dump_matches)) => dump_cli_command(dump_matches)
_ => raise CliError::Usage(message="missing required command", exit_code=2)
Expand Down
81 changes: 79 additions & 2 deletions cli/cli_wbtest.mbt
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,26 @@ fn expect_pattern_guard(
}

///|
test "cli args require rules or pattern option" {
test "cli args require command" {
expect_cli_usage([], 2)
expect_cli_usage(["src"], 2)
expect_cli_usage(["scan", "src"], 2)
expect_cli_usage(["--rules", "custom-rules"], 2)
}

///|
test "cli args use default rules root" {
match parse_cli_command(["scan"]) {
("scan", Some(options), None) => {
expect_rules_root(options, "./.moongrep/rules")
assert_true(options.rule_file is None)
expect_no_patterns(options)
inspect(options.scan_root, content=".")
assert_false(options.enable_builtin_rules)
}
_ => fail("unexpected command")
}
}

///|
test "cli args parse docs list command" {
match parse_cli_command(["docs", "--list"]) {
Expand Down Expand Up @@ -432,12 +445,76 @@ test "cli args parse builtin rules flag without rules root" {
}
}

///|
test "cli args parse lint with builtin rules and default scan root" {
match parse_cli_command(["lint"]) {
("scan", Some(options), None) => {
assert_true(options.rules_root is None)
assert_true(options.rule_file is None)
expect_no_patterns(options)
inspect(options.scan_root, content=".")
assert_true(options.exclude_dirs.is_empty())
assert_true(options.exclude_rules.is_empty())
assert_false(options.verbose)
assert_true(options.enable_builtin_rules)
assert_false(options.no_color)
assert_false(options.output_json)
}
_ => fail("unexpected command")
}
}

///|
test "cli args parse lint scan options in addition to builtin rules" {
match
parse_cli_command(
[
"lint", "--rules", "custom-rules", "--rule", "custom-rules/example.yaml",
"--pattern", "$(callee:id)()", "--guard", "{$callee: \"^safe_\"}", "--exclude-dir",
"./vendor/", "--exclude-rule", "moonbitlang/match_option", "--verbose", "--output-json",
"src",
],
env={ "NO_COLOR": "1" },
) {
("scan", Some(options), None) => {
expect_rules_root(options, "custom-rules")
expect_rule_file(options, "custom-rules/example.yaml")
assert_eq(options.patterns.length(), 1)
expect_pattern_shape(options, 0, "$(callee:id)()")
expect_pattern_guard(options, 0, "$callee", "^safe_")
inspect(options.scan_root, content="src")
debug_inspect(options.exclude_dirs, content="[\"vendor\"]")
debug_inspect(
options.exclude_rules,
content="[\"moonbitlang/match_option\"]",
)
assert_true(options.verbose)
assert_true(options.enable_builtin_rules)
assert_true(options.no_color)
assert_true(options.output_json)
}
_ => fail("unexpected command")
}
}

///|
test "cli args reject redundant lint builtin rules flag" {
expect_cli_usage(["lint", "--enable-builtin-rules"], 2)
}

///|
test "cli args render argparse help" {
let help = moongrep_command.render_help()
assert_true(help.has_prefix("Usage: moongrep <command>"))
assert_true(help.contains("scan"))
assert_true(help.contains("lint"))
assert_true(help.contains("docs"))
let lint_help = lint_command.render_help()
assert_true(lint_help.contains("embedded builtin rules"))
assert_false(lint_help.contains("--enable-builtin-rules"))
assert_false(lint_help.contains("[default: ./.moongrep/rules]"))
let scan_help = scan_command.render_help()
assert_true(scan_help.contains("[default: ./.moongrep/rules]"))
}

///|
Expand Down
Loading
Loading