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
12 changes: 12 additions & 0 deletions docs/src/content/docs/pattern-syntax/matching-behavior.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ Negation patterns where all alternatives start with `-` also use order-independe
| `find . -delete` | Does not match |
| `find -fprint output .` | Does not match |

This also works with `=`-joined flags. For example, `!--pre` rejects both `--pre value` (space-separated) and `--pre=value` (`=`-joined):

```yaml
- allow: 'rg !--pre *'
```

| Command | Result |
| ------------------------ | -------------- |
| `rg pattern file.txt` | Matches |
| `rg --pre pdftotext pat` | Does not match |
| `rg --pre=pdftotext pat` | Does not match |

### Non-flag Tokens are Position-dependent

Tokens that do not start with `-` are matched **in order**:
Expand Down
44 changes: 42 additions & 2 deletions src/rules/pattern_matcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ fn match_tokens_core<'a>(
let negation_passed = if is_flag_only_negation(inner) {
!cmd_tokens
.iter()
.any(|t| match_single_token(inner, t, definitions))
.any(|t| match_flag_token_with_equals(inner, t, definitions))
} else {
!match_single_token(inner, cmd_tokens[0], definitions)
};
Expand Down Expand Up @@ -503,7 +503,7 @@ fn extract_placeholder_all<'a>(
let negation_passed = if is_flag_only_negation(inner) {
!cmd_tokens
.iter()
.any(|t| match_single_token(inner, t, definitions))
.any(|t| match_flag_token_with_equals(inner, t, definitions))
} else {
!match_single_token(inner, cmd_tokens[0], definitions)
};
Expand Down Expand Up @@ -837,6 +837,29 @@ fn is_flag_only_negation(inner: &PatternToken) -> bool {
}
}

/// Like [`match_single_token`] but also matches the flag portion of
/// `=`-joined command tokens (e.g. `--pre=pdftotext` matches pattern `--pre`).
/// This is used for flag-only negation so that `\!--pre` correctly rejects
/// `--pre=value` in addition to the space-separated `--pre value` form.
fn match_flag_token_with_equals(
pattern: &PatternToken,
cmd_token: &str,
definitions: &Definitions,
) -> bool {
if match_single_token(pattern, cmd_token, definitions) {
return true;
}
// If the command token contains `=` and the flag part starts with `-`,
// try matching only the flag portion before `=`.
if let Some(eq_pos) = cmd_token.find('=') {
let flag_part = &cmd_token[..eq_pos];
if flag_part.starts_with('-') {
return match_single_token(pattern, flag_part, definitions);
}
}
false
}

/// Remove elements at the given indices from a slice, returning a new Vec.
fn remove_indices<'a>(tokens: &[&'a str], indices: &[usize]) -> Vec<&'a str> {
tokens
Expand Down Expand Up @@ -988,6 +1011,23 @@ mod tests {
"find . -type f -name foo",
true
)]
// Flag-only negation with `=`-joined tokens
#[case::flag_negation_rejects_equals_form("rg !--pre *", "rg --pre=pdftotext pattern", false)]
#[case::flag_negation_allows_different_flag_equals(
"rg !--pre *",
"rg --color=always pattern",
true
)]
#[case::flag_negation_alt_rejects_equals_form(
"sort !-o|--output|--compress-program *",
"sort --output=result.txt file.txt",
false
)]
#[case::flag_negation_alt_allows_equals_different_flag(
"sort !-o|--output|--compress-program *",
"sort --reverse=true file.txt",
true
)]
fn negation_matching(
#[case] pattern_str: &str,
#[case] command_str: &str,
Expand Down
45 changes: 45 additions & 0 deletions tests/integration/config_to_rule_evaluation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -955,3 +955,48 @@ fn flag_negation_order_independent(
let result = evaluate_command(&config, command, &empty_context).unwrap();
expected(&result.action);
}

// ========================================
// Flag-only negation with `=`-joined tokens
// ========================================

#[rstest]
#[case::equals_form_rejected(
"rg --pre=pdftotext pattern",
assert_default as ActionAssertion,
)]
#[case::space_form_rejected(
"rg --pre pdftotext pattern",
assert_default as ActionAssertion,
)]
#[case::different_flag_equals_allowed(
"rg --color=always pattern",
assert_allow as ActionAssertion,
)]
#[case::no_flag_allowed(
"rg pattern file.txt",
assert_allow as ActionAssertion,
)]
#[case::alt_negation_equals_rejected(
"sort --output=result.txt file.txt",
assert_default as ActionAssertion,
)]
#[case::alt_negation_equals_different_flag_allowed(
"sort --reverse file.txt",
assert_allow as ActionAssertion,
)]
fn flag_negation_equals_form(
#[case] command: &str,
#[case] expected: ActionAssertion,
empty_context: EvalContext,
) {
let config = parse_config(indoc! {"
rules:
- allow: 'rg !--pre *'
- allow: 'sort !-o|--output|--compress-program *'
"})
.unwrap();

let result = evaluate_command(&config, command, &empty_context).unwrap();
expected(&result.action);
}
Loading