diff --git a/src/scanner/prefilter.rs b/src/scanner/prefilter.rs index d3fab9f..2fbc7ca 100644 --- a/src/scanner/prefilter.rs +++ b/src/scanner/prefilter.rs @@ -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", @@ -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)) } - /// 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( diff --git a/tests/unit/prefilter_should_scan_tests.rs b/tests/unit/prefilter_should_scan_tests.rs index 39abb06..f3483b1 100644 --- a/tests/unit/prefilter_should_scan_tests.rs +++ b/tests/unit/prefilter_should_scan_tests.rs @@ -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())); + } }