synapse-hub 2.2.4 → 2.4.1
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.
- package/.agents/agents/failure-analyst.md +11 -0
- package/.agents/agents/live-explorer.md +28 -0
- package/.agents/agents/orchestrator.md +23 -4
- package/.agents/agents/pom-crafter.md +15 -0
- package/.agents/agents/quality-gatekeeper.md +20 -4
- package/.agents/agents/test-runner.md +12 -0
- package/.agents/rules/communication-style.md +40 -0
- package/.agents/rules/context-engineering.md +23 -0
- package/.agents/rules/git-push-protocol.md +10 -16
- package/.agents/rules/graphify.md +14 -14
- package/.agents/rules/mandatory-playwright-live.md +29 -29
- package/.agents/rules/mobile_appium_rules.md +132 -0
- package/.agents/rules/performance_testing_rules.md +74 -0
- package/.agents/rules/systematic-debugging.md +6 -6
- package/.agents/skills/agent-orchestration-multi-agent-optimize/SKILL.md +64 -215
- package/.agents/skills/agent-orchestration-multi-agent-optimize/examples/swarm_workload_config.json +12 -0
- package/.agents/skills/antigravity-test-sentinel/SKILL.md +3 -3
- package/.agents/skills/antigravity-test-sentinel/examples/dod_gate_policy.json +11 -0
- package/.agents/skills/api-network-mocking-interception/SKILL.md +1 -1
- package/.agents/skills/api-network-mocking-interception/examples/network_route_stub.json +9 -0
- package/.agents/skills/api-spec-to-e2e-matrix/SKILL.md +9 -9
- package/.agents/skills/api-spec-to-e2e-matrix/examples/sample_openapi.json +19 -0
- package/.agents/skills/architecture-decision-records/SKILL.md +35 -412
- package/.agents/skills/architecture-decision-records/examples/0001-record-architecture-decisions.md +12 -0
- package/.agents/skills/auth-session-and-cookie-manager/SKILL.md +1 -1
- package/.agents/skills/auth-session-and-cookie-manager/examples/storage_state_sample.json +11 -0
- package/.agents/skills/backend-load-and-stress-patterns/SKILL.md +80 -186
- package/.agents/skills/backend-load-and-stress-patterns/examples/k6_load_test_sample.js +13 -0
- package/.agents/skills/bats-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/bats-testing-patterns/examples/cli_runner_test.bats +6 -0
- package/.agents/skills/bats-testing-patterns/resources/implementation-playbook.md +614 -614
- package/.agents/skills/clean-pom-crafter/SKILL.md +21 -3
- package/.agents/skills/clean-pom-crafter/examples/standard_test_case.groovy +28 -28
- package/.agents/skills/clean-pom-crafter/references/pom_patterns.groovy +138 -138
- package/.agents/skills/code-refactoring-refactor-clean/SKILL.md +2 -2
- package/.agents/skills/code-refactoring-refactor-clean/examples/refactored_page_sample.groovy +9 -0
- package/.agents/skills/db-state-verifier-and-seeder/SKILL.md +5 -5
- package/.agents/skills/db-state-verifier-and-seeder/examples/db_fixture_seed.json +12 -0
- package/.agents/skills/e2e-testing-patterns/SKILL.md +1 -1
- package/.agents/skills/e2e-testing-patterns/examples/playwright_sharded_config.ts +12 -0
- package/.agents/skills/e2e-testing-patterns/resources/implementation-playbook.md +522 -522
- package/.agents/skills/failure-autopsy-and-healing/SKILL.md +17 -3
- package/.agents/skills/failure-autopsy-and-healing/examples/sample_junit_failure.xml +8 -0
- package/.agents/skills/failure-autopsy-and-healing/references/healing_matrix.md +64 -64
- package/.agents/skills/failure-autopsy-and-healing/references/qa_investigation_protocol.md +56 -56
- package/.agents/skills/failure-autopsy-and-healing/scripts/triage_failures.sh +98 -98
- package/.agents/skills/file-and-report-validator/SKILL.md +1 -1
- package/.agents/skills/file-and-report-validator/examples/sample_export_schema.json +14 -0
- package/.agents/skills/flaky-test-sentinel-and-quarantine/SKILL.md +3 -3
- package/.agents/skills/flaky-test-sentinel-and-quarantine/examples/quarantine_registry.json +11 -0
- package/.agents/skills/gherkin-to-suite/SKILL.md +103 -0
- package/.agents/skills/gherkin-to-suite/examples/checkout.feature +39 -0
- package/.agents/skills/gherkin-to-suite/examples/suite_report_sample.json +334 -0
- package/.agents/skills/github-actions-templates/SKILL.md +47 -304
- package/.agents/skills/github-actions-templates/examples/matrix_test_pipeline.yml +8 -0
- package/.agents/skills/graphify-impact-analyzer/SKILL.md +6 -6
- package/.agents/skills/graphify-impact-analyzer/examples/dependency_blast_radius.json +8 -0
- package/.agents/skills/katalon-cli-runner/SKILL.md +2 -2
- package/.agents/skills/katalon-cli-runner/examples/headless_kre_exec.sh +3 -0
- package/.agents/skills/katalon-cli-runner/references/groovy-patterns.md +209 -209
- package/.agents/skills/katalon-cli-runner/references/katalon-api.md +155 -155
- package/.agents/skills/katalon-cli-runner/references/runner_commands.md +67 -67
- package/.agents/skills/lam-script-generator/SKILL.md +42 -1
- package/.agents/skills/lam-script-generator/examples/user_story_sample.md +8 -0
- package/.agents/skills/lean-code-ladder/SKILL.md +83 -0
- package/.agents/skills/lean-code-ladder/examples/ladder_walkthrough.md +37 -0
- package/.agents/skills/meta-prompt-crafter/SKILL.md +107 -0
- package/.agents/skills/meta-prompt-crafter/examples/qa_prompt_template.md +35 -0
- package/.agents/skills/persistent-planning-files/SKILL.md +108 -0
- package/.agents/skills/persistent-planning-files/examples/task_plan_example.md +56 -0
- package/.agents/skills/playwright-live-interaction/SKILL.md +49 -1
- package/.agents/skills/playwright-live-interaction/examples/live_probe_script.js +5 -0
- package/.agents/skills/playwright-live-interaction/references/advanced_superpowers.md +160 -160
- package/.agents/skills/playwright-live-interaction/references/compact_accessibility_snapshots.md +75 -75
- package/.agents/skills/playwright-live-interaction/references/recipes.md +388 -388
- package/.agents/skills/playwright-live-interaction/references/swarm_orchestration.md +134 -134
- package/.agents/skills/playwright-webapp-testing/SKILL.md +93 -0
- package/.agents/skills/playwright-webapp-testing/examples/server_lifecycle_playwright.py +63 -0
- package/.agents/skills/rbt-manual-testing/SKILL.md +150 -0
- package/.agents/skills/rbt-manual-testing/examples/checkout_rbt_manual_suite.md +25 -0
- package/.agents/skills/security-dast-sentinel/SKILL.md +54 -135
- package/.agents/skills/security-dast-sentinel/examples/dast_scan_config.json +10 -0
- package/.agents/skills/self-healing-locator/SKILL.md +24 -10
- package/.agents/skills/self-healing-locator/examples/aom_diff_sample.json +16 -0
- package/.agents/skills/skillspector-security-guard/SKILL.md +32 -90
- package/.agents/skills/skillspector-security-guard/examples/safe_mcp_config.json +12 -0
- package/.agents/skills/surgical-coding-principles/SKILL.md +89 -0
- package/.agents/skills/surgical-coding-principles/examples/surgical_vs_scope_creep.md +46 -0
- package/.agents/skills/synthetic-test-data-generator/SKILL.md +3 -3
- package/.agents/skills/synthetic-test-data-generator/examples/idempotent_data_recipe.json +9 -0
- package/.agents/skills/systematic-debugging/SKILL.md +100 -0
- package/.agents/skills/systematic-debugging/examples/four_phase_case.md +44 -0
- package/.agents/skills/test-driven-development/SKILL.md +114 -0
- package/.agents/skills/test-driven-development/examples/red_green_refactor_qa.md +78 -0
- package/.agents/skills/token-and-context-optimizer/SKILL.md +1 -1
- package/.agents/skills/token-and-context-optimizer/examples/distilled_log_sample.txt +3 -0
- package/.agents/skills/verification-before-completion/SKILL.md +100 -0
- package/.agents/skills/verification-before-completion/examples/evidence_log_example.md +46 -0
- package/.agents/skills/visual-regression-sentinel/SKILL.md +21 -1
- package/.agents/skills/visual-regression-sentinel/examples/visual_mask_rules.json +9 -0
- package/.agents/skills/wcag-audit-patterns/SKILL.md +3 -3
- package/.agents/skills/wcag-audit-patterns/examples/wcag_compliance_checklist.json +10 -0
- package/.agents/skills/wcag-audit-patterns/resources/implementation-playbook.md +541 -541
- package/.agents/skills/web-vitals-and-performance-profiler/SKILL.md +1 -1
- package/.agents/skills/web-vitals-and-performance-profiler/examples/cwv_budget_policy.json +6 -0
- package/.agents/templates/completion_report.md +57 -57
- package/.agents/workflows/analyze_requirement_document.md +18 -0
- package/.agents/workflows/export_testcases_excel.md +32 -0
- package/.agents/workflows/generate_combinatorial_test_data.md +28 -0
- package/.agents/workflows/generate_cross_module_test_plan.md +37 -0
- package/.agents/workflows/generate_manual_testcases_rbt.md +56 -0
- package/.agents/workflows/generate_requirements_from_website.md +25 -0
- package/.agents/workflows/generate_testcases_from_requirements.md +21 -0
- package/.agents/workflows/graphify.md +10 -10
- package/.agents/workflows/verify.md +2 -2
- package/.cursor/rules/language.mdc +7 -7
- package/.cursor/rules/synapse-qa.mdc +23 -0
- package/.cursorrules +7 -0
- package/.pre-commit-hooks.yaml +14 -0
- package/.windsurfrules +7 -0
- package/AGENTS.md +54 -15
- package/CLAUDE.md +35 -12
- package/LICENSE +30 -0
- package/NOTICE +180 -0
- package/README.en.md +331 -0
- package/README.md +252 -515
- package/action.yml +51 -0
- package/benchmarks/qa_eval/README.md +71 -0
- package/benchmarks/qa_eval/build_dataset.py +677 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-06_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-07_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-08_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-09_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/api_contract_drift/qa-challenge-10_api_contract_drift.json +22 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-21_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-22_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-23_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-24_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/fake_pass_tautology/qa-challenge-25_fake_pass_tautology.json +21 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-01_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-02_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-03_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-04_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/locator_drift/qa-challenge-05_locator_drift.json +23 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-16_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-17_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-18_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-19_stale_and_mutation.json +22 -0
- package/benchmarks/qa_eval/dataset/stale_and_mutation/qa-challenge-20_stale_and_mutation.json +23 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-11_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-12_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-13_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-14_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/dataset/timing_and_race/qa-challenge-15_timing_and_race.json +22 -0
- package/benchmarks/qa_eval/eval_runner.py +418 -0
- package/benchmarks/qa_eval/reports/AB_METHODOLOGY_REPORT.md +83 -0
- package/benchmarks/qa_eval/reports/BENCHMARK_REPORT.md +69 -0
- package/benchmarks/qa_eval/reports/eval_results.json +105 -0
- package/bin/agent-qa.js +16 -4
- package/docs/ARCHITECTURE.md +107 -0
- package/docs/CI_CD_INTEGRATION.md +94 -0
- package/docs/CLI_REFERENCE.md +157 -0
- package/docs/CONTRIBUTING.md +91 -0
- package/docs/PLUGIN_DEVELOPMENT_GUIDE.md +126 -0
- package/docs/TOKEN_QUOTA_PLAYBOOK.md +41 -0
- package/docs/adopted-ecosystem-tools.md +87 -0
- package/docs/schemas/synapse.config.schema.json +64 -0
- package/examples/cypress-ecommerce-suite/README.md +45 -0
- package/examples/cypress-ecommerce-suite/cypress/e2e/checkout.cy.js +17 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/BasePage.js +29 -0
- package/examples/cypress-ecommerce-suite/cypress/pages/CartPage.js +51 -0
- package/examples/cypress-ecommerce-suite/cypress.config.js +13 -0
- package/examples/cypress-ecommerce-suite/package.json +13 -0
- package/examples/katalon-suite/Keywords/pages/BasePage.groovy +19 -0
- package/examples/katalon-suite/Keywords/pages/DashboardPage.groovy +29 -0
- package/examples/katalon-suite/Keywords/pages/LoginPage.groovy +45 -0
- package/examples/katalon-suite/Profiles/default.glbl +12 -0
- package/examples/katalon-suite/README.md +18 -0
- package/examples/katalon-suite/Scripts/Auth/TC_LoginSuccess/Script1234567890.groovy +12 -0
- package/examples/katalon-suite/Scripts/Dashboard/TC_ViewSummary/Script1234567891.groovy +15 -0
- package/examples/katalon-suite/katalon_suite.prj +21 -0
- package/examples/playwright-crm-suite/README.md +40 -0
- package/examples/playwright-crm-suite/package.json +14 -0
- package/examples/playwright-crm-suite/pages/BasePage.ts +37 -0
- package/examples/playwright-crm-suite/pages/CustomerPage.ts +57 -0
- package/examples/playwright-crm-suite/pages/LoginPage.ts +32 -0
- package/examples/playwright-crm-suite/playwright.config.ts +27 -0
- package/examples/playwright-crm-suite/tests/auth.spec.ts +22 -0
- package/examples/playwright-crm-suite/tests/customers.spec.ts +33 -0
- package/examples/plugins/sample-custom-plugin/README.md +14 -0
- package/examples/plugins/sample-custom-plugin/plugin.json +16 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/SKILL.md +29 -0
- package/examples/plugins/sample-custom-plugin/skills/custom-security-headers-audit/examples/audit_fixture.json +8 -0
- package/examples/plugins/sample-custom-plugin/tools/custom_security_headers.py +125 -0
- package/examples/practices/README.md +22 -0
- package/examples/practices/requirements/crm/requirements_contracts.md +292 -0
- package/examples/practices/requirements/crm/requirements_customers.md +462 -0
- package/examples/practices/requirements/crm/requirements_login.md +202 -0
- package/examples/practices/requirements/crm/requirements_projects.md +396 -0
- package/examples/practices/requirements/crm/requirements_tasks.md +343 -0
- package/examples/practices/requirements/ecommerce/requirements_category_brand.txt +402 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/2026-04-03_04-28-27.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-1/KAN-1_requirement.md +52 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-19.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/2026-04-03_04-28-57.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-4/KAN-4_requirement.md +119 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-5_overview.md +9 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-08.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/2026-04-03_05-58-23.png +0 -0
- package/examples/practices/requirements/jira/KAN-5/KAN-6/KAN-6_requirement.md +32 -0
- package/examples/practices/testcases/api/book_management_api_test_cases.md +144 -0
- package/examples/practices/testcases/api/user_management_api_test_cases.md +139 -0
- package/examples/practices/testcases/crm/test_cases_crm_login.md +148 -0
- package/examples/practices/testcases/crm/test_cases_customers.md +156 -0
- package/examples/pytest-api-suite/README.md +45 -0
- package/examples/pytest-api-suite/conftest.py +31 -0
- package/examples/pytest-api-suite/pages/user_api_client.py +36 -0
- package/examples/pytest-api-suite/requirements.txt +2 -0
- package/examples/pytest-api-suite/tests/test_user_api.py +44 -0
- package/examples/selenium-suite/README.md +18 -0
- package/examples/selenium-suite/pages/base_page.py +27 -0
- package/examples/selenium-suite/pages/login_page.py +29 -0
- package/examples/selenium-suite/pages/order_page.py +25 -0
- package/examples/selenium-suite/pytest.ini +6 -0
- package/examples/selenium-suite/requirements.txt +2 -0
- package/examples/selenium-suite/tests/test_login.py +10 -0
- package/examples/selenium-suite/tests/test_order.py +10 -0
- package/package.json +27 -12
- package/scripts/generate_skill_examples.py +244 -0
- package/scripts/install.ps1 +112 -0
- package/scripts/install_global_plugin.ps1 +94 -0
- package/scripts/install_global_plugin.sh +49 -0
- package/scripts/lint_skills.ps1 +57 -0
- package/scripts/package_dist.py +323 -0
- package/scripts/setup.sh +59 -0
- package/scripts/update_hub.ps1 +19 -0
- package/scripts/update_hub.sh +15 -0
- package/skills_manifest.json +209 -6
- package/synapse.config.json +30 -0
- package/templates/PROJECT_AI_GUIDE_TEMPLATE.md +7 -7
- package/templates/PROJECT_STATE_TEMPLATE.md +1 -1
- package/templates/ai_bridges/CLAUDE.md +33 -16
- package/templates/ai_bridges/copilot-instructions.md +14 -21
- package/templates/ai_bridges/cursor_synapse_qa.mdc +17 -20
- package/templates/ai_bridges/windsurfrules +21 -21
- package/templates/ci/github-actions-synapse.yml +71 -4
- package/templates/welcome.html.tmpl +1674 -0
- package/tools/.infer_baseline.json +1102 -0
- package/tools/TOOLS_RUNBOOK.md +98 -131
- package/tools/__init__.py +26 -0
- package/tools/agent_canary.py +501 -501
- package/tools/analyze_project.py +31 -17
- package/tools/api_fuzzer.py +278 -6
- package/tools/author_integrity.py +6 -6
- package/tools/auto_heal.py +1 -1
- package/tools/browser_helpers.js +105 -7
- package/tools/bug_reporter.py +31 -1
- package/tools/bump_version.py +30 -3
- package/tools/cache_resolver.py +248 -14
- package/tools/check_env.py +55 -14
- package/tools/check_version_sync.py +171 -0
- package/tools/ci_healing_hook.py +1 -1
- package/tools/cli.py +454 -42
- package/tools/count_tokens.py +1257 -0
- package/tools/dashboard_generator.py +2638 -0
- package/tools/db_sandbox.py +198 -20
- package/tools/e2e_flow_prober.ps1 +12 -64
- package/tools/e2e_flow_prober.py +99 -0
- package/tools/e2e_flow_prober.sh +4 -0
- package/tools/flaky_detector.py +4 -3
- package/tools/generate_allure_report.ps1 +12 -29
- package/tools/generate_allure_report.py +77 -0
- package/tools/generate_allure_report.sh +4 -0
- package/tools/generate_jmx_plan.py +257 -58
- package/tools/generate_test_reports.ps1 +12 -134
- package/tools/generate_test_reports.py +133 -0
- package/tools/generate_test_reports.sh +4 -0
- package/tools/gherkin_suite.py +758 -0
- package/tools/git_pre_commit_guard.py +12 -6
- package/tools/goldfish_review.py +366 -0
- package/tools/heal_pr_automator.py +1 -1
- package/tools/heuristic_locator_matcher.py +170 -21
- package/tools/infer_linter.py +612 -70
- package/tools/lint_ai_docs.py +64 -0
- package/tools/matrix_sharder.py +1 -1
- package/tools/mcp_excel_server.ps1 +12 -250
- package/tools/mcp_excel_server.py +249 -0
- package/tools/mcp_excel_server.sh +4 -0
- package/tools/mcp_server.py +368 -144
- package/tools/md_to_xlsx.py +307 -0
- package/tools/micro_mutator.py +302 -69
- package/tools/omni_transpiler.py +2 -2
- package/tools/output_formatter.py +128 -0
- package/tools/pinpoint_failure.py +88 -88
- package/tools/project_state.py +2 -2
- package/tools/repo_map.py +1 -1
- package/tools/safety_firewall.py +196 -45
- package/tools/setup_project.py +295 -31
- package/tools/skillspector.py +31 -0
- package/tools/spec_drift_sentinel.py +439 -0
- package/tools/sync_agent_rules.py +248 -0
- package/tools/telemetry_correlator.py +45 -0
- package/tools/test_impact.py +51 -12
- package/tools/tms_sync_bridge.py +21 -5
- package/tools/universal_ir.py +3 -0
- package/tools/update_sentinel.py +87 -15
- package/tools/verify_all.ps1 +13 -0
- package/tools/verify_all.py +479 -85
- package/tools/visual_diff_canvas.py +1 -0
- package/tools/visual_som_helper.py +1 -1
- package/tools/watch_mode.py +305 -0
- package/tools/welcome_page.py +291 -0
package/tools/infer_linter.py
CHANGED
|
@@ -16,6 +16,7 @@ import sys
|
|
|
16
16
|
import re
|
|
17
17
|
import json
|
|
18
18
|
import glob
|
|
19
|
+
import html as html_lib
|
|
19
20
|
import argparse
|
|
20
21
|
from datetime import datetime
|
|
21
22
|
|
|
@@ -41,20 +42,96 @@ CYAN = "\033[96m"
|
|
|
41
42
|
BOLD = "\033[1m"
|
|
42
43
|
RESET = "\033[0m"
|
|
43
44
|
|
|
44
|
-
#
|
|
45
|
+
# Optional Tree-Sitter Integration for compiler-grade AST parsing across TS/Java/C#
|
|
46
|
+
try:
|
|
47
|
+
import tree_sitter # type: ignore
|
|
48
|
+
HAS_TREE_SITTER = True
|
|
49
|
+
except ImportError:
|
|
50
|
+
HAS_TREE_SITTER = False
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def parse_with_tree_sitter(file_path, content):
|
|
54
|
+
"""
|
|
55
|
+
Optional compiler-grade AST parsing via tree-sitter when installed.
|
|
56
|
+
Extracts syntactic AST nodes for TypeScript, JavaScript, Java, and C#.
|
|
57
|
+
Returns None if tree-sitter is unavailable or grammar is not installed.
|
|
58
|
+
"""
|
|
59
|
+
if not HAS_TREE_SITTER:
|
|
60
|
+
return None
|
|
61
|
+
try:
|
|
62
|
+
from tree_sitter_languages import get_parser # type: ignore
|
|
63
|
+
ext = os.path.splitext(file_path)[1].lower()
|
|
64
|
+
lang_map = {
|
|
65
|
+
".ts": "typescript",
|
|
66
|
+
".js": "javascript",
|
|
67
|
+
".java": "java",
|
|
68
|
+
".cs": "c_sharp",
|
|
69
|
+
}
|
|
70
|
+
lang_name = lang_map.get(ext)
|
|
71
|
+
if not lang_name:
|
|
72
|
+
return None
|
|
73
|
+
parser = get_parser(lang_name)
|
|
74
|
+
return parser.parse(bytes(content, "utf-8"))
|
|
75
|
+
except Exception:
|
|
76
|
+
return None
|
|
77
|
+
|
|
78
|
+
# Generic Detection regexes (including Cypress .should and .and assertions)
|
|
45
79
|
ASSERTION_PATTERN = re.compile(
|
|
46
|
-
r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b'
|
|
80
|
+
r'\b(verify[A-Za-z0-9_]*|assert[A-Za-z0-9_]*|waitForToast|checkStatus|assertEquals|assertTrue|assertFalse|assertThat|expect|ensure[A-Z][A-Za-z0-9_]*Exists)\b|\.should\s*\(|\.and\s*\('
|
|
47
81
|
)
|
|
48
82
|
|
|
83
|
+
# Rule L1 & L5: Empty catch / swallowed exceptions across JS/TS/Groovy/Java and Python
|
|
84
|
+
# Also catches try/catch that only console.log or promise .catch(() => {})
|
|
49
85
|
EMPTY_CATCH_PATTERN = re.compile(
|
|
50
|
-
r'catch\s
|
|
86
|
+
r'(?:catch\s*(?:\([^\)]*\))?\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}'
|
|
87
|
+
r'|\.catch\s*\(\s*(?:\([^)]*\)|[a-zA-Z0-9_]+)?\s*=>\s*\{\s*(?:console\.(?:log|warn|error)\([^)]*\);?\s*)?\}\s*\)'
|
|
88
|
+
r'|\bexcept(?:\s+[^:]*)?:\s*(?:pass|\.\.\.)(?:\s*$|\s*#))',
|
|
89
|
+
re.MULTILINE
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Whitelisted best-effort compatibility calls (UTF-8 console reconfigure, file
|
|
93
|
+
# permission chmod, stream flush). An empty catch guarding ONLY these is an
|
|
94
|
+
# intentional no-op-on-failure idiom, not a swallowed test failure.
|
|
95
|
+
BENIGN_TRY_CALL_PATTERN = re.compile(
|
|
96
|
+
r'^[\w\.]*\.(?:reconfigure|chmod|flush)\s*\('
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Rule L3: Tautological assertions (e.g. expect(true).toBe(true), assert r == r, expect(true).toBeTruthy())
|
|
100
|
+
TAUTOLOGY_PATTERN = re.compile(
|
|
101
|
+
r'(?:expect\s*\(\s*(?:true|1)\s*\)\s*\.\s*(?:toBe|toEqual|toBeTruthy)\s*\(\s*(?:true|1)?\s*\)'
|
|
102
|
+
r'|\bassert\s+(?:True|true|1\s*==\s*1)\b'
|
|
103
|
+
r'|\bassert\s+([a-zA-Z0-9_]+)\s*==\s*\1\b'
|
|
104
|
+
r'|assertEquals\s*\(\s*(?:true|1)\s*,\s*(?:true|1)\s*\)'
|
|
105
|
+
r'|assertTrue\s*\(\s*(?:true|1)\s*\))',
|
|
106
|
+
re.IGNORECASE
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
# Rule L4: Unawaited Playwright assertions in JS/TS
|
|
110
|
+
# Expanded with all standard Playwright async matchers and optional .not modifier
|
|
111
|
+
PLAYWRIGHT_ASYNC_MATCHERS = (
|
|
112
|
+
r'(?:toBeVisible|toBeHidden|toHaveText|toHaveValue|toHaveCount|toBeEnabled|toBeDisabled|'
|
|
113
|
+
r'toBeChecked|toHaveURL|toContainText|toHaveTitle|toHaveAttribute|toHaveClass|toHaveId|'
|
|
114
|
+
r'toBeFocused|toBeEmpty|toBeAttached|toBeInViewport|toBeOK)'
|
|
115
|
+
)
|
|
116
|
+
UNAWAITED_EXPECT_PATTERN = re.compile(
|
|
117
|
+
r'\bexpect\s*\([^;\n]+\)\s*(?:\.\s*not)?\s*\.\s*' + PLAYWRIGHT_ASYNC_MATCHERS + r'\s*\(',
|
|
51
118
|
re.MULTILINE
|
|
52
119
|
)
|
|
53
120
|
|
|
54
|
-
|
|
55
|
-
|
|
121
|
+
# Rule 3: Flaky Static Delays across Groovy, Java, Playwright (JS/TS/Python), Pytest/Python, Cypress
|
|
122
|
+
# Millisecond-based delays: Thread.sleep(ms), waitForTimeout(ms), wait_for_timeout(ms), cy.wait(ms)
|
|
123
|
+
MS_DELAY_PATTERN = re.compile(
|
|
124
|
+
r'(?:Thread\.sleep|(?:page\.)?waitForTimeout|(?:page\.)?wait_for_timeout|cy\.wait)\s*\(\s*([^)]+)\s*\)'
|
|
56
125
|
)
|
|
57
126
|
|
|
127
|
+
# Second-based delays: WebUI.delay(sec), time.sleep(sec)
|
|
128
|
+
SEC_DELAY_PATTERN = re.compile(
|
|
129
|
+
r'(?:WebUI\.delay|time\.sleep)\s*\(\s*([^)]+)\s*\)'
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
# Backward-compatibility alias
|
|
133
|
+
THREAD_SLEEP_PATTERN = MS_DELAY_PATTERN
|
|
134
|
+
|
|
58
135
|
|
|
59
136
|
def strip_comments_and_strings(source_code, preserve_newlines=True):
|
|
60
137
|
"""
|
|
@@ -63,12 +140,13 @@ def strip_comments_and_strings(source_code, preserve_newlines=True):
|
|
|
63
140
|
Prevents false-positive / false-negative matches in AST linter.
|
|
64
141
|
"""
|
|
65
142
|
pattern = re.compile(
|
|
66
|
-
r'(?P<BLOCK_COMMENT>/\*.*?\*/)|'
|
|
67
|
-
r'(?P<LINE_COMMENT>//[^\r\n]*)|'
|
|
68
143
|
r'(?P<TRIPLE_DOUBLE>"""[\s\S]*?""")|'
|
|
69
144
|
r'(?P<TRIPLE_SINGLE>\'\'\'[\s\S]*?\'\'\')|'
|
|
70
145
|
r'(?P<DOUBLE_STRING>"(?:\\.|[^"\\])*")|'
|
|
71
|
-
r'(?P<SINGLE_STRING>\'(?:\\.|[^\'\\])*\')'
|
|
146
|
+
r'(?P<SINGLE_STRING>\'(?:\\.|[^\'\\])*\')|'
|
|
147
|
+
r'(?P<BLOCK_COMMENT>/\*.*?\*/)|'
|
|
148
|
+
r'(?P<LINE_COMMENT>//[^\r\n]*)|'
|
|
149
|
+
r'(?P<HASH_COMMENT>#[^\r\n]*)',
|
|
72
150
|
re.DOTALL
|
|
73
151
|
)
|
|
74
152
|
|
|
@@ -81,37 +159,227 @@ def strip_comments_and_strings(source_code, preserve_newlines=True):
|
|
|
81
159
|
return pattern.sub(replacer, source_code)
|
|
82
160
|
|
|
83
161
|
|
|
84
|
-
def
|
|
85
|
-
"""
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
162
|
+
def _is_benign_empty_catch(clean_code, match_start):
|
|
163
|
+
"""
|
|
164
|
+
True when an empty catch block exclusively guards whitelisted best-effort
|
|
165
|
+
calls (e.g. sys.stdout.reconfigure(), os.chmod()) - an intentional
|
|
166
|
+
no-op-on-failure compatibility idiom rather than a swallowed test failure.
|
|
167
|
+
Conservative by design: only Python-style 'try:' blocks are whitelisted;
|
|
168
|
+
C-style brace bodies still get flagged.
|
|
169
|
+
"""
|
|
170
|
+
body_lines = []
|
|
171
|
+
for line in reversed(clean_code[:match_start].splitlines()):
|
|
172
|
+
stripped = line.strip()
|
|
173
|
+
if not stripped:
|
|
174
|
+
continue
|
|
175
|
+
if re.match(r'try\s*(?:[:{]|$)', stripped):
|
|
176
|
+
return bool(body_lines) and all(BENIGN_TRY_CALL_PATTERN.match(l) for l in body_lines)
|
|
177
|
+
body_lines.append(stripped)
|
|
178
|
+
return False
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def load_config(repo_root=None, config_path=None):
|
|
182
|
+
"""Loads configuration from synapse.config.json if present."""
|
|
183
|
+
if config_path and os.path.exists(config_path):
|
|
184
|
+
try:
|
|
185
|
+
with open(config_path, "r", encoding="utf-8") as f:
|
|
186
|
+
return json.load(f)
|
|
187
|
+
except (OSError, json.JSONDecodeError):
|
|
188
|
+
pass
|
|
189
|
+
|
|
190
|
+
root = repo_root or REPO_ROOT
|
|
191
|
+
candidates = [
|
|
192
|
+
os.path.join(root, "synapse.config.json"),
|
|
193
|
+
os.path.join(os.getcwd(), "synapse.config.json")
|
|
194
|
+
]
|
|
195
|
+
for c in candidates:
|
|
196
|
+
if os.path.exists(c):
|
|
197
|
+
try:
|
|
198
|
+
with open(c, "r", encoding="utf-8") as f:
|
|
199
|
+
return json.load(f)
|
|
200
|
+
except (OSError, json.JSONDecodeError):
|
|
201
|
+
pass
|
|
202
|
+
return {}
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def load_baseline(baseline_path):
|
|
206
|
+
"""Loads existing baseline findings to suppress known technical debt."""
|
|
207
|
+
if not baseline_path or not os.path.exists(baseline_path):
|
|
208
|
+
return set()
|
|
209
|
+
try:
|
|
210
|
+
with open(baseline_path, "r", encoding="utf-8") as f:
|
|
211
|
+
data = json.load(f)
|
|
212
|
+
items = data.get("defects", data) if isinstance(data, dict) else data
|
|
213
|
+
signatures = set()
|
|
214
|
+
for it in items:
|
|
215
|
+
f_path = it.get("file", "").replace("\\", "/")
|
|
216
|
+
d_type = it.get("type", "").upper()
|
|
217
|
+
d_line = it.get("line")
|
|
218
|
+
signatures.add((f_path, d_type))
|
|
219
|
+
if d_line is not None:
|
|
220
|
+
signatures.add((f_path, d_type, d_line))
|
|
221
|
+
return signatures
|
|
222
|
+
except (OSError, json.JSONDecodeError):
|
|
223
|
+
return set()
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def save_baseline(baseline_path, all_defects):
|
|
227
|
+
"""Saves current findings into a baseline JSON file."""
|
|
228
|
+
os.makedirs(os.path.dirname(os.path.abspath(baseline_path)), exist_ok=True)
|
|
229
|
+
with open(baseline_path, "w", encoding="utf-8") as f:
|
|
230
|
+
json.dump({
|
|
231
|
+
"generated_at": datetime.now().isoformat(),
|
|
232
|
+
"total_defects": len(all_defects),
|
|
233
|
+
"defects": all_defects
|
|
234
|
+
}, f, indent=2, ensure_ascii=False)
|
|
235
|
+
|
|
236
|
+
|
|
237
|
+
def format_sarif(report_data):
|
|
238
|
+
"""Formats findings in standard SARIF 2.1.0 JSON format for GitHub Code Scanning."""
|
|
239
|
+
results = []
|
|
240
|
+
rules_dict = {}
|
|
241
|
+
for d in report_data.get("all_defects", []):
|
|
242
|
+
rule_id = d.get("type", "STATIC_DEFECT")
|
|
243
|
+
if rule_id not in rules_dict:
|
|
244
|
+
rules_dict[rule_id] = {
|
|
245
|
+
"id": rule_id,
|
|
246
|
+
"shortDescription": {"text": d.get("message", rule_id)},
|
|
247
|
+
"defaultConfiguration": {
|
|
248
|
+
"level": "error" if d.get("severity") == "CRITICAL" else "warning"
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
results.append({
|
|
252
|
+
"ruleId": rule_id,
|
|
253
|
+
"level": "error" if d.get("severity") == "CRITICAL" else "warning",
|
|
254
|
+
"message": {"text": d.get("message", "")},
|
|
255
|
+
"locations": [{
|
|
256
|
+
"physicalLocation": {
|
|
257
|
+
"artifactLocation": {"uri": d.get("file", "")},
|
|
258
|
+
"region": {"startLine": max(1, d.get("line", 1))}
|
|
259
|
+
}
|
|
260
|
+
}]
|
|
261
|
+
})
|
|
262
|
+
sarif_doc = {
|
|
263
|
+
"$schema": "https://json.schemastore.org/sarif-2.1.0.json",
|
|
264
|
+
"version": "2.1.0",
|
|
265
|
+
"runs": [{
|
|
266
|
+
"tool": {
|
|
267
|
+
"driver": {
|
|
268
|
+
"name": "synapse-infer-linter",
|
|
269
|
+
"version": "2.3.0",
|
|
270
|
+
"rules": list(rules_dict.values())
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
"results": results
|
|
274
|
+
}]
|
|
275
|
+
}
|
|
276
|
+
return json.dumps(sarif_doc, indent=2, ensure_ascii=False)
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def format_junit(report_data):
|
|
280
|
+
"""Formats findings in standard JUnit XML format."""
|
|
281
|
+
total = max(1, report_data.get("total_scripts", 0))
|
|
282
|
+
defects = report_data.get("all_defects", [])
|
|
283
|
+
failures = len([d for d in defects if d.get("severity") == "CRITICAL"])
|
|
284
|
+
warnings = len([d for d in defects if d.get("severity") != "CRITICAL"])
|
|
285
|
+
lines = [
|
|
286
|
+
'<?xml version="1.0" encoding="utf-8"?>',
|
|
287
|
+
f'<testsuite name="synapse-infer-linter" tests="{max(total, len(defects))}" failures="{failures}" errors="{warnings}">'
|
|
288
|
+
]
|
|
289
|
+
for d in defects:
|
|
290
|
+
cname = d.get("file", "").replace("/", ".").replace("\\", ".")
|
|
291
|
+
tname = f"{d.get('type')}_L{d.get('line', 1)}"
|
|
292
|
+
msg = html_lib.escape(d.get("message", ""))
|
|
293
|
+
tag = "failure" if d.get("severity") == "CRITICAL" else "error"
|
|
294
|
+
lines.append(f' <testcase classname="{cname}" name="{tname}">')
|
|
295
|
+
lines.append(f' <{tag} message="{msg}" type="{d.get("type")}">Line {d.get("line", 1)}: {msg}</{tag}>')
|
|
296
|
+
lines.append(' </testcase>')
|
|
297
|
+
if not defects:
|
|
298
|
+
lines.append(' <testcase classname="synapse.linter" name="clean_run"/>')
|
|
299
|
+
lines.append('</testsuite>')
|
|
300
|
+
return "\n".join(lines)
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
def classify_file_role(file_path: str) -> str:
|
|
304
|
+
"""
|
|
305
|
+
Classifies a source file into:
|
|
306
|
+
- 'CONFIG': Configuration files (playwright.config, cypress.config, conftest, wdio.conf, jest.config)
|
|
307
|
+
- 'PAGE_OBJECT': Page Classes / POM (files in pages/, ending in Page.ts/js/py/groovy/java, or BasePage)
|
|
308
|
+
- 'HELPER': Utilities / Fixtures / Custom Keywords (files in helpers/, fixtures/, utils/, support/)
|
|
309
|
+
- 'TEST_SCRIPT': Test specifications / test cases (test_*, *test, *.spec, *.cy, *.tc, files in tests/, Scripts/)
|
|
310
|
+
"""
|
|
311
|
+
norm = file_path.replace("\\", "/").lower()
|
|
312
|
+
base = os.path.basename(norm)
|
|
313
|
+
parts = norm.split("/")[:-1]
|
|
314
|
+
|
|
315
|
+
# 1. Config files
|
|
316
|
+
if any(cfg in base for cfg in ["playwright.config", "cypress.config", "wdio.conf", "jest.config", "karma.conf", "conftest.py", "tsconfig"]):
|
|
317
|
+
return "CONFIG"
|
|
318
|
+
if any(p in ("config", ".config") for p in parts):
|
|
319
|
+
return "CONFIG"
|
|
320
|
+
|
|
321
|
+
# 2. Page Objects (Clean POM)
|
|
322
|
+
if any(p in ("pages", "page_objects", "page-objects", "po") for p in parts) or \
|
|
323
|
+
("keywords" in parts and "pages" in norm):
|
|
324
|
+
return "PAGE_OBJECT"
|
|
325
|
+
if re.search(r'(^|_)basepage\b', base) or re.search(r'[a-z0-9_]page\.(ts|js|py|groovy|java)$', base):
|
|
326
|
+
return "PAGE_OBJECT"
|
|
327
|
+
|
|
328
|
+
# 3. Helpers & Fixtures / Tools & Utilities
|
|
329
|
+
if any(p in ("fixtures", "helpers", "utils", "support", "tools", "bin") for p in parts):
|
|
330
|
+
return "HELPER"
|
|
331
|
+
if any(base.endswith(sfx) for sfx in ("helper.ts", "helper.js", "helper.py", "helper.groovy", "utils.ts", "utils.js", "utils.py", "fixture.ts", "fixture.js")):
|
|
332
|
+
return "HELPER"
|
|
333
|
+
|
|
334
|
+
# 4. Test Scripts
|
|
335
|
+
test_suffixes = ("_test.py", ".spec.ts", ".spec.js", ".test.ts", ".test.js", ".cy.js", ".cy.ts", ".spec.jsx", ".test.jsx", ".tc", "test.java", "tests.java", "spec.groovy")
|
|
336
|
+
if base.startswith("test_") or base.endswith(test_suffixes):
|
|
337
|
+
return "TEST_SCRIPT"
|
|
338
|
+
if any(p in ("tests", "test", "e2e", "specs", "cypress") for p in parts):
|
|
339
|
+
return "TEST_SCRIPT"
|
|
340
|
+
if "scripts" in parts:
|
|
341
|
+
if file_path.endswith((".groovy", ".tc")) or base.startswith("test_") or base.endswith(test_suffixes):
|
|
342
|
+
return "TEST_SCRIPT"
|
|
343
|
+
return "HELPER"
|
|
344
|
+
|
|
345
|
+
# Default fallback: standalone script without directory context
|
|
346
|
+
return "TEST_SCRIPT"
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def analyze_script(file_path, baseline_set=None, disabled_rules=None):
|
|
350
|
+
"""Performs static rule scanning on a single test script or automation asset."""
|
|
351
|
+
role = classify_file_role(file_path)
|
|
89
352
|
|
|
90
|
-
defects = []
|
|
91
353
|
try:
|
|
92
354
|
rel_path = os.path.relpath(file_path, REPO_ROOT)
|
|
93
355
|
except ValueError:
|
|
94
356
|
rel_path = os.path.abspath(file_path)
|
|
95
357
|
|
|
358
|
+
# Config files have zero test assertions by definition
|
|
359
|
+
if role == "CONFIG":
|
|
360
|
+
return {
|
|
361
|
+
"file": rel_path,
|
|
362
|
+
"role": role,
|
|
363
|
+
"defects": [],
|
|
364
|
+
"is_clean": True
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
with open(file_path, "r", encoding="utf-8", errors="ignore") as f:
|
|
368
|
+
content = f.read()
|
|
369
|
+
|
|
370
|
+
defects = []
|
|
371
|
+
|
|
96
372
|
# Lexically strip comments and string literals for accurate analysis
|
|
97
373
|
clean_code = strip_comments_and_strings(content, preserve_newlines=True)
|
|
98
374
|
clean_lines = [l.strip() for l in clean_code.splitlines()]
|
|
99
375
|
|
|
100
|
-
|
|
101
|
-
executable_lines = [
|
|
102
|
-
l for l in clean_lines
|
|
103
|
-
if l and not l.startswith("import ") and not l.startswith("package ")
|
|
104
|
-
]
|
|
105
|
-
if len(executable_lines) <= 2:
|
|
106
|
-
defects.append({
|
|
107
|
-
"type": "EMPTY_TEST_SCRIPT",
|
|
108
|
-
"severity": "CRITICAL",
|
|
109
|
-
"line": 1,
|
|
110
|
-
"message": "Test script appears empty or contains only comments/imports."
|
|
111
|
-
})
|
|
376
|
+
raw_lines = content.splitlines()
|
|
112
377
|
|
|
378
|
+
# Code quality checks across all automation assets (Test Scripts, Page Objects, Helpers)
|
|
113
379
|
# Rule 2: Swallowed Exceptions (empty catch blocks)
|
|
114
380
|
for match in EMPTY_CATCH_PATTERN.finditer(clean_code):
|
|
381
|
+
if _is_benign_empty_catch(clean_code, match.start()):
|
|
382
|
+
continue
|
|
115
383
|
line_no = clean_code[:match.start()].count("\n") + 1
|
|
116
384
|
defects.append({
|
|
117
385
|
"type": "SWALLOWED_EXCEPTION",
|
|
@@ -120,42 +388,220 @@ def analyze_script(file_path):
|
|
|
120
388
|
"message": "Empty catch block detected. Swallowed exceptions cause false-pass test runs."
|
|
121
389
|
})
|
|
122
390
|
|
|
123
|
-
# Rule 3: Flaky Static Delays
|
|
391
|
+
# Rule 3: Flaky Static Delays (Playwright, Cypress, Pytest, Katalon, Java/Groovy)
|
|
124
392
|
for idx, line in enumerate(clean_code.splitlines(), 1):
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
393
|
+
raw_line = raw_lines[idx - 1].strip() if idx <= len(raw_lines) else line.strip()
|
|
394
|
+
|
|
395
|
+
# Millisecond delays
|
|
396
|
+
m_ms = MS_DELAY_PATTERN.search(line)
|
|
397
|
+
if m_ms:
|
|
398
|
+
arg = m_ms.group(1).strip()
|
|
399
|
+
# F08: Ignore Cypress route alias waits like cy.wait('@getUsers')
|
|
400
|
+
if arg.startswith(("@", "'@", '"@')):
|
|
401
|
+
pass
|
|
402
|
+
elif arg.isdigit():
|
|
403
|
+
delay_val = int(arg)
|
|
404
|
+
if delay_val > 0:
|
|
405
|
+
d_type = "EXCESSIVE_HARD_DELAY" if delay_val >= 3000 else "HARD_DELAY_ANTI_PATTERN"
|
|
406
|
+
defects.append({
|
|
407
|
+
"type": d_type,
|
|
408
|
+
"severity": "WARNING",
|
|
409
|
+
"line": idx,
|
|
410
|
+
"message": f"Static sleep of {delay_val}ms found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
411
|
+
})
|
|
412
|
+
continue
|
|
413
|
+
elif re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg) and not line.strip().startswith("cy.wait"):
|
|
129
414
|
defects.append({
|
|
130
|
-
"type": "
|
|
415
|
+
"type": "HARD_DELAY_ANTI_PATTERN",
|
|
131
416
|
"severity": "WARNING",
|
|
132
417
|
"line": idx,
|
|
133
|
-
"message": f"Static sleep
|
|
418
|
+
"message": f"Static sleep with variable/expression '{arg}' found ('{raw_line}'). Replace with dynamic Smart Wait."
|
|
419
|
+
})
|
|
420
|
+
continue
|
|
421
|
+
|
|
422
|
+
# Second delays
|
|
423
|
+
m_sec = SEC_DELAY_PATTERN.search(line)
|
|
424
|
+
if m_sec:
|
|
425
|
+
arg = m_sec.group(1).strip()
|
|
426
|
+
try:
|
|
427
|
+
delay_val = float(arg)
|
|
428
|
+
if delay_val > 0:
|
|
429
|
+
delay_ms = int(delay_val * 1000)
|
|
430
|
+
delay_str = f"{delay_val:g}"
|
|
431
|
+
d_type = "EXCESSIVE_HARD_DELAY" if delay_ms >= 3000 else "HARD_DELAY_ANTI_PATTERN"
|
|
432
|
+
defects.append({
|
|
433
|
+
"type": d_type,
|
|
434
|
+
"severity": "WARNING",
|
|
435
|
+
"line": idx,
|
|
436
|
+
"message": f"Static sleep of {delay_str}s ({delay_ms}ms) found ('{raw_line}'). Replace with dynamic Smart Wait (waitForElement/networkidle)."
|
|
437
|
+
})
|
|
438
|
+
except ValueError:
|
|
439
|
+
if re.match(r'^[a-zA-Z_][a-zA-Z0-9_]*$', arg):
|
|
440
|
+
defects.append({
|
|
441
|
+
"type": "HARD_DELAY_ANTI_PATTERN",
|
|
442
|
+
"severity": "WARNING",
|
|
443
|
+
"line": idx,
|
|
444
|
+
"message": f"Static sleep with variable/expression '{arg}' found ('{raw_line}'). Replace with dynamic Smart Wait."
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
# Test Script Specific Invariants (Do NOT apply to Page Objects or Helpers)
|
|
448
|
+
if role == "TEST_SCRIPT":
|
|
449
|
+
executable_lines = [
|
|
450
|
+
l for l in clean_lines
|
|
451
|
+
if l and not l.startswith("import ") and not l.startswith("package ") and not l.startswith("from ")
|
|
452
|
+
]
|
|
453
|
+
# Rule 1: Empty Test Script
|
|
454
|
+
if len(executable_lines) == 0:
|
|
455
|
+
defects.append({
|
|
456
|
+
"type": "EMPTY_TEST_SCRIPT",
|
|
457
|
+
"severity": "CRITICAL",
|
|
458
|
+
"line": 1,
|
|
459
|
+
"message": "Test script is empty or contains only comments/imports."
|
|
460
|
+
})
|
|
461
|
+
|
|
462
|
+
# Rule 4 (L3): Tautological Assertions
|
|
463
|
+
for idx, line in enumerate(clean_code.splitlines(), 1):
|
|
464
|
+
if TAUTOLOGY_PATTERN.search(line):
|
|
465
|
+
raw_line = raw_lines[idx - 1].strip() if idx <= len(raw_lines) else line.strip()
|
|
466
|
+
defects.append({
|
|
467
|
+
"type": "TAUTOLOGICAL_ASSERTION",
|
|
468
|
+
"severity": "CRITICAL",
|
|
469
|
+
"line": idx,
|
|
470
|
+
"message": f"Tautological assertion detected ('{raw_line}'). Test assertion always evaluates to True without testing system state."
|
|
134
471
|
})
|
|
135
472
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
473
|
+
# Rule 5 (L4): Unawaited Playwright Assertions in JS/TS
|
|
474
|
+
if file_path.endswith((".ts", ".tsx", ".js", ".jsx")):
|
|
475
|
+
for match in UNAWAITED_EXPECT_PATTERN.finditer(clean_code):
|
|
476
|
+
prefix = clean_code[max(0, match.start() - 40):match.start()]
|
|
477
|
+
if re.search(r'\b(?:await|return)\s+$', prefix):
|
|
478
|
+
continue
|
|
479
|
+
surrounding = clean_code[max(0, match.start() - 150):match.start()]
|
|
480
|
+
if any(w in surrounding for w in ("Promise.all", "Promise.allSettled", "return ")):
|
|
481
|
+
continue
|
|
482
|
+
line_no = clean_code[:match.start()].count("\n") + 1
|
|
483
|
+
line_text = raw_lines[line_no - 1].strip() if line_no <= len(raw_lines) else ""
|
|
484
|
+
defects.append({
|
|
485
|
+
"type": "UNAWAITED_ASYNC_ASSERTION",
|
|
486
|
+
"severity": "CRITICAL",
|
|
487
|
+
"line": line_no,
|
|
488
|
+
"message": f"Unawaited async Playwright assertion detected: '{line_text}'. Without 'await', assertion returns a Pending Promise that never validates."
|
|
489
|
+
})
|
|
490
|
+
|
|
491
|
+
# Rule 6 (L2 & L6): Missing Assertions / Fake-Pass
|
|
492
|
+
assert_matches = list(ASSERTION_PATTERN.finditer(clean_code))
|
|
493
|
+
tautology_matches = list(TAUTOLOGY_PATTERN.finditer(clean_code))
|
|
494
|
+
has_real_assert = any(
|
|
495
|
+
not any(tm.start() <= am.start() < tm.end() for tm in tautology_matches)
|
|
496
|
+
for am in assert_matches
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
# F06: Check test cases per function block if multiple tests exist
|
|
500
|
+
TEST_BLOCK_PATTERN = re.compile(
|
|
501
|
+
r'(?m)^[ \t]*(?:(?:test|it)(?:\.(?:only|skip))?\s*\(|def\s+test_[a-zA-Z0-9_]+\s*\(|(?:@Test\s+)?(?:public\s+)?(?:void|def)\s+test[a-zA-Z0-9_]*\s*\()'
|
|
502
|
+
)
|
|
503
|
+
test_matches = list(TEST_BLOCK_PATTERN.finditer(clean_code))
|
|
504
|
+
if len(test_matches) >= 2:
|
|
505
|
+
for i, tm in enumerate(test_matches):
|
|
506
|
+
b_start = tm.start()
|
|
507
|
+
b_end = test_matches[i + 1].start() if i + 1 < len(test_matches) else len(clean_code)
|
|
508
|
+
block_code = clean_code[b_start:b_end]
|
|
509
|
+
b_asserts = list(ASSERTION_PATTERN.finditer(block_code))
|
|
510
|
+
b_tauts = list(TAUTOLOGY_PATTERN.finditer(block_code))
|
|
511
|
+
block_has_real_assert = any(
|
|
512
|
+
not any(tt.start() <= ta.start() < tt.end() for tt in b_tauts)
|
|
513
|
+
for ta in b_asserts
|
|
514
|
+
)
|
|
515
|
+
b_has_interactions = any(
|
|
516
|
+
kw in block_code for kw in ["click", "select", "save", "delete", "create", "search", "navigate", "open", "fill", "goto", "post", "get"]
|
|
517
|
+
)
|
|
518
|
+
b_line = clean_code[:b_start].count("\n") + 1
|
|
519
|
+
if b_has_interactions and not block_has_real_assert:
|
|
520
|
+
defects.append({
|
|
521
|
+
"type": "FAKE_PASS_NO_ASSERTION",
|
|
522
|
+
"severity": "WARNING",
|
|
523
|
+
"line": b_line,
|
|
524
|
+
"message": f"Test block starting at line {b_line} performs interactions but contains zero verification/assertion statements."
|
|
525
|
+
})
|
|
526
|
+
else:
|
|
527
|
+
has_interactions = any(
|
|
528
|
+
kw in clean_code for kw in ["click", "select", "save", "delete", "create", "search", "navigate", "open", "fill", "goto", "post", "get"]
|
|
529
|
+
)
|
|
530
|
+
is_explicit_test = any(t in os.path.basename(file_path).lower() for t in ["test", "spec", "tc-", "tc_"])
|
|
531
|
+
|
|
532
|
+
if has_interactions and not has_real_assert:
|
|
533
|
+
defects.append({
|
|
534
|
+
"type": "FAKE_PASS_NO_ASSERTION",
|
|
535
|
+
"severity": "WARNING",
|
|
536
|
+
"line": 1,
|
|
537
|
+
"message": "Test script performs AUT mutations/interactions but contains zero verification/assertion statements."
|
|
538
|
+
})
|
|
539
|
+
elif is_explicit_test and not has_real_assert and not has_interactions and len(executable_lines) > 0:
|
|
540
|
+
defects.append({
|
|
541
|
+
"type": "NO_ASSERTION_IN_TEST",
|
|
542
|
+
"severity": "WARNING",
|
|
543
|
+
"line": 1,
|
|
544
|
+
"message": "Test case file contains executable logic but has no verification or assertion statements."
|
|
545
|
+
})
|
|
546
|
+
|
|
547
|
+
active_defects = []
|
|
548
|
+
disabled = {r.upper() for r in (disabled_rules or [])}
|
|
549
|
+
norm_rel = rel_path.replace("\\", "/")
|
|
550
|
+
|
|
551
|
+
for d in defects:
|
|
552
|
+
d_type = d.get("type", "").upper()
|
|
553
|
+
if d_type in disabled:
|
|
554
|
+
continue
|
|
555
|
+
line_idx = d.get("line", 1)
|
|
556
|
+
|
|
557
|
+
# Check suppression comment (# synapse-ignore or // synapse-ignore)
|
|
558
|
+
suppressed = False
|
|
559
|
+
candidates = []
|
|
560
|
+
if 1 <= line_idx <= len(raw_lines):
|
|
561
|
+
candidates.append(raw_lines[line_idx - 1])
|
|
562
|
+
if line_idx > 1 and line_idx - 1 <= len(raw_lines):
|
|
563
|
+
candidates.append(raw_lines[line_idx - 2])
|
|
564
|
+
for c in candidates:
|
|
565
|
+
if "synapse-ignore" in c.lower():
|
|
566
|
+
m = re.search(r'synapse-ignore:\s*([A-Za-z0-9_,\s\*\-]+)', c, re.IGNORECASE)
|
|
567
|
+
if m:
|
|
568
|
+
rules = [r.strip().upper() for r in m.group(1).split(",") if r.strip()]
|
|
569
|
+
if "*" in rules or "ALL" in rules or d_type in rules:
|
|
570
|
+
suppressed = True
|
|
571
|
+
break
|
|
572
|
+
if suppressed:
|
|
573
|
+
continue
|
|
574
|
+
|
|
575
|
+
# Check baseline suppression
|
|
576
|
+
if baseline_set:
|
|
577
|
+
if (norm_rel, d_type) in baseline_set or (norm_rel, d_type, line_idx) in baseline_set:
|
|
578
|
+
continue
|
|
579
|
+
|
|
580
|
+
active_defects.append(d)
|
|
581
|
+
|
|
582
|
+
defects = active_defects
|
|
148
583
|
|
|
149
584
|
return {
|
|
150
585
|
"file": rel_path,
|
|
586
|
+
"role": role,
|
|
151
587
|
"defects": defects,
|
|
152
588
|
"is_clean": len(defects) == 0
|
|
153
589
|
}
|
|
154
590
|
|
|
155
591
|
|
|
156
|
-
def run_infer_linter(target_dir=None
|
|
157
|
-
|
|
158
|
-
|
|
592
|
+
def run_infer_linter(target_dir=None, output_path=None, strict: bool = False,
|
|
593
|
+
output_format="text", baseline_path=None, generate_baseline_path=None, config_path=None):
|
|
594
|
+
"""Scans test scripts across Groovy, TypeScript, JavaScript, Python, and Java with baseline and SARIF support."""
|
|
595
|
+
EXTENSIONS = ("*.groovy", "*.ts", "*.tsx", "*.js", "*.jsx", "*.py", "*.java")
|
|
596
|
+
|
|
597
|
+
cfg = load_config(config_path=config_path)
|
|
598
|
+
linter_cfg = cfg.get("infer_linter", {})
|
|
599
|
+
extra_ignored = set(linter_cfg.get("exclude_paths", []))
|
|
600
|
+
disabled_rules = set(linter_cfg.get("disabled_rules", []))
|
|
601
|
+
baseline_set = load_baseline(baseline_path)
|
|
602
|
+
|
|
603
|
+
if linter_cfg.get("strict") is not None and not strict:
|
|
604
|
+
strict = bool(linter_cfg.get("strict"))
|
|
159
605
|
|
|
160
606
|
if target_dir:
|
|
161
607
|
if os.path.isfile(target_dir):
|
|
@@ -163,9 +609,16 @@ def run_infer_linter(target_dir=None):
|
|
|
163
609
|
scan_dir = os.path.dirname(target_dir)
|
|
164
610
|
elif os.path.isdir(target_dir):
|
|
165
611
|
scan_dir = target_dir
|
|
166
|
-
|
|
612
|
+
raw_files = []
|
|
167
613
|
for ext in EXTENSIONS:
|
|
168
|
-
|
|
614
|
+
raw_files.extend(glob.glob(os.path.join(scan_dir, "**", ext), recursive=True))
|
|
615
|
+
# Filter out non-test files like tools, __init__.py, node_modules when scanning generic directory
|
|
616
|
+
ignored_parts = {"tools", "node_modules", ".git", "__pycache__", "venv", ".venv"} | extra_ignored
|
|
617
|
+
script_files = [
|
|
618
|
+
f for f in raw_files
|
|
619
|
+
if not any(part.lower() in ignored_parts for part in os.path.relpath(f, scan_dir).replace("\\", "/").split("/")[:-1])
|
|
620
|
+
and os.path.basename(f) not in {"__init__.py", "setup.py"}
|
|
621
|
+
]
|
|
169
622
|
else:
|
|
170
623
|
scan_dir = target_dir
|
|
171
624
|
script_files = []
|
|
@@ -186,9 +639,10 @@ def run_infer_linter(target_dir=None):
|
|
|
186
639
|
critical_count = 0
|
|
187
640
|
warning_count = 0
|
|
188
641
|
clean_count = 0
|
|
642
|
+
all_defects = []
|
|
189
643
|
|
|
190
644
|
for s in sorted(set(script_files)):
|
|
191
|
-
res = analyze_script(s)
|
|
645
|
+
res = analyze_script(s, baseline_set=baseline_set, disabled_rules=disabled_rules)
|
|
192
646
|
results.append(res)
|
|
193
647
|
crit = sum(1 for d in res["defects"] if d["severity"] == "CRITICAL")
|
|
194
648
|
warn = sum(1 for d in res["defects"] if d["severity"] == "WARNING")
|
|
@@ -196,15 +650,84 @@ def run_infer_linter(target_dir=None):
|
|
|
196
650
|
warning_count += warn
|
|
197
651
|
if res["is_clean"]:
|
|
198
652
|
clean_count += 1
|
|
653
|
+
for d in res["defects"]:
|
|
654
|
+
all_defects.append({
|
|
655
|
+
"file": res["file"],
|
|
656
|
+
"role": res["role"],
|
|
657
|
+
"type": d["type"],
|
|
658
|
+
"severity": d["severity"],
|
|
659
|
+
"line": d["line"],
|
|
660
|
+
"message": d["message"]
|
|
661
|
+
})
|
|
662
|
+
|
|
663
|
+
# If requested to generate a baseline file, save and exit
|
|
664
|
+
if generate_baseline_path:
|
|
665
|
+
save_baseline(generate_baseline_path, all_defects)
|
|
666
|
+
print(f"✅ Generated baseline file with {len(all_defects)} defects at: {generate_baseline_path}")
|
|
667
|
+
return 0
|
|
199
668
|
|
|
200
669
|
total_scripts = len(script_files)
|
|
201
670
|
if total_scripts == 0:
|
|
202
671
|
health_score = 0.0
|
|
203
672
|
clean_pct = 0.0
|
|
204
673
|
else:
|
|
205
|
-
health_score = round(((total_scripts - critical_count) / total_scripts) * 100, 1)
|
|
206
674
|
clean_pct = round((clean_count / total_scripts) * 100, 1)
|
|
675
|
+
critical_penalty = (critical_count * 20.0) / total_scripts
|
|
676
|
+
raw_score = (clean_count / total_scripts) * 100.0 - critical_penalty
|
|
677
|
+
health_score = max(0.0, min(100.0, round(raw_score, 1)))
|
|
678
|
+
|
|
679
|
+
if clean_count < total_scripts and health_score >= 100.0:
|
|
680
|
+
health_score = 99.0
|
|
681
|
+
if clean_count == 0 and total_scripts > 0:
|
|
682
|
+
health_score = 0.0
|
|
683
|
+
|
|
684
|
+
defect_scripts = [r for r in results if not r["is_clean"]]
|
|
685
|
+
report_data = {
|
|
686
|
+
"timestamp": datetime.now().isoformat(),
|
|
687
|
+
"total_scripts": total_scripts,
|
|
688
|
+
"clean_scripts": clean_count,
|
|
689
|
+
"critical_defects": critical_count,
|
|
690
|
+
"warnings": warning_count,
|
|
691
|
+
"health_score_pct": health_score,
|
|
692
|
+
"defect_details": defect_scripts,
|
|
693
|
+
"all_defects": all_defects
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
# Handle specialized output formats
|
|
697
|
+
if output_format == "sarif":
|
|
698
|
+
sarif_out = format_sarif(report_data)
|
|
699
|
+
if output_path:
|
|
700
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
|
701
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
702
|
+
f.write(sarif_out)
|
|
703
|
+
print(f"SARIF 2.1.0 report saved to: {output_path}")
|
|
704
|
+
else:
|
|
705
|
+
print(sarif_out)
|
|
706
|
+
return 1 if critical_count > 0 else 0
|
|
707
|
+
|
|
708
|
+
if output_format == "junit":
|
|
709
|
+
junit_out = format_junit(report_data)
|
|
710
|
+
if output_path:
|
|
711
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
|
712
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
713
|
+
f.write(junit_out)
|
|
714
|
+
print(f"JUnit XML report saved to: {output_path}")
|
|
715
|
+
else:
|
|
716
|
+
print(junit_out)
|
|
717
|
+
return 1 if critical_count > 0 else 0
|
|
718
|
+
|
|
719
|
+
if output_format == "json":
|
|
720
|
+
json_out = json.dumps(report_data, indent=2, ensure_ascii=False)
|
|
721
|
+
if output_path:
|
|
722
|
+
os.makedirs(os.path.dirname(os.path.abspath(output_path)), exist_ok=True)
|
|
723
|
+
with open(output_path, "w", encoding="utf-8") as f:
|
|
724
|
+
f.write(json_out)
|
|
725
|
+
print(f"JSON report saved to: {output_path}")
|
|
726
|
+
else:
|
|
727
|
+
print(json_out)
|
|
728
|
+
return 1 if critical_count > 0 else 0
|
|
207
729
|
|
|
730
|
+
# Default: Text output with ANSI banner
|
|
208
731
|
print(f"\n{BOLD}{CYAN}======================================================================{RESET}")
|
|
209
732
|
print(f"{BOLD}{CYAN} ENTERPRISE STATIC DEFECT & FAKE-PASS SENTINEL (Meta Infer-Style) {RESET}")
|
|
210
733
|
print(f"{BOLD}{CYAN}======================================================================{RESET}")
|
|
@@ -216,38 +739,41 @@ def run_infer_linter(target_dir=None):
|
|
|
216
739
|
print(f" Codebase Health Score : {YELLOW}N/A (0 test scripts discovered){RESET}")
|
|
217
740
|
else:
|
|
218
741
|
print(f" Target Scope : {total_scripts} Test Scripts in {os.path.basename(scan_dir)}/ (Polyglot)")
|
|
742
|
+
if baseline_set:
|
|
743
|
+
print(f" Baseline Filter : {CYAN}ACTIVE ({len(baseline_set)} known debt items suppressed){RESET}")
|
|
219
744
|
print(f" Critical Defects : {RED if critical_count > 0 else GREEN}{critical_count}{RESET} (Swallowed exceptions, empty scripts)")
|
|
220
745
|
print(f" Warnings / Suggestions : {YELLOW if warning_count > 0 else GREEN}{warning_count}{RESET} (Missing explicit asserts, excessive delays)")
|
|
221
746
|
print(f" Clean Scripts : {GREEN}{clean_count}/{total_scripts} ({clean_pct:.1f}%){RESET}")
|
|
222
747
|
print(f" Codebase Health Score : {GREEN if health_score == 100 else YELLOW}{health_score:.1f}%{RESET}")
|
|
223
748
|
print(f"{BOLD}{CYAN}======================================================================{RESET}\n")
|
|
224
749
|
|
|
225
|
-
# Print defect breakdown if any
|
|
226
|
-
defect_scripts = [r for r in results if not r["is_clean"]]
|
|
227
750
|
if defect_scripts:
|
|
228
751
|
print(f"{BOLD}Detected Items ({len(defect_scripts)} scripts):{RESET}")
|
|
229
|
-
for r in defect_scripts[:10]:
|
|
752
|
+
for r in defect_scripts[:10]:
|
|
230
753
|
for d in r["defects"]:
|
|
231
754
|
color = RED if d["severity"] == "CRITICAL" else YELLOW
|
|
232
755
|
print(f" - [{color}{d['severity']}{RESET}] {r['file']}:{d['line']} - {d['message']}")
|
|
233
756
|
if len(defect_scripts) > 10:
|
|
234
757
|
print(f" ... and {len(defect_scripts) - 10} more scripts (see full JSON report).\n")
|
|
235
758
|
|
|
236
|
-
|
|
237
|
-
os.makedirs(
|
|
238
|
-
|
|
239
|
-
"timestamp": datetime.now().isoformat(),
|
|
240
|
-
"total_scripts": total_scripts,
|
|
241
|
-
"clean_scripts": clean_count,
|
|
242
|
-
"critical_defects": critical_count,
|
|
243
|
-
"warnings": warning_count,
|
|
244
|
-
"health_score_pct": health_score,
|
|
245
|
-
"defect_details": defect_scripts
|
|
246
|
-
}
|
|
247
|
-
with open(OUTPUT_JSON, "w", encoding="utf-8") as f:
|
|
759
|
+
dest_json = os.path.abspath(output_path) if output_path else OUTPUT_JSON
|
|
760
|
+
os.makedirs(os.path.dirname(dest_json), exist_ok=True)
|
|
761
|
+
with open(dest_json, "w", encoding="utf-8") as f:
|
|
248
762
|
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
|
249
|
-
print(f"Detailed audit saved to: {
|
|
250
|
-
|
|
763
|
+
print(f"Detailed audit saved to: {dest_json}\n")
|
|
764
|
+
|
|
765
|
+
if not output_path and target_dir and os.path.isdir(target_dir) and not os.path.abspath(target_dir).startswith(REPO_ROOT):
|
|
766
|
+
try:
|
|
767
|
+
target_out = os.path.join(os.path.abspath(target_dir), "reports", "infer_linter_report.json")
|
|
768
|
+
os.makedirs(os.path.dirname(target_out), exist_ok=True)
|
|
769
|
+
with open(target_out, "w", encoding="utf-8") as f:
|
|
770
|
+
json.dump(report_data, f, indent=2, ensure_ascii=False)
|
|
771
|
+
except OSError:
|
|
772
|
+
pass
|
|
773
|
+
|
|
774
|
+
if strict:
|
|
775
|
+
fake_pass_count = sum(1 for r in results for d in r["defects"] if d["type"] in ("FAKE_PASS_NO_ASSERTION", "NO_ASSERTION_IN_TEST"))
|
|
776
|
+
return 0 if (critical_count == 0 and fake_pass_count == 0) else 1
|
|
251
777
|
return 0 if critical_count == 0 else 1
|
|
252
778
|
|
|
253
779
|
|
|
@@ -256,12 +782,28 @@ def main():
|
|
|
256
782
|
parser.add_argument("target", nargs="?", default=None, help="Target directory or script to scan")
|
|
257
783
|
parser.add_argument("--dir", help="Target directory to scan (defaults to Scripts/)")
|
|
258
784
|
parser.add_argument("--repo", help="Target repository root containing Scripts/")
|
|
785
|
+
parser.add_argument("--out", "--output", dest="output_file", help="Path to save report")
|
|
786
|
+
parser.add_argument("--format", choices=["text", "json", "sarif", "junit"], default="text", help="Output format (default: text)")
|
|
787
|
+
parser.add_argument("--baseline", help="Path to baseline JSON file to ignore known technical debt")
|
|
788
|
+
parser.add_argument("--generate-baseline", dest="generate_baseline", help="Generate a baseline JSON file from current findings")
|
|
789
|
+
parser.add_argument("--config", help="Path to synapse.config.json")
|
|
790
|
+
parser.add_argument("--strict", action="store_true", default=True, help="Treat missing assertions / fake-passes as fatal errors (default: True)")
|
|
791
|
+
parser.add_argument("--no-strict", "--lenient", action="store_false", dest="strict", help="Allow missing assertions with warning only")
|
|
259
792
|
args = parser.parse_args()
|
|
260
793
|
|
|
261
794
|
target_dir = args.dir or args.target
|
|
262
795
|
if not target_dir and args.repo:
|
|
263
796
|
target_dir = os.path.join(args.repo, "Scripts")
|
|
264
|
-
|
|
797
|
+
|
|
798
|
+
return run_infer_linter(
|
|
799
|
+
target_dir=target_dir,
|
|
800
|
+
output_path=args.output_file,
|
|
801
|
+
strict=args.strict,
|
|
802
|
+
output_format=args.format,
|
|
803
|
+
baseline_path=args.baseline,
|
|
804
|
+
generate_baseline_path=args.generate_baseline,
|
|
805
|
+
config_path=args.config
|
|
806
|
+
)
|
|
265
807
|
|
|
266
808
|
|
|
267
809
|
if __name__ == "__main__":
|