attocode-code-intel 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attocode_code_intel-0.1.0.dist-info/METADATA +221 -0
- attocode_code_intel-0.1.0.dist-info/RECORD +345 -0
- attocode_code_intel-0.1.0.dist-info/WHEEL +4 -0
- attocode_code_intel-0.1.0.dist-info/entry_points.txt +2 -0
- attocode_code_intel-0.1.0.dist-info/licenses/LICENSE +21 -0
- attocode_intel/GUIDELINES.md +432 -0
- attocode_intel/__init__.py +52 -0
- attocode_intel/_internal/__init__.py +1 -0
- attocode_intel/_internal/errors/__init__.py +126 -0
- attocode_intel/_internal/integrations/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/__init__.py +1 -0
- attocode_intel/_internal/integrations/context/architecture_drift.py +354 -0
- attocode_intel/_internal/integrations/context/ast_chunker.py +202 -0
- attocode_intel/_internal/integrations/context/ast_service.py +1483 -0
- attocode_intel/_internal/integrations/context/blob_oid.py +423 -0
- attocode_intel/_internal/integrations/context/cache_manifest.py +221 -0
- attocode_intel/_internal/integrations/context/cas.py +466 -0
- attocode_intel/_internal/integrations/context/code_analyzer.py +528 -0
- attocode_intel/_internal/integrations/context/codebase_ast.py +1461 -0
- attocode_intel/_internal/integrations/context/codebase_context.py +1736 -0
- attocode_intel/_internal/integrations/context/cross_mode.py +263 -0
- attocode_intel/_internal/integrations/context/cross_references.py +616 -0
- attocode_intel/_internal/integrations/context/embedding_rotation.py +597 -0
- attocode_intel/_internal/integrations/context/embeddings.py +282 -0
- attocode_intel/_internal/integrations/context/frecency.py +427 -0
- attocode_intel/_internal/integrations/context/fuzzy.py +320 -0
- attocode_intel/_internal/integrations/context/hierarchical_explorer.py +331 -0
- attocode_intel/_internal/integrations/context/hydration.py +90 -0
- attocode_intel/_internal/integrations/context/index_repair.py +87 -0
- attocode_intel/_internal/integrations/context/index_store.py +586 -0
- attocode_intel/_internal/integrations/context/memory_store.py +602 -0
- attocode_intel/_internal/integrations/context/module_resolution.py +52 -0
- attocode_intel/_internal/integrations/context/query_constraints.py +361 -0
- attocode_intel/_internal/integrations/context/query_history.py +417 -0
- attocode_intel/_internal/integrations/context/reranker.py +351 -0
- attocode_intel/_internal/integrations/context/semantic_search.py +2931 -0
- attocode_intel/_internal/integrations/context/syntax_evidence.py +94 -0
- attocode_intel/_internal/integrations/context/systemone_ranker.py +245 -0
- attocode_intel/_internal/integrations/context/temporal_coupling.py +411 -0
- attocode_intel/_internal/integrations/context/trigram_index.py +820 -0
- attocode_intel/_internal/integrations/context/trigram_regex.py +171 -0
- attocode_intel/_internal/integrations/context/ts_parser.py +2199 -0
- attocode_intel/_internal/integrations/context/vector_store.py +795 -0
- attocode_intel/_internal/integrations/feature_flags/__init__.py +357 -0
- attocode_intel/_internal/integrations/lsp/__init__.py +1 -0
- attocode_intel/_internal/integrations/lsp/client.py +1140 -0
- attocode_intel/_internal/integrations/mcp/__init__.py +1 -0
- attocode_intel/_internal/integrations/mcp/client.py +253 -0
- attocode_intel/_internal/integrations/security/__init__.py +1 -0
- attocode_intel/_internal/integrations/security/custom_rules.py +176 -0
- attocode_intel/_internal/integrations/security/dataflow.py +522 -0
- attocode_intel/_internal/integrations/security/dependency_audit.py +303 -0
- attocode_intel/_internal/integrations/security/matcher.py +44 -0
- attocode_intel/_internal/integrations/security/patterns.py +724 -0
- attocode_intel/_internal/integrations/security/scanner.py +351 -0
- attocode_intel/_internal/integrations/utilities/__init__.py +1 -0
- attocode_intel/_internal/integrations/utilities/ignore.py +64 -0
- attocode_intel/_internal/integrations/utilities/token_estimate.py +51 -0
- attocode_intel/_internal/tricks/__init__.py +1 -0
- attocode_intel/_internal/tricks/recursive_context.py +357 -0
- attocode_intel/_internal/types/__init__.py +1 -0
- attocode_intel/_internal/types/events.py +289 -0
- attocode_intel/_internal/types/severity.py +15 -0
- attocode_intel/_shared.py +341 -0
- attocode_intel/analysis_coverage.py +56 -0
- attocode_intel/api/__init__.py +1 -0
- attocode_intel/api/access.py +32 -0
- attocode_intel/api/app.py +247 -0
- attocode_intel/api/auth/__init__.py +128 -0
- attocode_intel/api/auth/api_keys.py +61 -0
- attocode_intel/api/auth/context.py +34 -0
- attocode_intel/api/auth/jwt.py +66 -0
- attocode_intel/api/auth/oauth.py +146 -0
- attocode_intel/api/auth/passwords.py +15 -0
- attocode_intel/api/deps.py +351 -0
- attocode_intel/api/knowledge_bridge.py +13 -0
- attocode_intel/api/mcp_transport.py +51 -0
- attocode_intel/api/middleware.py +411 -0
- attocode_intel/api/models.py +698 -0
- attocode_intel/api/providers/__init__.py +1 -0
- attocode_intel/api/providers/db_provider.py +1106 -0
- attocode_intel/api/providers/local_provider.py +476 -0
- attocode_intel/api/providers/remote_provider.py +740 -0
- attocode_intel/api/repository_source.py +21 -0
- attocode_intel/api/routes/__init__.py +1 -0
- attocode_intel/api/routes/_pin_helper.py +158 -0
- attocode_intel/api/routes/activity.py +148 -0
- attocode_intel/api/routes/adr.py +102 -0
- attocode_intel/api/routes/analysis.py +515 -0
- attocode_intel/api/routes/api_keys.py +170 -0
- attocode_intel/api/routes/auth.py +428 -0
- attocode_intel/api/routes/branches.py +262 -0
- attocode_intel/api/routes/cross_repo_search.py +198 -0
- attocode_intel/api/routes/embeddings.py +386 -0
- attocode_intel/api/routes/files.py +132 -0
- attocode_intel/api/routes/files_v2.py +388 -0
- attocode_intel/api/routes/gc.py +322 -0
- attocode_intel/api/routes/git_v2.py +433 -0
- attocode_intel/api/routes/graph.py +174 -0
- attocode_intel/api/routes/graph_viz.py +160 -0
- attocode_intel/api/routes/health.py +62 -0
- attocode_intel/api/routes/history.py +175 -0
- attocode_intel/api/routes/jobs.py +84 -0
- attocode_intel/api/routes/learning.py +205 -0
- attocode_intel/api/routes/lsp.py +272 -0
- attocode_intel/api/routes/notify.py +623 -0
- attocode_intel/api/routes/operations.py +27 -0
- attocode_intel/api/routes/orgs.py +924 -0
- attocode_intel/api/routes/preferences.py +100 -0
- attocode_intel/api/routes/presence.py +42 -0
- attocode_intel/api/routes/projects.py +116 -0
- attocode_intel/api/routes/repos.py +47 -0
- attocode_intel/api/routes/search.py +180 -0
- attocode_intel/api/routes/snapshots.py +614 -0
- attocode_intel/api/routes/webhooks.py +313 -0
- attocode_intel/api/routes/websocket.py +116 -0
- attocode_intel/api/static/graph.html +239 -0
- attocode_intel/api/utils.py +96 -0
- attocode_intel/artifacts/__init__.py +45 -0
- attocode_intel/artifacts/hashing.py +230 -0
- attocode_intel/artifacts/provenance.py +190 -0
- attocode_intel/artifacts/retrieval_pin.py +159 -0
- attocode_intel/audit.py +59 -0
- attocode_intel/bug_finder.py +206 -0
- attocode_intel/bundle.py +80 -0
- attocode_intel/catalog.py +242 -0
- attocode_intel/change_review.py +75 -0
- attocode_intel/cli.py +3062 -0
- attocode_intel/community.py +63 -0
- attocode_intel/confidence/__init__.py +144 -0
- attocode_intel/confidence/jev.py +234 -0
- attocode_intel/confidence/llm.py +330 -0
- attocode_intel/confidence/redact.py +44 -0
- attocode_intel/confidence/settings.py +74 -0
- attocode_intel/config.py +256 -0
- attocode_intel/crypto.py +31 -0
- attocode_intel/db/__init__.py +7 -0
- attocode_intel/db/base.py +33 -0
- attocode_intel/db/engine.py +61 -0
- attocode_intel/db/models.py +529 -0
- attocode_intel/entrypoint.py +185 -0
- attocode_intel/focused_evidence.py +130 -0
- attocode_intel/freshness.py +154 -0
- attocode_intel/gateway.py +734 -0
- attocode_intel/git/__init__.py +7 -0
- attocode_intel/git/credentials.py +75 -0
- attocode_intel/git/manager.py +493 -0
- attocode_intel/git/models.py +96 -0
- attocode_intel/git/storage.py +98 -0
- attocode_intel/graph_query_parser.py +761 -0
- attocode_intel/helpers.py +712 -0
- attocode_intel/indexing/__init__.py +29 -0
- attocode_intel/indexing/debouncer.py +136 -0
- attocode_intel/indexing/delta_indexer.py +151 -0
- attocode_intel/indexing/full_indexer.py +323 -0
- attocode_intel/indexing/incremental.py +240 -0
- attocode_intel/indexing/parser.py +403 -0
- attocode_intel/indexing/summarizer.py +264 -0
- attocode_intel/installer.py +1896 -0
- attocode_intel/knowledge.py +269 -0
- attocode_intel/local_knowledge.py +77 -0
- attocode_intel/migrations/alembic.ini +36 -0
- attocode_intel/migrations/env.py +71 -0
- attocode_intel/migrations/script.py.mako +27 -0
- attocode_intel/migrations/versions/001_initial_schema.py +219 -0
- attocode_intel/migrations/versions/002_branch_version_and_embeddings.py +61 -0
- attocode_intel/migrations/versions/003_embedding_uniqueness_and_cascade.py +51 -0
- attocode_intel/migrations/versions/004_audit_events.py +38 -0
- attocode_intel/migrations/versions/005_user_preferences.py +30 -0
- attocode_intel/migrations/versions/006_google_oauth.py +27 -0
- attocode_intel/migrations/versions/007_pgvector_embeddings.py +28 -0
- attocode_intel/migrations/versions/008_symbol_references.py +37 -0
- attocode_intel/migrations/versions/009_webhook_secret_encryption.py +25 -0
- attocode_intel/migrations/versions/010_add_commits_table.py +40 -0
- attocode_intel/migrations/versions/011_commit_changed_files.py +25 -0
- attocode_intel/migrations/versions/012_branch_merged_at.py +25 -0
- attocode_intel/migrations/versions/013_revoked_tokens.py +32 -0
- attocode_intel/migrations/versions/014_blame_hunks.py +40 -0
- attocode_intel/migrations/versions/015_commit_file_stats.py +40 -0
- attocode_intel/migrations/versions/016_embedding_provenance_columns.py +179 -0
- attocode_intel/migrations/versions/017_provenance_table.py +102 -0
- attocode_intel/migrations/versions/018_repo_snapshots.py +178 -0
- attocode_intel/migrations/versions/019_symref_caller.py +42 -0
- attocode_intel/migrations/versions/020_shared_knowledge.py +29 -0
- attocode_intel/onboarding.py +258 -0
- attocode_intel/output.py +178 -0
- attocode_intel/precision.py +133 -0
- attocode_intel/presence.py +160 -0
- attocode_intel/probe.py +110 -0
- attocode_intel/project_dir.py +97 -0
- attocode_intel/pubsub.py +113 -0
- attocode_intel/query_ranking.py +242 -0
- attocode_intel/readiness.py +972 -0
- attocode_intel/redis.py +51 -0
- attocode_intel/reference_pages.py +60 -0
- attocode_intel/remote.py +131 -0
- attocode_intel/repo_ranker.py +246 -0
- attocode_intel/request_context.py +82 -0
- attocode_intel/rules/__init__.py +28 -0
- attocode_intel/rules/ci.py +320 -0
- attocode_intel/rules/combinators.py +293 -0
- attocode_intel/rules/enricher.py +127 -0
- attocode_intel/rules/evolution.py +452 -0
- attocode_intel/rules/executor.py +531 -0
- attocode_intel/rules/filters/__init__.py +5 -0
- attocode_intel/rules/filters/pipeline.py +151 -0
- attocode_intel/rules/formatter.py +198 -0
- attocode_intel/rules/hygiene.py +242 -0
- attocode_intel/rules/importers/__init__.py +1 -0
- attocode_intel/rules/importers/semgrep.py +382 -0
- attocode_intel/rules/loader.py +292 -0
- attocode_intel/rules/marketplace.py +375 -0
- attocode_intel/rules/metavar.py +181 -0
- attocode_intel/rules/model.py +317 -0
- attocode_intel/rules/packs/__init__.py +1 -0
- attocode_intel/rules/packs/community/_license_templates/Apache-2.0.txt +176 -0
- attocode_intel/rules/packs/community/_license_templates/MIT.txt +21 -0
- attocode_intel/rules/packs/community/bandit-python/LICENSE +176 -0
- attocode_intel/rules/packs/community/bandit-python/NOTICE +13 -0
- attocode_intel/rules/packs/community/bandit-python/PORTING.md +23 -0
- attocode_intel/rules/packs/community/bandit-python/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B105-hardcoded-password.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B303-weak-md5.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B304-weak-des.yaml +20 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B307-use-of-eval.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B324-weak-sha1.yaml +21 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B501-no-cert-validation.yaml +23 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B602-shell-true.yaml +22 -0
- attocode_intel/rules/packs/community/bandit-python/rules/B608-sqli.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/LICENSE +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/NOTICE +13 -0
- attocode_intel/rules/packs/community/eslint-typescript/PORTING.md +23 -0
- attocode_intel/rules/packs/community/eslint-typescript/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/eqeqeq.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-eval.yaml +22 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-implied-eval.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-new-func.yaml +21 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-throw-literal.yaml +19 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-var.yaml +20 -0
- attocode_intel/rules/packs/community/eslint-typescript/rules/no-with.yaml +18 -0
- attocode_intel/rules/packs/community/gosec-go/LICENSE +176 -0
- attocode_intel/rules/packs/community/gosec-go/NOTICE +13 -0
- attocode_intel/rules/packs/community/gosec-go/PORTING.md +23 -0
- attocode_intel/rules/packs/community/gosec-go/manifest.yaml +14 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G101-hardcoded-credentials.yaml +22 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G102-bind-all-interfaces.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G103-unsafe-block.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G201-sql-format.yaml +21 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G304-file-inclusion.yaml +23 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G401-weak-cipher.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G402-tls-min-version.yaml +20 -0
- attocode_intel/rules/packs/community/gosec-go/rules/G501-md5-import.yaml +21 -0
- attocode_intel/rules/packs/examples/cpp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/cpp/rules/security.yaml +90 -0
- attocode_intel/rules/packs/examples/cpp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/csharp/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/csharp/rules/security.yaml +69 -0
- attocode_intel/rules/packs/examples/csharp/rules/structural.yaml +42 -0
- attocode_intel/rules/packs/examples/go/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/go/rules/performance.yaml +136 -0
- attocode_intel/rules/packs/examples/go/rules/security.yaml +48 -0
- attocode_intel/rules/packs/examples/go/rules/structural.yaml +58 -0
- attocode_intel/rules/packs/examples/go/taint/sanitizers.yaml +24 -0
- attocode_intel/rules/packs/examples/go/taint/sinks.yaml +51 -0
- attocode_intel/rules/packs/examples/go/taint/sources.yaml +26 -0
- attocode_intel/rules/packs/examples/java/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/java/rules/correctness.yaml +55 -0
- attocode_intel/rules/packs/examples/java/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/examples/java/taint/sinks.yaml +37 -0
- attocode_intel/rules/packs/examples/java/taint/sources.yaml +21 -0
- attocode_intel/rules/packs/examples/kotlin/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/kotlin/rules/correctness.yaml +50 -0
- attocode_intel/rules/packs/examples/kotlin/rules/structural.yaml +38 -0
- attocode_intel/rules/packs/examples/php/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/php/rules/security.yaml +66 -0
- attocode_intel/rules/packs/examples/php/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/python/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/python/rules/performance.yaml +90 -0
- attocode_intel/rules/packs/examples/python/rules/structural.yaml +55 -0
- attocode_intel/rules/packs/examples/ruby/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/ruby/rules/security.yaml +73 -0
- attocode_intel/rules/packs/examples/ruby/rules/structural.yaml +41 -0
- attocode_intel/rules/packs/examples/rust/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/rust/rules/correctness.yaml +51 -0
- attocode_intel/rules/packs/examples/rust/rules/structural.yaml +49 -0
- attocode_intel/rules/packs/examples/typescript/manifest.yaml +4 -0
- attocode_intel/rules/packs/examples/typescript/rules/performance.yaml +70 -0
- attocode_intel/rules/packs/examples/typescript/rules/structural.yaml +48 -0
- attocode_intel/rules/packs/pack_loader.py +244 -0
- attocode_intel/rules/plugins/__init__.py +1 -0
- attocode_intel/rules/plugins/plugin_loader.py +132 -0
- attocode_intel/rules/profiling.py +332 -0
- attocode_intel/rules/registry.py +213 -0
- attocode_intel/rules/sarif.py +187 -0
- attocode_intel/rules/synthesis.py +420 -0
- attocode_intel/rules/testing.py +326 -0
- attocode_intel/server.py +734 -0
- attocode_intel/service.py +3416 -0
- attocode_intel/sharing.py +57 -0
- attocode_intel/storage/__init__.py +6 -0
- attocode_intel/storage/branch_overlay.py +352 -0
- attocode_intel/storage/content_store.py +220 -0
- attocode_intel/storage/dependency_store.py +143 -0
- attocode_intel/storage/diff_engine.py +187 -0
- attocode_intel/storage/embedding_store.py +594 -0
- attocode_intel/storage/provenance_store.py +137 -0
- attocode_intel/storage/security_scanner_db.py +164 -0
- attocode_intel/storage/symbol_store.py +160 -0
- attocode_intel/structural_evidence.py +144 -0
- attocode_intel/symbol_inspection.py +141 -0
- attocode_intel/symbol_links.py +167 -0
- attocode_intel/telemetry.py +42 -0
- attocode_intel/test_ranking.py +73 -0
- attocode_intel/testing/__init__.py +43 -0
- attocode_intel/testing/fixtures.py +122 -0
- attocode_intel/testing/helpers.py +75 -0
- attocode_intel/testing/mocks.py +99 -0
- attocode_intel/tools/__init__.py +2 -0
- attocode_intel/tools/adr_tools.py +628 -0
- attocode_intel/tools/analysis_tools.py +382 -0
- attocode_intel/tools/composite_tools.py +695 -0
- attocode_intel/tools/cross_mode_tools.py +173 -0
- attocode_intel/tools/dead_code_tools.py +582 -0
- attocode_intel/tools/distill_tools.py +353 -0
- attocode_intel/tools/frecency_tools.py +220 -0
- attocode_intel/tools/fuzzy_tools.py +255 -0
- attocode_intel/tools/history_tools.py +486 -0
- attocode_intel/tools/learning_tools.py +222 -0
- attocode_intel/tools/lsp_tools.py +218 -0
- attocode_intel/tools/maintenance_tools.py +1662 -0
- attocode_intel/tools/navigation_tools.py +220 -0
- attocode_intel/tools/overlay_tools.py +540 -0
- attocode_intel/tools/pin_store.py +432 -0
- attocode_intel/tools/pin_tools.py +266 -0
- attocode_intel/tools/query_constraints_tools.py +262 -0
- attocode_intel/tools/query_history_tools.py +158 -0
- attocode_intel/tools/readiness_tools.py +44 -0
- attocode_intel/tools/rule_tools.py +992 -0
- attocode_intel/tools/search_tools.py +770 -0
- attocode_intel/tools/snapshot_tools.py +624 -0
- attocode_intel/workers/__init__.py +1 -0
- attocode_intel/workers/job_utils.py +111 -0
- attocode_intel/workers/jobs.py +573 -0
- attocode_intel/workers/run.py +30 -0
- attocode_intel/workers/settings.py +61 -0
|
@@ -0,0 +1,326 @@
|
|
|
1
|
+
"""Rule testing framework with inline annotations.
|
|
2
|
+
|
|
3
|
+
Verifies that rules produce the correct findings against annotated
|
|
4
|
+
source files. Three annotation types are supported:
|
|
5
|
+
|
|
6
|
+
``# expect: rule-id`` — rule MUST fire on this line (TP assertion)
|
|
7
|
+
``# ok: rule-id`` — rule must NOT fire on this line (FP guard)
|
|
8
|
+
``# todoruleid: rule-id`` — rule SHOULD fire but is known-missing (not fatal)
|
|
9
|
+
|
|
10
|
+
Non-Python comments (``//``) are also supported.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
from dataclasses import dataclass, field
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
from attocode_intel.rules.executor import execute_rules
|
|
20
|
+
from attocode_intel.rules.filters.pipeline import run_pipeline
|
|
21
|
+
from attocode_intel.rules.model import EnrichedFinding, UnifiedRule
|
|
22
|
+
|
|
23
|
+
# ---------------------------------------------------------------------------
|
|
24
|
+
# Annotation parsing
|
|
25
|
+
# ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
# Matches: # expect: rule-id, // expect: rule-id, # ok: rule-id, etc.
|
|
28
|
+
# Also accepts the semgrep-rules ``ruleid:`` marker as an alias for ``expect:``
|
|
29
|
+
# so test fixtures imported from semgrep work without preprocessing.
|
|
30
|
+
_ANNOTATION_RE = re.compile(
|
|
31
|
+
r"(?:#|//)\s*(expect|ok|todoruleid|ruleid):\s*([\w./-]+)"
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass(slots=True, frozen=True)
|
|
36
|
+
class Expectation:
|
|
37
|
+
"""A single test annotation from a source file."""
|
|
38
|
+
|
|
39
|
+
line: int
|
|
40
|
+
kind: str # "expect" | "ok" | "todoruleid"
|
|
41
|
+
rule_id: str
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def parse_annotations(file_path: str) -> list[Expectation]:
|
|
45
|
+
"""Parse all test annotations from a source file."""
|
|
46
|
+
expectations: list[Expectation] = []
|
|
47
|
+
try:
|
|
48
|
+
with open(file_path, encoding="utf-8") as f:
|
|
49
|
+
for i, line in enumerate(f, 1):
|
|
50
|
+
for m in _ANNOTATION_RE.finditer(line):
|
|
51
|
+
kind = m.group(1)
|
|
52
|
+
# semgrep "ruleid" is treated as our "expect"
|
|
53
|
+
if kind == "ruleid":
|
|
54
|
+
kind = "expect"
|
|
55
|
+
expectations.append(Expectation(
|
|
56
|
+
line=i,
|
|
57
|
+
kind=kind,
|
|
58
|
+
rule_id=m.group(2),
|
|
59
|
+
))
|
|
60
|
+
except OSError:
|
|
61
|
+
pass # unreadable file — return empty
|
|
62
|
+
return expectations
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
# Test result model
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@dataclass(slots=True)
|
|
71
|
+
class RuleTestFileResult:
|
|
72
|
+
"""Result of testing rules against a single annotated file."""
|
|
73
|
+
|
|
74
|
+
file_path: str
|
|
75
|
+
passed: list[Expectation] = field(default_factory=list)
|
|
76
|
+
failed: list[tuple[Expectation, str]] = field(default_factory=list)
|
|
77
|
+
todoruleid_unexpected_passes: list[Expectation] = field(default_factory=list)
|
|
78
|
+
false_positives: list[tuple[EnrichedFinding, Expectation]] = field(default_factory=list)
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def ok(self) -> bool:
|
|
82
|
+
return not self.failed and not self.false_positives
|
|
83
|
+
|
|
84
|
+
@property
|
|
85
|
+
def total_assertions(self) -> int:
|
|
86
|
+
return len(self.passed) + len(self.failed) + len(self.false_positives) + len(self.todoruleid_unexpected_passes)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@dataclass(slots=True)
|
|
90
|
+
class RuleTestSuiteResult:
|
|
91
|
+
"""Aggregated result across multiple test files."""
|
|
92
|
+
|
|
93
|
+
file_results: list[RuleTestFileResult] = field(default_factory=list)
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def ok(self) -> bool:
|
|
97
|
+
return all(r.ok for r in self.file_results)
|
|
98
|
+
|
|
99
|
+
@property
|
|
100
|
+
def total_passed(self) -> int:
|
|
101
|
+
return sum(len(r.passed) for r in self.file_results)
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def total_failed(self) -> int:
|
|
105
|
+
return sum(len(r.failed) for r in self.file_results)
|
|
106
|
+
|
|
107
|
+
@property
|
|
108
|
+
def total_false_positives(self) -> int:
|
|
109
|
+
return sum(len(r.false_positives) for r in self.file_results)
|
|
110
|
+
|
|
111
|
+
@property
|
|
112
|
+
def total_todoruleid_passes(self) -> int:
|
|
113
|
+
return sum(len(r.todoruleid_unexpected_passes) for r in self.file_results)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
# Matching helpers
|
|
118
|
+
# ---------------------------------------------------------------------------
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _finding_matches_rule(finding_rule_id: str, expected_rule_id: str) -> bool:
|
|
122
|
+
"""Check if a finding's rule_id matches an expected annotation.
|
|
123
|
+
|
|
124
|
+
Matching rules (in order):
|
|
125
|
+
1. Exact match: ``"security/eval"`` == ``"security/eval"``
|
|
126
|
+
2. Suffix match: ``"plugin:team/no-print"`` ends with ``"/no-print"``
|
|
127
|
+
"""
|
|
128
|
+
if finding_rule_id == expected_rule_id:
|
|
129
|
+
return True
|
|
130
|
+
if finding_rule_id.endswith("/" + expected_rule_id):
|
|
131
|
+
return True
|
|
132
|
+
return False
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
# Public alias — the rule-bench harness reuses this matcher.
|
|
136
|
+
match_finding_to_expectation = _finding_matches_rule
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
# ---------------------------------------------------------------------------
|
|
140
|
+
# Test runner
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
class RuleTestRunner:
|
|
145
|
+
"""Run rule tests against annotated fixture files."""
|
|
146
|
+
|
|
147
|
+
def __init__(self, rules: list[UnifiedRule], *, project_dir: str = "") -> None:
|
|
148
|
+
self._rules = rules
|
|
149
|
+
self._project_dir = project_dir
|
|
150
|
+
|
|
151
|
+
def run_test_file(self, file_path: str) -> RuleTestFileResult:
|
|
152
|
+
"""Run rules against a single annotated file and check expectations."""
|
|
153
|
+
result = RuleTestFileResult(file_path=file_path)
|
|
154
|
+
annotations = parse_annotations(file_path)
|
|
155
|
+
|
|
156
|
+
if not annotations:
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
# Execute rules
|
|
160
|
+
findings = execute_rules(
|
|
161
|
+
[file_path], self._rules, project_dir=self._project_dir,
|
|
162
|
+
)
|
|
163
|
+
from attocode_intel.confidence.settings import disabled
|
|
164
|
+
|
|
165
|
+
with disabled():
|
|
166
|
+
findings = run_pipeline(findings, min_confidence=0.0)
|
|
167
|
+
|
|
168
|
+
# Build finding lookup: line -> list of rule_ids
|
|
169
|
+
finding_map: dict[int, list[str]] = {}
|
|
170
|
+
finding_objs: dict[tuple[int, str], EnrichedFinding] = {}
|
|
171
|
+
for f in findings:
|
|
172
|
+
finding_map.setdefault(f.line, []).append(f.rule_id)
|
|
173
|
+
finding_objs[(f.line, f.rule_id)] = f
|
|
174
|
+
|
|
175
|
+
for ann in annotations:
|
|
176
|
+
line_findings = finding_map.get(ann.line, [])
|
|
177
|
+
matched = any(
|
|
178
|
+
_finding_matches_rule(fid, ann.rule_id)
|
|
179
|
+
for fid in line_findings
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
if ann.kind == "expect":
|
|
183
|
+
if matched:
|
|
184
|
+
result.passed.append(ann)
|
|
185
|
+
else:
|
|
186
|
+
result.failed.append((
|
|
187
|
+
ann,
|
|
188
|
+
f"Expected '{ann.rule_id}' on line {ann.line} "
|
|
189
|
+
f"but got {line_findings or 'nothing'}",
|
|
190
|
+
))
|
|
191
|
+
|
|
192
|
+
elif ann.kind == "ok":
|
|
193
|
+
if matched:
|
|
194
|
+
# Find the offending finding for the report
|
|
195
|
+
for fid in line_findings:
|
|
196
|
+
if _finding_matches_rule(fid, ann.rule_id):
|
|
197
|
+
obj = finding_objs.get((ann.line, fid))
|
|
198
|
+
if obj:
|
|
199
|
+
result.false_positives.append((obj, ann))
|
|
200
|
+
break
|
|
201
|
+
else:
|
|
202
|
+
result.passed.append(ann)
|
|
203
|
+
|
|
204
|
+
elif ann.kind == "todoruleid":
|
|
205
|
+
if matched:
|
|
206
|
+
result.todoruleid_unexpected_passes.append(ann)
|
|
207
|
+
# Not matching is expected — not a failure
|
|
208
|
+
|
|
209
|
+
return result
|
|
210
|
+
|
|
211
|
+
def run_test_suite(self, fixtures_dir: str) -> RuleTestSuiteResult:
|
|
212
|
+
"""Run rules against all annotated files in a directory."""
|
|
213
|
+
suite = RuleTestSuiteResult()
|
|
214
|
+
fixtures_path = Path(fixtures_dir)
|
|
215
|
+
if not fixtures_path.is_dir():
|
|
216
|
+
return suite
|
|
217
|
+
|
|
218
|
+
for file_path in sorted(fixtures_path.rglob("*")):
|
|
219
|
+
if not file_path.is_file():
|
|
220
|
+
continue
|
|
221
|
+
if file_path.suffix not in {
|
|
222
|
+
".py", ".go", ".ts", ".js", ".rs", ".java",
|
|
223
|
+
".kt", ".rb", ".php", ".swift", ".cs", ".c",
|
|
224
|
+
".cpp", ".lua", ".sh", ".scala", ".ex",
|
|
225
|
+
}:
|
|
226
|
+
continue
|
|
227
|
+
# Only test files that have annotations
|
|
228
|
+
content = file_path.read_text(encoding="utf-8", errors="replace")
|
|
229
|
+
if not _ANNOTATION_RE.search(content):
|
|
230
|
+
continue
|
|
231
|
+
|
|
232
|
+
result = self.run_test_file(str(file_path))
|
|
233
|
+
suite.file_results.append(result)
|
|
234
|
+
|
|
235
|
+
return suite
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
# ---------------------------------------------------------------------------
|
|
239
|
+
# Inline test_cases validation (YAML rule self-check)
|
|
240
|
+
# ---------------------------------------------------------------------------
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def validate_inline_test_cases(
|
|
244
|
+
rule: UnifiedRule,
|
|
245
|
+
test_cases: list[dict],
|
|
246
|
+
) -> list[str]:
|
|
247
|
+
"""Validate a rule's inline test_cases at load time.
|
|
248
|
+
|
|
249
|
+
Each test case: ``{"code": "...", "should_match": true/false}``.
|
|
250
|
+
Returns list of error messages (empty = all passed).
|
|
251
|
+
"""
|
|
252
|
+
errors: list[str] = []
|
|
253
|
+
if not rule.pattern:
|
|
254
|
+
return errors
|
|
255
|
+
|
|
256
|
+
for i, tc in enumerate(test_cases):
|
|
257
|
+
code = str(tc.get("code", ""))
|
|
258
|
+
should_match = bool(tc.get("should_match", True))
|
|
259
|
+
# Match per-line (consistent with executor behavior)
|
|
260
|
+
matched = any(rule.pattern.search(line) for line in code.splitlines()) if code else False
|
|
261
|
+
|
|
262
|
+
if should_match and not matched:
|
|
263
|
+
errors.append(
|
|
264
|
+
f"Rule '{rule.id}' test_case[{i}]: expected match on "
|
|
265
|
+
f"'{code[:60]}' but pattern did not match"
|
|
266
|
+
)
|
|
267
|
+
elif not should_match and matched:
|
|
268
|
+
errors.append(
|
|
269
|
+
f"Rule '{rule.id}' test_case[{i}]: expected no match on "
|
|
270
|
+
f"'{code[:60]}' but pattern matched"
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
return errors
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
# ---------------------------------------------------------------------------
|
|
277
|
+
# Report formatting
|
|
278
|
+
# ---------------------------------------------------------------------------
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def format_test_report(suite: RuleTestSuiteResult) -> str:
|
|
282
|
+
"""Format a test suite result as markdown."""
|
|
283
|
+
lines: list[str] = []
|
|
284
|
+
|
|
285
|
+
status = "PASS" if suite.ok else "FAIL"
|
|
286
|
+
lines.append(f"# Rule Test Report: {status}\n")
|
|
287
|
+
lines.append(
|
|
288
|
+
f"Files: {len(suite.file_results)} | "
|
|
289
|
+
f"Passed: {suite.total_passed} | "
|
|
290
|
+
f"Failed: {suite.total_failed} | "
|
|
291
|
+
f"False Positives: {suite.total_false_positives} | "
|
|
292
|
+
f"Todo Passes: {suite.total_todoruleid_passes}\n"
|
|
293
|
+
)
|
|
294
|
+
|
|
295
|
+
for fr in suite.file_results:
|
|
296
|
+
if fr.ok and not fr.todoruleid_unexpected_passes:
|
|
297
|
+
continue # Only show interesting results
|
|
298
|
+
|
|
299
|
+
rel = Path(fr.file_path).name
|
|
300
|
+
lines.append(f"## {rel}\n")
|
|
301
|
+
|
|
302
|
+
if fr.failed:
|
|
303
|
+
lines.append("### Missing Expected Findings\n")
|
|
304
|
+
for exp, reason in fr.failed:
|
|
305
|
+
lines.append(f"- Line {exp.line}: `{exp.rule_id}` — {reason}")
|
|
306
|
+
lines.append("")
|
|
307
|
+
|
|
308
|
+
if fr.false_positives:
|
|
309
|
+
lines.append("### False Positives (# ok: violations)\n")
|
|
310
|
+
for finding, ann in fr.false_positives:
|
|
311
|
+
lines.append(
|
|
312
|
+
f"- Line {ann.line}: `{ann.rule_id}` fired but was marked `# ok:` "
|
|
313
|
+
f"({finding.code_snippet[:60]})"
|
|
314
|
+
)
|
|
315
|
+
lines.append("")
|
|
316
|
+
|
|
317
|
+
if fr.todoruleid_unexpected_passes:
|
|
318
|
+
lines.append("### Unexpected Passes (todoruleid now matching!)\n")
|
|
319
|
+
for exp in fr.todoruleid_unexpected_passes:
|
|
320
|
+
lines.append(
|
|
321
|
+
f"- Line {exp.line}: `{exp.rule_id}` — "
|
|
322
|
+
f"previously known-missing, now matching! Consider upgrading to `# expect:`"
|
|
323
|
+
)
|
|
324
|
+
lines.append("")
|
|
325
|
+
|
|
326
|
+
return "\n".join(lines)
|