From abf47af260bd1684f2aa2a650397e02432d9803a Mon Sep 17 00:00:00 2001 From: myfreess Date: Mon, 10 Aug 2026 13:52:38 +0800 Subject: [PATCH 1/5] add subcommand lint --- SKILL.md | 20 +++++-- SKILL_CN.md | 14 ++++- cli/cli_args.mbt | 143 ++++++++++++++++++++++++++++++--------------- cli/cli_wbtest.mbt | 61 +++++++++++++++++++ e2etests/BASIC.md | 39 ++++++++++++- 5 files changed, 222 insertions(+), 55 deletions(-) diff --git a/SKILL.md b/SKILL.md index 30ef1d5..9c00692 100644 --- a/SKILL.md +++ b/SKILL.md @@ -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) }' diff --git a/SKILL_CN.md b/SKILL_CN.md index b1567c4..93978c4 100644 --- a/SKILL_CN.md +++ b/SKILL_CN.md @@ -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) }' diff --git a/cli/cli_args.mbt b/cli/cli_args.mbt index 3cdbb19..8b15441 100644 --- a/cli/cli_args.mbt +++ b/cli/cli_args.mbt @@ -29,57 +29,81 @@ 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=[ +fn scan_like_command( + name : StringView, + about : StringView, + include_enable_builtin_rules : 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, + 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, +) + +///| +let lint_command : @argparse.Command = scan_like_command( + "lint", "Scan MoonBit source files with embedded builtin rules.", false, false, ) ///| @@ -118,7 +142,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, ) @@ -231,12 +255,11 @@ 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") @@ -400,6 +423,8 @@ fn parse_cli_guard_map(source : String) -> Map[String, String] raise { /// `--rule `, at least one `--pattern `, or /// `--enable-builtin-rules` is required. `--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 ` skips one matching child directory name or path /// during recursive source scanning. Each `--exclude-rule ` disables /// one loaded rule by exact rule id before matching. Both options may be @@ -424,7 +449,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) diff --git a/cli/cli_wbtest.mbt b/cli/cli_wbtest.mbt index 297a611..57a2259 100644 --- a/cli/cli_wbtest.mbt +++ b/cli/cli_wbtest.mbt @@ -432,12 +432,73 @@ 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 ")) 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")) } ///| diff --git a/e2etests/BASIC.md b/e2etests/BASIC.md index 49a7193..14892bb 100644 --- a/e2etests/BASIC.md +++ b/e2etests/BASIC.md @@ -11,6 +11,7 @@ Scan MoonBit source files with structural and taint rules. Commands: scan Scan MoonBit source files. + lint Scan MoonBit source files with embedded builtin rules. docs Print embedded moongrep documentation. dump Parse a MoonBit impl or expression and print untyped_ast debug output. help Print help for the subcommand(s). @@ -21,8 +22,9 @@ Options: ## moongrep subcommands without arguments -Each check verifies that invoking a subcommand without arguments succeeds and -prints exactly the same help text as invoking it with `--help`. +The `scan`, `docs`, and `dump` subcommands print their help when invoked +without arguments. `lint` instead treats an omitted scan root as the current +directory because builtin rules are enabled automatically. The scan subcommand falls back to its help when no scan root or rule input is provided. @@ -44,6 +46,39 @@ input. $ moonrun "$TESTDIR"/moongrep.wasm -- dump > /dev/null && diff -u <(moonrun "$TESTDIR"/moongrep.wasm -- dump --help) <(moonrun "$TESTDIR"/moongrep.wasm -- dump) ``` +Running bare `lint` from a fixture directory produces the same output as +enabling builtin rules explicitly through `scan`. + +```mooncram +$ cd "$TESTDIR"/../testdata/builtin-rules && diff -u <(moonrun "$TESTDIR"/moongrep.wasm -- scan --enable-builtin-rules) <(moonrun "$TESTDIR"/moongrep.wasm -- lint) +``` + +## moongrep lint --help + +The lint help exposes all scan options except the redundant +`--enable-builtin-rules` flag. + +```mooncram +$ moonrun "$TESTDIR"/moongrep.wasm -- lint --help +Usage: moongrep lint [options] [scan-root] + +Scan MoonBit source files with embedded builtin rules. + +Arguments: + scan-root Directory to scan. + +Options: + -h, --help Show help information. + --verbose Write loaded rule ids and traversal progress to stderr. + --output-json Write each match as one JSON record to stdout. + -r, --rules Directory containing YAML rules. + --rule Single YAML rule file. + --pattern Anonymous structural pattern to match. + --guard YAML guard map for the preceding anonymous pattern. + --exclude-dir Directory name or path to skip while recursively scanning. May be repeated. + --exclude-rule Rule id to disable after loading rules. May be repeated. +``` + ## moongrep scan --help The scan help documents the scan root together with every rule source, From 2c5233227c2863ea606a2a2520f6bdee2803da12 Mon Sep 17 00:00:00 2001 From: myfreess Date: Mon, 10 Aug 2026 15:17:16 +0800 Subject: [PATCH 2/5] update parser --- moon.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moon.mod b/moon.mod index c594937..849415f 100644 --- a/moon.mod +++ b/moon.mod @@ -5,7 +5,7 @@ version = "0.1.17" preferred_target = "wasm" import { - "moonbitlang/parser@0.3.10", + "moonbitlang/parser@0.3.13", "moonbit-community/yaml@0.0.6", "moonbit-community/chalk@0.0.1", "moonbitlang/lexer@0.3.10", From 01717fd87df75e8b9f91973b6efc083e9737e480 Mon Sep 17 00:00:00 2001 From: myfreess Date: Mon, 10 Aug 2026 15:55:18 +0800 Subject: [PATCH 3/5] fix guard matching with omitted body --- docs/RuleSpec.md | 55 +++++++++++++++++++++++++ docs/RuleSpec_CN.md | 52 +++++++++++++++++++++++ docs/WritingRules.md | 44 ++++++++++++++++++++ docs/WritingRules_CN.md | 44 ++++++++++++++++++++ docs/rule_spec.mbt | 55 +++++++++++++++++++++++++ docs/writing_rules.mbt | 44 ++++++++++++++++++++ e2etests/SCAN.md | 17 ++++++++ matching/INTERNAL.md | 18 ++++++-- matching/INTERNAL_CN.md | 16 ++++++-- matching/matching.mbt | 29 +++++++++++++ matching/matching_test.mbt | 64 +++++++++++++++++++++++++++++ rule/apply/apply_test.mbt | 23 +++++++++++ testdata/guard-omitted-body/hit.mbt | 4 ++ 13 files changed, 459 insertions(+), 6 deletions(-) create mode 100644 testdata/guard-omitted-body/hit.mbt diff --git a/docs/RuleSpec.md b/docs/RuleSpec.md index a3ed9da..4cc8cff 100644 --- a/docs/RuleSpec.md +++ b/docs/RuleSpec.md @@ -205,6 +205,61 @@ matching is not currently expressible as a structural shape. This shortcut applies only to ordinary `let` expressions. `let mut`, local function definitions, and `letrec` shapes use normal structural matching. +### Guard Shapes With Omitted Bodies + +A `guard` shape without an explicit body is a guard-header pattern. It matches +the condition and `else` expression but places no constraint on the candidate +continuation: + +```yaml +patterns: + - shape: guard ready() else { fallback() } +``` + +It can match candidates such as: + +```moonbit +guard ready() else { fallback() } +``` + +```moonbit +guard ready() else { fallback() }; continue_work() +``` + +```moonbit +guard ready() else { fallback() }; { prepare(); finish() } +``` + +The MoonBit parser represents the omitted body as a synthesized unit +expression. When that synthesized unit appears in the pattern shape, the +matcher ignores the candidate body. The condition and `else` expression still +use normal recursive structural matching. + +Write an explicit body when the continuation matters: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; continue_work() +``` + +To capture whichever body the candidate has, write a body metavar explicitly: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; $(body:exp) +``` + +To require a unit body, write an explicit `()` body: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; () +``` + +The explicit unit is matched structurally and is not a wildcard. As with an +omitted `let` body, matching only syntactically omitted guard bodies is not +currently expressible as a structural shape. + ## Metavariables Identifiers and labels in a shape are literal by default. Metavar syntax inside diff --git a/docs/RuleSpec_CN.md b/docs/RuleSpec_CN.md index 0c5a7a8..dea12c9 100644 --- a/docs/RuleSpec_CN.md +++ b/docs/RuleSpec_CN.md @@ -168,6 +168,58 @@ patterns: 这个快捷匹配只适用于普通 `let` 表达式。`let mut`、局部函数定义和 `letrec` shape 使用普通结构匹配。 +### 省略 body 的 guard shape + +没有显式 body 的 `guard` shape 是 guard-header pattern。它会匹配 condition +和 `else` 表达式,但不约束候选表达式的 continuation: + +```yaml +patterns: + - shape: guard ready() else { fallback() } +``` + +它可以匹配下面这些候选形式: + +```moonbit +guard ready() else { fallback() } +``` + +```moonbit +guard ready() else { fallback() }; continue_work() +``` + +```moonbit +guard ready() else { fallback() }; { prepare(); finish() } +``` + +MoonBit parser 会把省略的 body 表示为一个合成的 unit 表达式。当 pattern +shape 中出现这个合成 unit 时,matcher 会忽略候选 body。condition 和 +`else` 表达式仍使用普通的递归结构匹配。 + +如果 continuation 重要,请显式写出 body: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; continue_work() +``` + +如需捕获任意形式的候选 body,请显式写 body 元变量: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; $(body:exp) +``` + +如果期望 body 是 unit,请显式写 `()` body: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; () +``` + +显式 unit 会按结构匹配,不是通配符。与省略 body 的 `let` 一样,当前结构 +shape 无法表达“只匹配语法上省略 body 的 guard”。 + ## 元变量 shape 中的标识符和标签默认都是字面量。只有在 `shape` 内使用内联元变量语法时,一个名称才会成为元变量;下面描述的内置通配符除外。 diff --git a/docs/WritingRules.md b/docs/WritingRules.md index 075a75a..560536d 100644 --- a/docs/WritingRules.md +++ b/docs/WritingRules.md @@ -236,6 +236,50 @@ Do not use `let $(name:id) = $(value:exp)` when you mean "the body is empty" or the candidate has. Matching only syntactically omitted let bodies is not currently expressible as a structural shape. +### 1.2. Decide whether a `guard` body matters + +A `guard` shape with no explicit body matches the condition and `else` +expression while ignoring the candidate continuation: + +```yaml +patterns: + - shape: guard ready() else { fallback() } +``` + +That shape matches both an isolated guard and a guard followed by any body: + +```moonbit +guard ready() else { fallback() }; continue_work() +``` + +```moonbit +guard ready() else { fallback() }; { prepare(); finish() } +``` + +If the continuation matters, write it explicitly: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; continue_work() +``` + +To capture an unrestricted body, add an explicit body metavar: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; $(body:exp) +``` + +An explicit unit body also stays exact: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; () +``` + +The final form is not a wildcard. Only the parser-synthesized unit from an +omitted pattern body makes the candidate body unrestricted. + ### 2. Mark metavars inline in `shape` Names that look like placeholders are literal by default in `shape`. diff --git a/docs/WritingRules_CN.md b/docs/WritingRules_CN.md index fe1c8e4..12b99a1 100644 --- a/docs/WritingRules_CN.md +++ b/docs/WritingRules_CN.md @@ -163,6 +163,50 @@ patterns: `let $(name:id) = $(value:exp)`;省略 body 的形式会有意忽略候选表达式的任意 body。当前结构 shape 无法表达“只匹配语法上省略 body 的 let”。 +### 1.2. 判断 `guard` body 是否重要 + +没有显式 body 的 `guard` shape 会匹配 condition 和 `else` 表达式,同时忽略 +候选表达式的 continuation: + +```yaml +patterns: + - shape: guard ready() else { fallback() } +``` + +这个 shape 既能匹配单独的 guard,也能匹配后面带任意 body 的 guard: + +```moonbit +guard ready() else { fallback() }; continue_work() +``` + +```moonbit +guard ready() else { fallback() }; { prepare(); finish() } +``` + +如果 continuation 重要,请显式写出: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; continue_work() +``` + +如需捕获任意形式的 body,请显式添加 body 元变量: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; $(body:exp) +``` + +显式 unit body 也仍然精确匹配: + +```yaml +patterns: + - shape: guard ready() else { fallback() }; () +``` + +最后一种形式不是通配符。只有 pattern 省略 body 时由 parser 合成的 unit +才会让候选 body 不受约束。 + ### 2. 在 `shape` 中内联标记元变量 `shape` 中看起来像占位符的名称默认也是字面量。 diff --git a/docs/rule_spec.mbt b/docs/rule_spec.mbt index c8b7536..3042baa 100644 --- a/docs/rule_spec.mbt +++ b/docs/rule_spec.mbt @@ -209,6 +209,61 @@ let _embed_rulespec_md : String = #|This shortcut applies only to ordinary `let` expressions. `let mut`, local #|function definitions, and `letrec` shapes use normal structural matching. #| + #|### Guard Shapes With Omitted Bodies + #| + #|A `guard` shape without an explicit body is a guard-header pattern. It matches + #|the condition and `else` expression but places no constraint on the candidate + #|continuation: + #| + #|```yaml + #|patterns: + #| - shape: guard ready() else { fallback() } + #|``` + #| + #|It can match candidates such as: + #| + #|```moonbit + #|guard ready() else { fallback() } + #|``` + #| + #|```moonbit + #|guard ready() else { fallback() }; continue_work() + #|``` + #| + #|```moonbit + #|guard ready() else { fallback() }; { prepare(); finish() } + #|``` + #| + #|The MoonBit parser represents the omitted body as a synthesized unit + #|expression. When that synthesized unit appears in the pattern shape, the + #|matcher ignores the candidate body. The condition and `else` expression still + #|use normal recursive structural matching. + #| + #|Write an explicit body when the continuation matters: + #| + #|```yaml + #|patterns: + #| - shape: guard ready() else { fallback() }; continue_work() + #|``` + #| + #|To capture whichever body the candidate has, write a body metavar explicitly: + #| + #|```yaml + #|patterns: + #| - shape: guard ready() else { fallback() }; $(body:exp) + #|``` + #| + #|To require a unit body, write an explicit `()` body: + #| + #|```yaml + #|patterns: + #| - shape: guard ready() else { fallback() }; () + #|``` + #| + #|The explicit unit is matched structurally and is not a wildcard. As with an + #|omitted `let` body, matching only syntactically omitted guard bodies is not + #|currently expressible as a structural shape. + #| #|## Metavariables #| #|Identifiers and labels in a shape are literal by default. Metavar syntax inside diff --git a/docs/writing_rules.mbt b/docs/writing_rules.mbt index cedc973..b5b040f 100644 --- a/docs/writing_rules.mbt +++ b/docs/writing_rules.mbt @@ -240,6 +240,50 @@ let _embed_writingrules_md : String = #|the candidate has. Matching only syntactically omitted let bodies is not #|currently expressible as a structural shape. #| + #|### 1.2. Decide whether a `guard` body matters + #| + #|A `guard` shape with no explicit body matches the condition and `else` + #|expression while ignoring the candidate continuation: + #| + #|```yaml + #|patterns: + #| - shape: guard ready() else { fallback() } + #|``` + #| + #|That shape matches both an isolated guard and a guard followed by any body: + #| + #|```moonbit + #|guard ready() else { fallback() }; continue_work() + #|``` + #| + #|```moonbit + #|guard ready() else { fallback() }; { prepare(); finish() } + #|``` + #| + #|If the continuation matters, write it explicitly: + #| + #|```yaml + #|patterns: + #| - shape: guard ready() else { fallback() }; continue_work() + #|``` + #| + #|To capture an unrestricted body, add an explicit body metavar: + #| + #|```yaml + #|patterns: + #| - shape: guard ready() else { fallback() }; $(body:exp) + #|``` + #| + #|An explicit unit body also stays exact: + #| + #|```yaml + #|patterns: + #| - shape: guard ready() else { fallback() }; () + #|``` + #| + #|The final form is not a wildcard. Only the parser-synthesized unit from an + #|omitted pattern body makes the candidate body unrestricted. + #| #|### 2. Mark metavars inline in `shape` #| #|Names that look like placeholders are literal by default in `shape`. diff --git a/e2etests/SCAN.md b/e2etests/SCAN.md index 3073a36..d60b4fa 100644 --- a/e2etests/SCAN.md +++ b/e2etests/SCAN.md @@ -176,6 +176,23 @@ source: 3 | } ``` +A guard pattern without an explicit body matches the guard header and ignores +the candidate continuation. The reported range still covers the complete +candidate guard expression, including the following call. + +```mooncram +$ cd "$TESTDIR"/.. && moonrun "$TESTDIR"/moongrep.wasm -- scan --pattern 'guard ready() else { fallback() }' testdata/guard-omitted-body +testdata/guard-omitted-body/hit.mbt:2:3-3:18 +rule: guard ready() else { fallback() } +description: + Anonymous CLI pattern. +source: +1 | fn sample { +2 > guard ready() else { fallback() } +3 > continue_work() +4 | } +``` + Qualified call matching is independent of whitespace before the long identifier dot. The source still matches the compact anonymous pattern. diff --git a/matching/INTERNAL.md b/matching/INTERNAL.md index d7ed0bd..1fb3415 100644 --- a/matching/INTERNAL.md +++ b/matching/INTERNAL.md @@ -79,7 +79,7 @@ argument, pattern, and type captures. Repeated binding comparison is handled by child labels and child values in order. Leaf values compare through their node kind. The `loc` field is ignored throughout the walk. -## Let Head Matching +## Let and Guard Header Matching MoonBit parses an expression such as `let ($_, $_) = $_` as an `Expr::Let` whose body is a parser-synthesized `Unit(faked=true)`. @@ -97,9 +97,21 @@ their old meaning: `LetMut`, `LetFn`, and `LetAnd` do not use the faked-unit shortcut. -This behavior belongs in the matcher. It lets `inside-expr` use nested let +Guard expressions use the same parser convention. A shape such as +`guard ready() else { fallback() }` has a parser-synthesized +`Unit(faked=true)` body. For `Expr::Guard`, the matcher still recursively +matches `cond` and `otherwise`, but places no requirement on the candidate body +when the pattern body is that faked unit. + +Explicit guard bodies use normal structural matching, including +`guard ready() else { fallback() }; ()`. An explicit unit has `faked=false` +and is not a wildcard. A body metavar such as `$(body:exp)` continues to bind +the candidate body normally. + +These behaviors belong in the matcher. They let `inside-expr` use nested let expressions such as `let println = $_; __TARGET__` and traverse the target body -normally. +normally. They do not change scoped traversal, YAML `guard` filters, or taint +matching. ## Exactness and Small Exceptions diff --git a/matching/INTERNAL_CN.md b/matching/INTERNAL_CN.md index 9fd33c8..52c30ca 100644 --- a/matching/INTERNAL_CN.md +++ b/matching/INTERNAL_CN.md @@ -65,7 +65,7 @@ matcher 在比较重复的 expression、argument、pattern 和 type 捕获时会 `node_equal_ignoring_loc` 要求节点 kind 相同,并按顺序递归比较子节点标签和子值。 Leaf 值通过节点 kind 比较;整个遍历都会忽略 `loc` 字段。 -## Let 头部匹配 +## Let 和 Guard 头部匹配 MoonBit 会把 `let ($_, $_) = $_` 这样的表达式解析为 `Expr::Let`, 其 body 是 parser 合成的 `Unit(faked=true)`。 @@ -81,8 +81,18 @@ matcher 会把这个 faked unit 视为“pattern 中省略了 body”。这条 `LetMut`、`LetFn` 和 `LetAnd` 不使用 faked-unit 快捷路径。 -这个行为属于 matcher。它允许 `inside-expr` 使用嵌套 let 表达式, -例如 `let println = $_; __TARGET__`,并正常遍历目标 body。 +guard 表达式使用相同的 parser 约定。`guard ready() else { fallback() }` +这样的 shape 具有 parser 合成的 `Unit(faked=true)` body。对于 +`Expr::Guard`,matcher 仍然递归匹配 `cond` 和 `otherwise`;只有当 pattern +body 是这个 faked unit 时,才不要求候选 body 匹配。 + +显式 guard body 仍使用普通结构匹配,包括 +`guard ready() else { fallback() }; ()`。显式 unit 的 `faked=false`,不会被 +当成通配符。`$(body:exp)` 这样的 body 元变量也仍会正常绑定候选 body。 + +这些行为属于 matcher。它们允许 `inside-expr` 使用嵌套 let 表达式, +例如 `let println = $_; __TARGET__`,并正常遍历目标 body。它们不会改变 +作用域遍历、YAML `guard` 过滤器或 taint 匹配。 ## 精确性和小例外 diff --git a/matching/matching.mbt b/matching/matching.mbt index 36d2725..82fcfe4 100644 --- a/matching/matching.mbt +++ b/matching/matching.mbt @@ -80,6 +80,7 @@ fn match_node( result } else { match pattern.kind { + Expr_Guard => match_guard_node(pattern, candidate, compiled, bindings) Expr_Let => match_let_node(pattern, candidate, compiled, bindings) InterpElem_Source => candidate.kind == InterpElem_Source Var => match_var(pattern, candidate, compiled, bindings) @@ -235,6 +236,34 @@ fn match_pattern_placeholder( } } +///| +fn match_guard_node( + pattern : @untyped_ast.Node, + candidate : @untyped_ast.Node, + compiled : CompiledExprPattern, + bindings : @hashmap.HashMap[String, BoundValue], +) -> Bool { + if candidate.kind != Expr_Guard { + return false + } + guard child(pattern, "cond") is Some(pattern_cond) else { return false } + guard child(candidate, "cond") is Some(candidate_cond) else { return false } + guard child(pattern, "otherwise") is Some(pattern_otherwise) else { + return false + } + guard child(candidate, "otherwise") is Some(candidate_otherwise) else { + return false + } + guard child(pattern, "body") is Some(pattern_body) else { return false } + guard child(candidate, "body") is Some(candidate_body) else { return false } + match_node(pattern_cond, candidate_cond, compiled, bindings) && + match_node(pattern_otherwise, candidate_otherwise, compiled, bindings) && + ( + is_faked_unit(pattern_body) || + match_node(pattern_body, candidate_body, compiled, bindings) + ) +} + ///| fn match_let_node( pattern : @untyped_ast.Node, diff --git a/matching/matching_test.mbt b/matching/matching_test.mbt index 258dd4d..4c6d37a 100644 --- a/matching/matching_test.mbt +++ b/matching/matching_test.mbt @@ -745,3 +745,67 @@ test "let pattern with omitted body matches let header only" { ), ) } + +///| +test "guard pattern with omitted body matches guard header only" { + let pattern = parse_expr("guard ready() else { fallback() }") + assert_true( + expr_matches( + pattern, + parse_expr("guard ready() else { fallback() }; continue_work()"), + ), + ) + assert_true( + expr_matches( + pattern, + parse_expr("guard ready() else { fallback() }; { prepare(); finish() }"), + ), + ) +} + +///| +test "guard pattern with omitted body still matches condition and otherwise" { + let pattern = parse_expr("guard ready() else { fallback() }") + assert_false( + expr_matches( + pattern, + parse_expr("guard waiting() else { fallback() }; continue_work()"), + ), + ) + assert_false( + expr_matches( + pattern, + parse_expr("guard ready() else { recover() }; continue_work()"), + ), + ) +} + +///| +test "guard pattern with explicit body matches body exactly" { + let pattern = parse_expr("guard ready() else { fallback() }; continue_work()") + assert_true( + expr_matches( + pattern, + parse_expr("guard ready() else { fallback() }; continue_work()"), + ), + ) + assert_false( + expr_matches( + pattern, + parse_expr("guard ready() else { fallback() }; finish_work()"), + ), + ) + let explicit_unit = parse_expr("guard ready() else { fallback() }; ()") + assert_true( + expr_matches( + explicit_unit, + parse_expr("guard ready() else { fallback() }; ()"), + ), + ) + assert_false( + expr_matches( + explicit_unit, + parse_expr("guard ready() else { fallback() }; continue_work()"), + ), + ) +} diff --git a/rule/apply/apply_test.mbt b/rule/apply/apply_test.mbt index 5e5c631..9a14fd1 100644 --- a/rule/apply/apply_test.mbt +++ b/rule/apply/apply_test.mbt @@ -1912,6 +1912,29 @@ test "apply structural rule matches let pattern header with omitted body" { assert_eq(hits.length(), 1) } +///| +test "apply structural rule matches guard header with omitted body" { + let rule_source = + #|id: example + #|description: | + #| Guard header. + #|patterns: + #| - shape: guard ready() else { fallback() } + #| + let source = + #|fn sample { + #| guard ready() else { fallback() } + #| { continue_work(); finish_work() } + #|} + #| + let hits = apply_structural_test_rules( + "sample.mbt", + parse_root_node(source), + compile_rule_source(rule_source), + ) + assert_eq(hits.length(), 1) +} + ///| test "apply taint rule reports source reaching sink" { let rule_source = diff --git a/testdata/guard-omitted-body/hit.mbt b/testdata/guard-omitted-body/hit.mbt new file mode 100644 index 0000000..5e5ea2e --- /dev/null +++ b/testdata/guard-omitted-body/hit.mbt @@ -0,0 +1,4 @@ +fn sample { + guard ready() else { fallback() } + continue_work() +} From 35d0796ffc4cd6e58e26a80ad324d16a70704c2f Mon Sep 17 00:00:00 2001 From: myfreess Date: Tue, 11 Aug 2026 16:19:47 +0800 Subject: [PATCH 4/5] add default rules directory --- cli/cli_args.mbt | 35 ++++++++++++++----- cli/cli_wbtest.mbt | 20 +++++++++-- docs/WritingRules.md | 23 ++++++------ docs/WritingRules_CN.md | 14 ++++---- docs/writing_rules.mbt | 23 ++++++------ e2etests/BASIC.md | 16 +++------ e2etests/SCAN.md | 13 +++++++ .../.moongrep/rules/example.yaml | 5 +++ testdata/default-rules/src/hit.mbt | 1 + 9 files changed, 102 insertions(+), 48 deletions(-) create mode 100644 testdata/default-rules/.moongrep/rules/example.yaml create mode 100644 testdata/default-rules/src/hit.mbt diff --git a/cli/cli_args.mbt b/cli/cli_args.mbt index 8b15441..00c40e1 100644 --- a/cli/cli_args.mbt +++ b/cli/cli_args.mbt @@ -28,11 +28,15 @@ pub fn runtime_cli_args(argv : Array[String]) -> Array[String] { args } +///| +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] = [ @@ -68,6 +72,11 @@ fn scan_like_command( "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."), @@ -98,12 +107,13 @@ fn scan_like_command( ///| let scan_command : @argparse.Command = scan_like_command( - "scan", "Scan MoonBit source files.", true, true, + "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, ) ///| @@ -262,9 +272,15 @@ fn scan_cli_options( 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"), ) @@ -419,10 +435,11 @@ 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=`, `-r `, -/// `--rule `, at least one `--pattern `, or -/// `--enable-builtin-rules` is required. `--verbose` and `--output-json` are -/// optional. +/// `--rules `, `--rules=`, and `-r ` select +/// a rules directory. When no rule source is given, the rules directory +/// defaults to `./.moongrep/rules`. An explicit `--rule `, at least +/// one `--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 ` skips one matching child directory name or path @@ -433,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], diff --git a/cli/cli_wbtest.mbt b/cli/cli_wbtest.mbt index 57a2259..2a900f6 100644 --- a/cli/cli_wbtest.mbt +++ b/cli/cli_wbtest.mbt @@ -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"]) { @@ -499,6 +512,9 @@ test "cli args render argparse help" { 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]")) } ///| diff --git a/docs/WritingRules.md b/docs/WritingRules.md index 560536d..ea5299a 100644 --- a/docs/WritingRules.md +++ b/docs/WritingRules.md @@ -13,10 +13,11 @@ See also: ## Introduction YAML rule files are scanner input. A rules root can be any directory supplied to -the `moongrep scan` CLI with `--rules` or `-r`. Files ending in `.yaml` or -`.yml` are discovered recursively below that root; other files are ignored. The -discovered files are loaded in sorted order for deterministic output. An empty -rules root is an error. +the `moongrep scan` CLI with `--rules` or `-r`. When no rule source option is +given, the rules root defaults to `./.moongrep/rules`. Files ending in `.yaml` +or `.yml` are discovered recursively below that root; other files are ignored. +The discovered files are loaded in sorted order for deterministic output. An +empty rules root is an error. Rule ids come from the rule file directory plus the YAML `id`. For example, `rules/security/raw.yaml` with `id: raw-html` becomes `security/raw-html` when @@ -679,21 +680,23 @@ Run the scanner from the root of the MoonBit module you want to scan. If `moongrep` is installed, use: ```bash -moongrep scan [--verbose] --rules [scan-root] +moongrep scan [--verbose] [--rules ] [scan-root] ``` To run the published WebAssembly CLI from Mooncakes without installing `moongrep`, use: ```bash -moonx moonbit-community/moongrep -- scan [--verbose] --rules [scan-root] +moonx moonbit-community/moongrep -- scan [--verbose] [--rules ] [scan-root] ``` `--rules=` and `-r ` are accepted as equivalent forms. -If `scan-root` is omitted, the scanner uses `.`. Match results are streamed to -standard output. `--verbose` writes loaded rule ids and directory traversal -progress to standard error as the scan proceeds; scan warnings also use -standard error. +If no rule source option is given, `--rules` uses `./.moongrep/rules`. An +explicit `--rule`, `--pattern`, or `--enable-builtin-rules` suppresses this +default. If `scan-root` is omitted, the scanner uses `.`. Match results are +streamed to standard output. `--verbose` writes loaded rule ids and directory +traversal progress to standard error as the scan proceeds; scan warnings also +use standard error. The scanner uses the untyped AST matcher by default. Repeated `exp`, `arg`, `pat`, and `type` captures are compared by structural untyped AST equality diff --git a/docs/WritingRules_CN.md b/docs/WritingRules_CN.md index 12b99a1..8392854 100644 --- a/docs/WritingRules_CN.md +++ b/docs/WritingRules_CN.md @@ -10,7 +10,7 @@ ## 介绍 -YAML 规则文件是扫描器的输入。规则根目录可以是通过 `--rules` 或 `-r` 传给 `moongrep scan` CLI 的任意目录。以 `.yaml` 或 `.yml` 结尾的文件会在该根目录下递归发现;其他文件会被忽略。发现的文件会按排序后的顺序加载,以得到确定性的输出。空规则根目录是错误。 +YAML 规则文件是扫描器的输入。规则根目录可以是通过 `--rules` 或 `-r` 传给 `moongrep scan` CLI 的任意目录。未指定规则来源选项时,规则根目录默认为 `./.moongrep/rules`。以 `.yaml` 或 `.yml` 结尾的文件会在该根目录下递归发现;其他文件会被忽略。发现的文件会按排序后的顺序加载,以得到确定性的输出。空规则根目录是错误。 规则 id 来自规则文件目录加 YAML `id`。例如,当 `rules` 是规则根目录时,`rules/security/raw.yaml` 中的 `id: raw-html` 会变成 `security/raw-html`。直接位于规则根目录下的文件只使用其 `id`。文件名不参与规则 id。YAML `id` 不能为空,且不能包含 `/`;目录归属由文件位置编码。 @@ -531,18 +531,20 @@ patterns: 在要扫描的 MoonBit 模块根目录下运行扫描器。如果已安装 `moongrep`,请使用: ```bash -moongrep scan [--verbose] --rules [scan-root] +moongrep scan [--verbose] [--rules ] [scan-root] ``` 如果不安装 `moongrep`,而是通过 Mooncakes 运行已发布的 WebAssembly CLI,请使用: ```bash -moonx moonbit-community/moongrep -- scan [--verbose] --rules [scan-root] +moonx moonbit-community/moongrep -- scan [--verbose] [--rules ] [scan-root] ``` -`--rules=` 和 `-r ` 是等价形式。如果省略 `scan-root`, -扫描器使用 `.`。匹配结果会流式写入标准输出;`--verbose` 会在扫描过程中把已加载的 -rule id 和目录遍历进度写入标准错误,扫描 warning 也写入标准错误。 +`--rules=` 和 `-r ` 是等价形式。未指定规则来源选项时, +`--rules` 使用 `./.moongrep/rules`;显式指定 `--rule`、`--pattern` 或 +`--enable-builtin-rules` 会停用该默认值。如果省略 `scan-root`,扫描器使用 `.`。 +匹配结果会流式写入标准输出;`--verbose` 会在扫描过程中把已加载的 rule id 和 +目录遍历进度写入标准错误,扫描 warning 也写入标准错误。 扫描器默认使用 untyped AST matcher。重复的 `exp`、`arg`、`pat` 和 `type` 捕获会按忽略源码位置的 untyped AST 结构相等性进行比较。 diff --git a/docs/writing_rules.mbt b/docs/writing_rules.mbt index b5b040f..3fbb0a3 100644 --- a/docs/writing_rules.mbt +++ b/docs/writing_rules.mbt @@ -17,10 +17,11 @@ let _embed_writingrules_md : String = #|## Introduction #| #|YAML rule files are scanner input. A rules root can be any directory supplied to - #|the `moongrep scan` CLI with `--rules` or `-r`. Files ending in `.yaml` or - #|`.yml` are discovered recursively below that root; other files are ignored. The - #|discovered files are loaded in sorted order for deterministic output. An empty - #|rules root is an error. + #|the `moongrep scan` CLI with `--rules` or `-r`. When no rule source option is + #|given, the rules root defaults to `./.moongrep/rules`. Files ending in `.yaml` + #|or `.yml` are discovered recursively below that root; other files are ignored. + #|The discovered files are loaded in sorted order for deterministic output. An + #|empty rules root is an error. #| #|Rule ids come from the rule file directory plus the YAML `id`. For example, #|`rules/security/raw.yaml` with `id: raw-html` becomes `security/raw-html` when @@ -683,21 +684,23 @@ let _embed_writingrules_md : String = #|`moongrep` is installed, use: #| #|```bash - #|moongrep scan [--verbose] --rules [scan-root] + #|moongrep scan [--verbose] [--rules ] [scan-root] #|``` #| #|To run the published WebAssembly CLI from Mooncakes without installing #|`moongrep`, use: #| #|```bash - #|moonx moonbit-community/moongrep -- scan [--verbose] --rules [scan-root] + #|moonx moonbit-community/moongrep -- scan [--verbose] [--rules ] [scan-root] #|``` #| #|`--rules=` and `-r ` are accepted as equivalent forms. - #|If `scan-root` is omitted, the scanner uses `.`. Match results are streamed to - #|standard output. `--verbose` writes loaded rule ids and directory traversal - #|progress to standard error as the scan proceeds; scan warnings also use - #|standard error. + #|If no rule source option is given, `--rules` uses `./.moongrep/rules`. An + #|explicit `--rule`, `--pattern`, or `--enable-builtin-rules` suppresses this + #|default. If `scan-root` is omitted, the scanner uses `.`. Match results are + #|streamed to standard output. `--verbose` writes loaded rule ids and directory + #|traversal progress to standard error as the scan proceeds; scan warnings also + #|use standard error. #| #|The scanner uses the untyped AST matcher by default. Repeated `exp`, `arg`, #|`pat`, and `type` captures are compared by structural untyped AST equality diff --git a/e2etests/BASIC.md b/e2etests/BASIC.md index 14892bb..ec57a95 100644 --- a/e2etests/BASIC.md +++ b/e2etests/BASIC.md @@ -22,16 +22,10 @@ Options: ## moongrep subcommands without arguments -The `scan`, `docs`, and `dump` subcommands print their help when invoked -without arguments. `lint` instead treats an omitted scan root as the current -directory because builtin rules are enabled automatically. - -The scan subcommand falls back to its help when no scan root or rule input is -provided. - -```mooncram -$ moonrun "$TESTDIR"/moongrep.wasm -- scan > /dev/null && diff -u <(moonrun "$TESTDIR"/moongrep.wasm -- scan --help) <(moonrun "$TESTDIR"/moongrep.wasm -- scan) -``` +The `docs` and `dump` subcommands print their help when invoked without +arguments. `scan` and `lint` instead treat an omitted scan root as the current +directory. `scan` uses its default rules directory, while `lint` enables +builtin rules automatically. The docs subcommand uses the same no-argument help behavior. @@ -98,7 +92,7 @@ Options: --verbose Write loaded rule ids and traversal progress to stderr. --enable-builtin-rules Enable embedded builtin rules. --output-json Write each match as one JSON record to stdout. - -r, --rules Directory containing YAML rules. + -r, --rules Directory containing YAML rules. [default: ./.moongrep/rules] --rule Single YAML rule file. --pattern Anonymous structural pattern to match. --guard YAML guard map for the preceding anonymous pattern. diff --git a/e2etests/SCAN.md b/e2etests/SCAN.md index d60b4fa..4e43f62 100644 --- a/e2etests/SCAN.md +++ b/e2etests/SCAN.md @@ -1,5 +1,18 @@ ## Directory scanning +When no rule source option is present, `scan` loads rules from +`./.moongrep/rules`. The path is resolved from the current directory. + +```mooncram +$ cd "$TESTDIR"/../testdata/default-rules && moonrun "$TESTDIR"/moongrep.wasm -- scan +./src/hit.mbt:1:13-1:21 +rule: example +description: + Target call. +source: +1 > fn sample { target() } +``` + The scanner ignores common repository and build directories by default. The fixture creates matching files under `.git`, `_build`, `.mooncakes`, and `target`, but only the source file at the scan root is reported. diff --git a/testdata/default-rules/.moongrep/rules/example.yaml b/testdata/default-rules/.moongrep/rules/example.yaml new file mode 100644 index 0000000..ee341fe --- /dev/null +++ b/testdata/default-rules/.moongrep/rules/example.yaml @@ -0,0 +1,5 @@ +id: example +description: | + Target call. +patterns: + - shape: target() diff --git a/testdata/default-rules/src/hit.mbt b/testdata/default-rules/src/hit.mbt new file mode 100644 index 0000000..c72a18f --- /dev/null +++ b/testdata/default-rules/src/hit.mbt @@ -0,0 +1 @@ +fn sample { target() } From dad068517dd3ae2996a958a7b910f0f5fed30876 Mon Sep 17 00:00:00 2001 From: myfreess Date: Tue, 11 Aug 2026 18:15:54 +0800 Subject: [PATCH 5/5] update version --- moon.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moon.mod b/moon.mod index 849415f..1ef09f2 100644 --- a/moon.mod +++ b/moon.mod @@ -1,6 +1,6 @@ name = "moonbit-community/moongrep" -version = "0.1.17" +version = "0.1.18" preferred_target = "wasm"