Skip to content
Open
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
59 changes: 53 additions & 6 deletions src/scanner/prefilter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,11 +216,56 @@ impl PreFilter {
}
}

/// Simple pattern matching for test frameworks
/// Helper to check if a token or module pattern matches on identifier boundaries
fn is_identifier_character(c: char) -> bool {
c.is_alphanumeric() || c == '_'
}

fn matches_token_or_module(text: &str, pattern: &str) -> bool {
if pattern.contains('.')
|| pattern.contains('@')
|| pattern.contains('/')
|| pattern.contains('-')
{
return text.contains(pattern);
}

let mut search_idx = 0;
while let Some(pos) = text[search_idx..].find(pattern) {
let abs_pos = search_idx + pos;
let before_ok = if abs_pos == 0 {
true
} else {
let prev_char = text[..abs_pos].chars().next_back().unwrap();
!Self::is_identifier_character(prev_char)
};

let end_pos = abs_pos + pattern.len();
let after_ok = if end_pos >= text.len() {
true
} else {
let next_char = text[end_pos..].chars().next().unwrap();
!Self::is_identifier_character(next_char)
};

if before_ok && after_ok {
return true;
}

search_idx = abs_pos + pattern.len();
}

false
}

/// Pattern matching for test frameworks on token boundaries
fn has_test_patterns(&self, imports_text: &str) -> bool {
let test_patterns = [
// Universal test indicators
"test",
"testing",
"testify",
"vitest",
"mock",
"spec",
"jest",
Expand All @@ -235,30 +280,32 @@ impl PreFilter {
"django.test",
"flask.testing",
"@testing-library",
"github.com/stretchr/testify",
"org.junit",
"org.mockito",
"org.testng",
];

test_patterns.iter().any(|pattern| imports_text.contains(pattern))
test_patterns.iter().any(|pattern| Self::matches_token_or_module(imports_text, pattern))

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

vitest, Go testing, and testify no longer match the test patterns, so their imports are scanned. could we retain explicit patterns for these framework roots while preserving the boundary fix?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@juangaitanv Thanks for the review! Added explicit patterns for "testing", "testify", "vitest", and "github.com/stretchr/testify" to test_patterns while retaining token boundary checking. Added unit test file_with_testing_or_vitest_import_is_skipped.

}

/// Simple pattern matching for migration frameworks
/// Pattern matching for migration frameworks on token boundaries
fn has_migration_patterns(&self, imports_text: &str) -> bool {
let migration_patterns = [
"migration",
"migrations",
"migrate",
"django.db.migrations",
"alembic",
"flyway",
"liquibase",
"django.db.migrations",
"sequelize",
"knex",
"typeorm",
];

migration_patterns.iter().any(|pattern| imports_text.contains(pattern))
migration_patterns
.iter()
.any(|pattern| Self::matches_token_or_module(imports_text, pattern))
}

pub fn filter_files(
Expand Down
53 changes: 53 additions & 0 deletions tests/unit/prefilter_should_scan_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,57 @@ mod prefilter_should_scan_tests {
let filter = PreFilter::new(&empty_rules(), "python");
assert!(filter.should_scan_file(path.to_str().unwrap()));
}

#[test]
fn production_file_with_attestation_or_contest_import_is_scanned() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("auth_service.py");
fs::write(
&path,
"from myapp.attestation import verify_attestation\nfrom contest_service import handle_contest\nfrom services.latest_events import get_latest\nfrom fastest_cache import cache\n\ndef login(req):\n return verify_attestation(req)\n",
)
.unwrap();

let filter = PreFilter::new(&empty_rules(), "python");
assert!(filter.should_scan_file(path.to_str().unwrap()));
}

#[test]
fn production_file_with_migrate_user_import_is_scanned() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("account_service.py");
fs::write(
&path,
"from accounts.user_migration import user_migration_step\n\ndef run(user):\n user_migration_step(user)\n",
)
.unwrap();

let filter = PreFilter::new(&empty_rules(), "python");
assert!(filter.should_scan_file(path.to_str().unwrap()));
}

#[test]
fn file_with_testing_or_vitest_import_is_skipped() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("service.ts");
fs::write(&path, "import { describe, it } from 'vitest';\n\ndescribe('test', () => {});\n")
.unwrap();

let filter = PreFilter::new(&empty_rules(), "typescript");
assert!(!filter.should_scan_file(path.to_str().unwrap()));
}

#[test]
fn django_migration_file_with_migrations_import_is_skipped() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("0001_initial.py");
fs::write(
&path,
"from django.db import migrations, models\n\nclass Migration(migrations.Migration):\n dependencies = []\n",
)
.unwrap();

let filter = PreFilter::new(&empty_rules(), "python");
assert!(!filter.should_scan_file(path.to_str().unwrap()));
}
}
Loading