@huicai/cli 2026.814.0 → 2026.1019.0
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/LICENSE +391 -32
- package/README.md +137 -8
- package/bin/huicai.js +146 -73
- package/package.json +27 -8
- package/skills/ab-testing/trigger-cases.json +30 -0
- package/skills/academic-writing/trigger-cases.json +30 -0
- package/skills/accounting-ops/trigger-cases.json +30 -0
- package/skills/ai-agent/trigger-cases.json +30 -0
- package/skills/ai-aiops/trigger-cases.json +30 -0
- package/skills/ai-coding/trigger-cases.json +30 -0
- package/skills/ai-compliance/trigger-cases.json +30 -0
- package/skills/ai-eval/trigger-cases.json +30 -0
- package/skills/ai-llm-app/trigger-cases.json +30 -0
- package/skills/ai-mlops/trigger-cases.json +30 -0
- package/skills/ai-multimodal/trigger-cases.json +30 -0
- package/skills/ai-prompt/trigger-cases.json +30 -0
- package/skills/ai-rag/trigger-cases.json +30 -0
- package/skills/anomaly-detection/trigger-cases.json +30 -0
- package/skills/api-design/trigger-cases.json +31 -0
- package/skills/api-integration/trigger-cases.json +31 -0
- package/skills/arch-design/trigger-cases.json +12 -0
- package/skills/arch-diagram/SKILL.md +118 -0
- package/skills/arch-diagram/trigger-cases.json +30 -0
- package/skills/arch-distributed/trigger-cases.json +30 -0
- package/skills/arch-multitenant/trigger-cases.json +30 -0
- package/skills/audio-brief/SKILL.md +65 -0
- package/skills/audio-brief/trigger-cases.json +30 -0
- package/skills/audit-assist/trigger-cases.json +30 -0
- package/skills/audit-chain/trigger-cases.json +31 -0
- package/skills/banking-credit/trigger-cases.json +30 -0
- package/skills/brand-design/trigger-cases.json +30 -0
- package/skills/business-plan/trigger-cases.json +30 -0
- package/skills/capital-ops/trigger-cases.json +30 -0
- package/skills/cicd-pipeline/trigger-cases.json +31 -0
- package/skills/code-archaeology/SKILL.md +55 -0
- package/skills/code-archaeology/trigger-cases.json +9 -0
- package/skills/code-quality/SKILL.md +138 -0
- package/skills/code-quality/references/languages.md +107 -0
- package/skills/code-quality/scripts/__pycache__/code_battery.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_check.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_csharp.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_fix.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_java.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_jsts.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_lens.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_python.cpython-313.pyc +0 -0
- package/skills/code-quality/scripts/__pycache__/code_testplan.cpython-312.pyc +0 -0
- package/skills/code-quality/scripts/code_battery.py +1072 -0
- package/skills/code-quality/scripts/code_check.py +1269 -0
- package/skills/code-quality/scripts/code_csharp.py +371 -0
- package/skills/code-quality/scripts/code_fix.py +201 -0
- package/skills/code-quality/scripts/code_java.py +695 -0
- package/skills/code-quality/scripts/code_jsts.py +313 -0
- package/skills/code-quality/scripts/code_lens.py +1079 -0
- package/skills/code-quality/scripts/code_python.py +536 -0
- package/skills/code-quality/scripts/code_testplan.py +239 -0
- package/skills/code-quality/trigger-cases.json +45 -0
- package/skills/code-review/SKILL.md +24 -0
- package/skills/code-review/trigger-cases.json +35 -0
- package/skills/commit-and-pr/trigger-cases.json +31 -0
- package/skills/content-creation/trigger-cases.json +30 -0
- package/skills/corporate-governance/trigger-cases.json +30 -0
- package/skills/crawl-chain/trigger-cases.json +31 -0
- package/skills/customer-service/trigger-cases.json +30 -0
- package/skills/data-analysis/trigger-cases.json +30 -0
- package/skills/data-chain/SKILL.md +1 -1
- package/skills/data-chain/trigger-cases.json +31 -0
- package/skills/data-governance/trigger-cases.json +30 -0
- package/skills/data-report-pro/trigger-cases.json +30 -0
- package/skills/data-streaming/trigger-cases.json +30 -0
- package/skills/data-wrangling/SKILL.md +1 -1
- package/skills/data-wrangling/trigger-cases.json +30 -0
- package/skills/db-es/trigger-cases.json +30 -0
- package/skills/db-migration/trigger-cases.json +30 -0
- package/skills/db-mysql/trigger-cases.json +30 -0
- package/skills/db-postgres/trigger-cases.json +30 -0
- package/skills/db-redis/trigger-cases.json +30 -0
- package/skills/debug-root-cause/trigger-cases.json +30 -0
- package/skills/dependency-audit/SKILL.md +56 -0
- package/skills/dependency-audit/trigger-cases.json +7 -0
- package/skills/dependency-upgrade/trigger-cases.json +31 -0
- package/skills/dev-cpp/trigger-cases.json +30 -0
- package/skills/dev-cross-platform/trigger-cases.json +30 -0
- package/skills/dev-csharp/SKILL.md +23 -0
- package/skills/dev-csharp/trigger-cases.json +30 -0
- package/skills/dev-frontend/SKILL.md +19 -0
- package/skills/dev-frontend/trigger-cases.json +30 -0
- package/skills/dev-go/trigger-cases.json +30 -0
- package/skills/dev-harmony/trigger-cases.json +30 -0
- package/skills/dev-java/SKILL.md +20 -0
- package/skills/dev-java/trigger-cases.json +30 -0
- package/skills/dev-mobile/trigger-cases.json +30 -0
- package/skills/dev-node/trigger-cases.json +30 -0
- package/skills/dev-php/trigger-cases.json +30 -0
- package/skills/dev-python/SKILL.md +21 -0
- package/skills/dev-python/trigger-cases.json +30 -0
- package/skills/dev-rust/trigger-cases.json +30 -0
- package/skills/dev-toolchain/trigger-cases.json +31 -0
- package/skills/doc-chain/trigger-cases.json +31 -0
- package/skills/doc-compare/trigger-cases.json +30 -0
- package/skills/docx-author/SKILL.md +127 -25
- package/skills/docx-author/scripts/__pycache__/docx_battery.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_build.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_check.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/docx_layout.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/__pycache__/zh_prose.cpython-312.pyc +0 -0
- package/skills/docx-author/scripts/docx_battery.py +586 -0
- package/skills/docx-author/scripts/docx_build.py +995 -0
- package/skills/docx-author/scripts/docx_check.py +1306 -0
- package/skills/docx-author/scripts/docx_doc.py +14 -154
- package/skills/docx-author/scripts/docx_layout.py +490 -0
- package/skills/docx-author/scripts/zh_prose.py +639 -0
- package/skills/docx-author/trigger-cases.json +58 -0
- package/skills/echo-brief/SKILL.md +84 -0
- package/skills/echo-brief/references/echo-format.md +68 -0
- package/skills/echo-brief/trigger-cases.json +40 -0
- package/skills/ecommerce-ops/trigger-cases.json +30 -0
- package/skills/education-training/trigger-cases.json +30 -0
- package/skills/ehs-safety/trigger-cases.json +30 -0
- package/skills/email-writing/trigger-cases.json +30 -0
- package/skills/eng-ai/trigger-cases.json +30 -0
- package/skills/eng-architecture/trigger-cases.json +31 -0
- package/skills/eng-automation/trigger-cases.json +31 -0
- package/skills/eng-collaboration/trigger-cases.json +30 -0
- package/skills/eng-complex-env/trigger-cases.json +30 -0
- package/skills/eng-data-platform/trigger-cases.json +31 -0
- package/skills/eng-database/trigger-cases.json +31 -0
- package/skills/eng-develop/trigger-cases.json +30 -0
- package/skills/eng-docs/SKILL.md +24 -0
- package/skills/eng-docs/trigger-cases.json +30 -0
- package/skills/eng-efficiency/trigger-cases.json +30 -0
- package/skills/eng-infra/trigger-cases.json +30 -0
- package/skills/eng-integration/trigger-cases.json +31 -0
- package/skills/eng-legacy/trigger-cases.json +31 -0
- package/skills/eng-observability/trigger-cases.json +31 -0
- package/skills/eng-ops/trigger-cases.json +31 -0
- package/skills/eng-performance/trigger-cases.json +31 -0
- package/skills/eng-planning/trigger-cases.json +31 -0
- package/skills/eng-quality/SKILL.md +8 -0
- package/skills/eng-quality/trigger-cases.json +31 -0
- package/skills/eng-release/trigger-cases.json +31 -0
- package/skills/eng-reliability/trigger-cases.json +31 -0
- package/skills/eng-requirements/trigger-cases.json +12 -0
- package/skills/eng-research/trigger-cases.json +30 -0
- package/skills/eng-security/trigger-cases.json +31 -0
- package/skills/eng-ux/trigger-cases.json +30 -0
- package/skills/engineering-chain/trigger-cases.json +30 -0
- package/skills/env-offline/trigger-cases.json +31 -0
- package/skills/env-private-deploy/trigger-cases.json +31 -0
- package/skills/env-xinchuang/trigger-cases.json +31 -0
- package/skills/equipment-tpm/trigger-cases.json +30 -0
- package/skills/esg-report/trigger-cases.json +30 -0
- package/skills/event-management/trigger-cases.json +30 -0
- package/skills/excel-analysis-chain/SKILL.md +17 -4
- package/skills/excel-analysis-chain/trigger-cases.json +31 -0
- package/skills/explain-codebase/trigger-cases.json +11 -0
- package/skills/file-ingest/trigger-cases.json +30 -0
- package/skills/finance-analysis/trigger-cases.json +31 -0
- package/skills/finance-chain/trigger-cases.json +31 -0
- package/skills/financial-modeling/trigger-cases.json +31 -0
- package/skills/find-simplifications/SKILL.md +43 -0
- package/skills/find-simplifications/trigger-cases.json +23 -0
- package/skills/foreign-trade/trigger-cases.json +30 -0
- package/skills/funnel-cohort/trigger-cases.json +30 -0
- package/skills/gov-document/trigger-cases.json +30 -0
- package/skills/gui-change-evidence/SKILL.md +47 -0
- package/skills/gui-change-evidence/trigger-cases.json +23 -0
- package/skills/hr-chain/trigger-cases.json +31 -0
- package/skills/hr-operations/trigger-cases.json +30 -0
- package/skills/info-disclosure/trigger-cases.json +31 -0
- package/skills/infra-gateway/trigger-cases.json +31 -0
- package/skills/infra-k8s/trigger-cases.json +31 -0
- package/skills/infra-mq/trigger-cases.json +30 -0
- package/skills/insurance-advisory/trigger-cases.json +30 -0
- package/skills/internal-audit/trigger-cases.json +30 -0
- package/skills/ip-management/trigger-cases.json +31 -0
- package/skills/ipo-listing/trigger-cases.json +31 -0
- package/skills/it-audit/trigger-cases.json +31 -0
- package/skills/itsm-ops/trigger-cases.json +31 -0
- package/skills/knowledge-sop/trigger-cases.json +31 -0
- package/skills/legal-chain/trigger-cases.json +31 -0
- package/skills/legal-contract/trigger-cases.json +30 -0
- package/skills/litigation-support/trigger-cases.json +31 -0
- package/skills/logistics-warehouse/trigger-cases.json +30 -0
- package/skills/long-doc-analysis/trigger-cases.json +31 -0
- package/skills/management-accounting/trigger-cases.json +30 -0
- package/skills/manufacturing-chain/trigger-cases.json +31 -0
- package/skills/manufacturing-quality/trigger-cases.json +30 -0
- package/skills/marketing-analytics/trigger-cases.json +31 -0
- package/skills/marketing-chain/trigger-cases.json +31 -0
- package/skills/meeting-minutes/trigger-cases.json +30 -0
- package/skills/metrics-system/trigger-cases.json +31 -0
- package/skills/negotiation-prep/trigger-cases.json +30 -0
- package/skills/obs-prometheus/trigger-cases.json +31 -0
- package/skills/office-automation/trigger-cases.json +31 -0
- package/skills/okr-review/trigger-cases.json +30 -0
- package/skills/ops-fleet/trigger-cases.json +31 -0
- package/skills/ops-linux/trigger-cases.json +31 -0
- package/skills/optimization-solver/trigger-cases.json +30 -0
- package/skills/patent-disclosure/SKILL.md +105 -0
- package/skills/patent-disclosure/references/figures.md +110 -0
- package/skills/patent-disclosure/references/outline.md +162 -0
- package/skills/patent-disclosure/scripts/patent_docx.py +230 -0
- package/skills/patent-disclosure/trigger-cases.json +31 -0
- package/skills/patent-drafting/SKILL.md +108 -0
- package/skills/patent-drafting/trigger-cases.json +31 -0
- package/skills/patent-portfolio/SKILL.md +81 -0
- package/skills/patent-portfolio/trigger-cases.json +31 -0
- package/skills/pdf-toolkit/trigger-cases.json +31 -0
- package/skills/perf-optimize/trigger-cases.json +31 -0
- package/skills/pivot-crosstab/trigger-cases.json +30 -0
- package/skills/policy-analysis/trigger-cases.json +31 -0
- package/skills/portfolio-analysis/trigger-cases.json +31 -0
- package/skills/ppt-chain/SKILL.md +14 -6
- package/skills/ppt-chain/trigger-cases.json +31 -0
- package/skills/pptx-author/SKILL.md +86 -33
- package/skills/pptx-author/scripts/__pycache__/pptx_check.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_deck.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/__pycache__/pptx_layout.cpython-312.pyc +0 -0
- package/skills/pptx-author/scripts/pptx_battery.py +178 -0
- package/skills/pptx-author/scripts/pptx_build.py +119 -0
- package/skills/pptx-author/scripts/pptx_check.py +1182 -0
- package/skills/pptx-author/scripts/pptx_deck.py +736 -145
- package/skills/pptx-author/scripts/pptx_layout.py +384 -0
- package/skills/pptx-author/trigger-cases.json +15 -0
- package/skills/pr-comms/trigger-cases.json +30 -0
- package/skills/procurement-supply/trigger-cases.json +31 -0
- package/skills/product-design/trigger-cases.json +31 -0
- package/skills/production-planning/trigger-cases.json +31 -0
- package/skills/project-management/trigger-cases.json +31 -0
- package/skills/project-proposal/trigger-cases.json +31 -0
- package/skills/prose-standard/SKILL.md +53 -0
- package/skills/prose-standard/trigger-cases.json +23 -0
- package/skills/qa-api-automation/trigger-cases.json +31 -0
- package/skills/qa-mobile-automation/trigger-cases.json +31 -0
- package/skills/qa-ui-automation/trigger-cases.json +31 -0
- package/skills/quant-analysis/trigger-cases.json +31 -0
- package/skills/rag-triage/SKILL.md +64 -0
- package/skills/rag-triage/evals.json +57 -0
- package/skills/rag-triage/references/triage-playbook.md +75 -0
- package/skills/rag-triage/trigger-cases.json +31 -0
- package/skills/real-estate-property/trigger-cases.json +30 -0
- package/skills/refactor-safely/SKILL.md +1 -1
- package/skills/refactor-safely/trigger-cases.json +30 -0
- package/skills/regression-forecast/trigger-cases.json +31 -0
- package/skills/release-checklist/trigger-cases.json +31 -0
- package/skills/report-craft/SKILL.md +76 -0
- package/skills/report-craft/references/feasibility.md +118 -0
- package/skills/report-craft/references/review-checklist.md +51 -0
- package/skills/report-craft/references/visuals.md +114 -0
- package/skills/report-craft/references/writing.md +80 -0
- package/skills/report-craft/scripts/__pycache__/chart_render.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/fin_model.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/__pycache__/project_facts.cpython-312.pyc +0 -0
- package/skills/report-craft/scripts/chart_render.py +1222 -0
- package/skills/report-craft/scripts/fin_model.py +602 -0
- package/skills/report-craft/scripts/project_facts.py +580 -0
- package/skills/report-craft/scripts/report_battery.py +349 -0
- package/skills/report-craft/trigger-cases.json +15 -0
- package/skills/retail-store-ops/trigger-cases.json +30 -0
- package/skills/risk-compliance/trigger-cases.json +31 -0
- package/skills/scope-creep/SKILL.md +45 -0
- package/skills/scope-creep/trigger-cases.json +7 -0
- package/skills/sec-auth/trigger-cases.json +31 -0
- package/skills/sec-compliance/trigger-cases.json +31 -0
- package/skills/securities-chain/trigger-cases.json +31 -0
- package/skills/securities-research/trigger-cases.json +31 -0
- package/skills/security-audit/trigger-cases.json +31 -0
- package/skills/slides-html/SKILL.md +25 -10
- package/skills/slides-html/trigger-cases.json +11 -0
- package/skills/sql-analysis/SKILL.md +110 -0
- package/skills/sql-analysis/trigger-cases.json +34 -0
- package/skills/sql-tuning/trigger-cases.json +31 -0
- package/skills/statistical-testing/trigger-cases.json +31 -0
- package/skills/strategy-planning/trigger-cases.json +30 -0
- package/skills/supply-chain/trigger-cases.json +31 -0
- package/skills/tax-compliance/trigger-cases.json +30 -0
- package/skills/tech-doc/SKILL.md +262 -0
- package/skills/tech-doc/scripts/__pycache__/archive_battery.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_build.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_check.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/archive_spec.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/__pycache__/doc_templates.cpython-312.pyc +0 -0
- package/skills/tech-doc/scripts/archive_battery.py +860 -0
- package/skills/tech-doc/scripts/archive_build.py +599 -0
- package/skills/tech-doc/scripts/archive_check.py +1307 -0
- package/skills/tech-doc/scripts/archive_spec.py +499 -0
- package/skills/tech-doc/scripts/doc_templates.py +624 -0
- package/skills/tech-doc/trigger-cases.json +57 -0
- package/skills/tech-research/trigger-cases.json +31 -0
- package/skills/tender-bidding/trigger-cases.json +31 -0
- package/skills/translation-pro/trigger-cases.json +30 -0
- package/skills/treasury-mgmt/trigger-cases.json +31 -0
- package/skills/trim-cot-leakage/SKILL.md +58 -0
- package/skills/trim-cot-leakage/trigger-cases.json +23 -0
- package/skills/web-craft/SKILL.md +103 -0
- package/skills/web-craft/references/craft-checklist.md +75 -0
- package/skills/web-craft/references/design-direction.md +122 -0
- package/skills/web-craft/references/h5-mobile.md +100 -0
- package/skills/web-craft/references/motion.md +136 -0
- package/skills/web-craft/references/recipes.md +74 -0
- package/skills/web-craft/templates/wedding-invitation.html +1180 -0
- package/skills/web-craft/trigger-cases.json +14 -0
- package/skills/web-scraping/trigger-cases.json +34 -0
- package/skills/web-search/SKILL.md +11 -10
- package/skills/web-search/trigger-cases.json +31 -0
- package/skills/weekly-report/SKILL.md +134 -0
- package/skills/weekly-report/trigger-cases.json +11 -0
- package/skills/write-tests/trigger-cases.json +31 -0
- package/skills/xlsx-analysis/SKILL.md +143 -77
- package/skills/xlsx-analysis/scripts/__pycache__/data_profile.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_build.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_check.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_layout.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/__pycache__/xlsx_toolkit.cpython-312.pyc +0 -0
- package/skills/xlsx-analysis/scripts/data_profile.py +391 -0
- package/skills/xlsx-analysis/scripts/xlsx_battery.py +389 -0
- package/skills/xlsx-analysis/scripts/xlsx_build.py +522 -0
- package/skills/xlsx-analysis/scripts/xlsx_check.py +924 -0
- package/skills/xlsx-analysis/scripts/xlsx_layout.py +235 -0
- package/skills/xlsx-analysis/scripts/xlsx_style.py +28 -66
- package/skills/xlsx-analysis/scripts/xlsx_toolkit.py +61 -5
- package/skills/xlsx-analysis/trigger-cases.json +59 -0
- package/squads/hc-defense.md +47 -0
- package/squads/hc-rdarchive.md +110 -0
- package/squads/hc-redteam.md +50 -0
- package/squads/hc-weekly.md +125 -0
- package/src/account.js +1 -1
- package/src/acp.js +1 -0
- package/src/advisor.js +1 -1
- package/src/agent.js +1 -1
- package/src/api.js +1 -1
- package/src/archaeology.js +1 -0
- package/src/ask-ui.js +1 -0
- package/src/assets-import.js +1 -1
- package/src/autofix.js +1 -1
- package/src/automation-tools.js +1 -1
- package/src/automation.js +1 -1
- package/src/autorun.js +1 -1
- package/src/banner.js +1 -1
- package/src/capability-probe.js +1 -0
- package/src/checkpoints.js +1 -1
- package/src/code-tools.js +1 -0
- package/src/codeindex.js +1 -1
- package/src/codemode.js +1 -0
- package/src/command-bridge.js +1 -0
- package/src/command-ui.js +1 -0
- package/src/commands.js +1 -1
- package/src/compaction.js +1 -1
- package/src/computer.js +1 -1
- package/src/config.js +1 -1
- package/src/consult.js +1 -1
- package/src/context-report.js +1 -1
- package/src/context-window.js +1 -0
- package/src/context.js +1 -1
- package/src/crew.js +1 -0
- package/src/crush.js +1 -0
- package/src/daemon.js +1 -1
- package/src/decisions.js +1 -1
- package/src/depaudit.js +1 -0
- package/src/device.js +1 -1
- package/src/diagram-draw.js +1 -0
- package/src/diagram-layouts.js +1 -0
- package/src/diagram-raster.js +1 -0
- package/src/diagram.js +1 -0
- package/src/dictation.js +1 -0
- package/src/diffview.js +1 -1
- package/src/doctor.js +1 -1
- package/src/docx-tools.js +1 -0
- package/src/errkind.js +1 -1
- package/src/evidence.js +1 -0
- package/src/executors.js +1 -1
- package/src/expert-catalog.js +1 -1
- package/src/experts-store.js +1 -1
- package/src/experts.js +1 -1
- package/src/external-agents.js +1 -0
- package/src/forks.js +1 -1
- package/src/fs-observe.js +1 -0
- package/src/gitflow.js +1 -1
- package/src/goalround.js +1 -0
- package/src/hooks.js +1 -1
- package/src/input.js +1 -1
- package/src/insights.js +1 -1
- package/src/jobs.js +1 -0
- package/src/kbsync.js +1 -1
- package/src/lsp.js +1 -0
- package/src/mcp-apps.js +1 -0
- package/src/mcp.js +1 -1
- package/src/memory.js +1 -1
- package/src/notify.js +1 -1
- package/src/permissions.js +1 -1
- package/src/plan-limits.js +1 -0
- package/src/policy.js +1 -0
- package/src/pptx-tools.js +1 -0
- package/src/presence.js +1 -1
- package/src/privatedir.js +1 -0
- package/src/pyruntime.js +1 -1
- package/src/radar.js +1 -0
- package/src/render.js +1 -1
- package/src/repl.js +1 -1
- package/src/report-tools.js +1 -0
- package/src/routing.js +1 -0
- package/src/sandbox-probe.js +1 -0
- package/src/sandbox-win-launcher.mjs +156 -0
- package/src/sandbox.js +1 -0
- package/src/scan.js +1 -1
- package/src/schedule.js +1 -1
- package/src/scopecreep.js +1 -0
- package/src/sdk.js +1 -0
- package/src/selection.js +1 -0
- package/src/session-events.js +1 -0
- package/src/session.js +1 -1
- package/src/sessionindex.js +1 -0
- package/src/sessionlog.js +1 -0
- package/src/skill-catalog.js +1 -0
- package/src/skill-lint.js +1 -0
- package/src/skill-optimize.js +1 -0
- package/src/skill-scan.js +1 -0
- package/src/skills.js +1 -1
- package/src/spill.js +1 -0
- package/src/squad.js +1 -1
- package/src/subagent.js +1 -1
- package/src/surface-state.js +1 -0
- package/src/tapd.js +1 -0
- package/src/terminal-select.js +1 -0
- package/src/terminal.js +1 -0
- package/src/test-runner.js +1 -0
- package/src/todo.js +1 -1
- package/src/toolpipe.js +1 -0
- package/src/tools.js +1 -1
- package/src/toon.js +1 -0
- package/src/transcript.js +1 -1
- package/src/tui/app.js +1 -1
- package/src/tui/command-menu.js +1 -0
- package/src/tui/progress.js +1 -0
- package/src/tui/scroll.js +1 -0
- package/src/tui/welcome.js +1 -0
- package/src/tui/workbench.js +1 -0
- package/src/ui-spec.js +1 -0
- package/src/uitree.js +1 -0
- package/src/uninstall.js +1 -1
- package/src/visual.js +1 -1
- package/src/voice-form.js +1 -0
- package/src/voice.js +1 -0
- package/src/watch.js +1 -1
- package/src/web-audit.js +1 -0
- package/src/web-cdp.js +1 -0
- package/src/web-inspect.js +1 -0
- package/src/websearch.js +1 -0
- package/src/weekly.js +1 -0
- package/src/workflow.js +1 -0
- package/src/worklog.js +1 -0
- package/src/xlsx-tools.js +1 -0
- package/ui/command-picker.js +79 -0
- package/skills/ab-testing/scripts/__pycache__/ab_test.cpython-312.pyc +0 -0
- package/skills/accounting-ops/scripts/__pycache__/acct_tools.cpython-312.pyc +0 -0
- package/skills/anomaly-detection/scripts/__pycache__/anomaly.cpython-312.pyc +0 -0
- package/skills/audit-assist/scripts/__pycache__/audit_sampling.cpython-312.pyc +0 -0
- package/skills/banking-credit/scripts/__pycache__/credit_metrics.cpython-312.pyc +0 -0
- package/skills/data-report-pro/scripts/__pycache__/bigdata.cpython-312.pyc +0 -0
- package/skills/data-wrangling/scripts/__pycache__/join_tools.cpython-312.pyc +0 -0
- package/skills/ecommerce-ops/scripts/__pycache__/gmv_decompose.cpython-312.pyc +0 -0
- package/skills/funnel-cohort/scripts/__pycache__/funnel.cpython-312.pyc +0 -0
- package/skills/management-accounting/scripts/__pycache__/costing.cpython-312.pyc +0 -0
- package/skills/manufacturing-quality/scripts/__pycache__/spc.cpython-312.pyc +0 -0
- package/skills/marketing-analytics/scripts/__pycache__/rfm.cpython-312.pyc +0 -0
- package/skills/optimization-solver/scripts/__pycache__/solver.cpython-312.pyc +0 -0
- package/skills/pivot-crosstab/scripts/__pycache__/pivot_tools.cpython-312.pyc +0 -0
- package/skills/portfolio-analysis/scripts/__pycache__/portfolio.cpython-312.pyc +0 -0
- package/skills/procurement-supply/scripts/__pycache__/supply.cpython-312.pyc +0 -0
- package/skills/production-planning/scripts/__pycache__/mrp_capacity.cpython-312.pyc +0 -0
- package/skills/project-management/scripts/__pycache__/cpm.cpython-312.pyc +0 -0
- package/skills/quant-analysis/scripts/__pycache__/quant.cpython-312.pyc +0 -0
- package/skills/real-estate-property/scripts/__pycache__/property_calc.cpython-312.pyc +0 -0
- package/skills/regression-forecast/scripts/__pycache__/regress.cpython-312.pyc +0 -0
- package/skills/securities-research/scripts/__pycache__/valuation.cpython-312.pyc +0 -0
- package/skills/statistical-testing/scripts/__pycache__/stat_tests.cpython-312.pyc +0 -0
- package/skills/treasury-mgmt/scripts/__pycache__/cashflow_forecast.cpython-312.pyc +0 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
# C# 专属体检:异步陷阱 + 异常与资源 + LINQ + EF Core / ASP.NET Core。
|
|
2
|
+
#
|
|
3
|
+
# ── C# 的事故形态 ──
|
|
4
|
+
# 和 Java 很像——编译通过、运行不报错、出事时离现场很远。但有几个是 C# 独有的,
|
|
5
|
+
# 而且都属于「写错了反而更像对的」那一类:
|
|
6
|
+
#
|
|
7
|
+
# async void Handler() ← 异常**没有任何办法捕获**,直接终结进程。
|
|
8
|
+
# 改成 async Task 只差四个字母,行为天差地别。
|
|
9
|
+
# var x = FooAsync().Result; ← 在有同步上下文的环境(ASP.NET 经典、WinForms、WPF)
|
|
10
|
+
# 必然死锁,而在控制台程序里一切正常——本地测不出来。
|
|
11
|
+
# throw ex; ← 堆栈从这一行**重新开始**,原始出错位置就此丢失。
|
|
12
|
+
# 少写两个字母(`throw;`)才是对的。
|
|
13
|
+
# DateTime.Now ← 服务器换个时区、夏令时切换,所有时间计算一起错。
|
|
14
|
+
# double 算钱 ← 和 Java 的 double、Python 的 float 是同一个坑。
|
|
15
|
+
#
|
|
16
|
+
# ── 解析前提 ──
|
|
17
|
+
# C# 走「掩码 + 括号配对」,和 Java 同一套(parse_braces 的 csharp 分支)。
|
|
18
|
+
# 改造前它落在 js 与 java 两个分支的空隙里:**方法一个都解析不到,parse_ok 却是 True**,
|
|
19
|
+
# 于是所有基于函数的规则静默失效,而报告照样说「未发现问题」。
|
|
20
|
+
#
|
|
21
|
+
# ── 一条必须说明的局限 ──
|
|
22
|
+
# 前四轮(JS/Java/Python/Vue)的误报都是在本仓库的真实代码上治出来的:
|
|
23
|
+
# Java 在 836 个文件上从 237 条假必修降到个位数,Python 在 94 个文件上从 136 条降到 19 条。
|
|
24
|
+
# **本仓库没有 C# 代码**,这一组的误报率只经过了压测台的合规样本验证,
|
|
25
|
+
# 没有真实项目的检验。判据因此刻意取得比 Java 那组更保守——拿不准一律不报。
|
|
26
|
+
|
|
27
|
+
import os
|
|
28
|
+
import re
|
|
29
|
+
import sys
|
|
30
|
+
|
|
31
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
32
|
+
|
|
33
|
+
# ── 异步 ────────────────────────────────────────────────────────────────────
|
|
34
|
+
_RE_BLOCKING_ASYNC = re.compile(
|
|
35
|
+
r'\.\s*(Result)\b|\.\s*(Wait)\s*\(\s*\)|\.\s*GetAwaiter\s*\(\s*\)\s*\.\s*(GetResult)\s*\(\s*\)')
|
|
36
|
+
# ── 异常 ────────────────────────────────────────────────────────────────────
|
|
37
|
+
_RE_THROW_EX = re.compile(r'\bcatch\s*\(\s*\w+\s+(\w+)\s*\)')
|
|
38
|
+
# ── 时间与精度 ──────────────────────────────────────────────────────────────
|
|
39
|
+
_RE_DATETIME_NOW = re.compile(r'\bDateTime(?:Offset)?\s*\.\s*(Now|Today)\b')
|
|
40
|
+
_RE_MONEY_TYPE = re.compile(
|
|
41
|
+
r'\b(double|float)\s+(\w*(?:[Aa]mount|[Pp]rice|[Mm]oney|[Ff]ee|[Cc]ost|[Bb]alance|[Tt]otal|[Ss]alary|[Tt]ax)\w*)\b')
|
|
42
|
+
# ── LINQ ────────────────────────────────────────────────────────────────────
|
|
43
|
+
_RE_COUNT_COMPARE = re.compile(r'\.\s*Count\s*\(\s*\)\s*(>|>=|==|!=)\s*0\b|\b0\s*(<|<=)\s*\w+\.\s*Count\s*\(\s*\)')
|
|
44
|
+
_RE_LINQ_DEFERRED = re.compile(r'\b(?:var|IEnumerable<[^>]+>)\s+(\w+)\s*=\s*[^;]*\.\s*(?:Where|Select|OrderBy|OrderByDescending|Take|Skip|SelectMany|Distinct)\s*\(')
|
|
45
|
+
_RE_ENUMERATE = re.compile(r'\.\s*(Count|Any|First|FirstOrDefault|Last|LastOrDefault|Single|SingleOrDefault|Sum|Max|Min|Average|ToList|ToArray|ToDictionary)\s*\(')
|
|
46
|
+
# ── 并发 ────────────────────────────────────────────────────────────────────
|
|
47
|
+
_RE_LOCK_BAD = re.compile(r'\block\s*\(\s*(this|typeof\s*\([^)]*\)|"[^"]*")\s*\)')
|
|
48
|
+
# ── 文化 ────────────────────────────────────────────────────────────────────
|
|
49
|
+
_RE_PARSE_NO_CULTURE = re.compile(r'\b(?:decimal|double|float|int|long|DateTime)\s*\.\s*(Parse|TryParse)\s*\(')
|
|
50
|
+
# ── EF Core / ASP.NET Core ──────────────────────────────────────────────────
|
|
51
|
+
_RE_SQL_RAW = re.compile(r'\.\s*(FromSqlRaw|ExecuteSqlRaw|FromSql)\s*\(')
|
|
52
|
+
_RE_DI_SINGLETON = re.compile(r'\.\s*AddSingleton\s*<\s*([\w.<>, ]+?)\s*>')
|
|
53
|
+
_RE_DB_QUERY = re.compile(
|
|
54
|
+
r'\.\s*(?:ToListAsync|FirstOrDefaultAsync|SingleOrDefaultAsync|FindAsync|AnyAsync|CountAsync'
|
|
55
|
+
r'|ToList|FirstOrDefault|SingleOrDefault|Find)\s*\(')
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _line_of(lens, off):
|
|
59
|
+
return lens.text.count('\n', 0, off) + 1
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def _unit_of(lens, line):
|
|
63
|
+
u = lens.unit_at(line)
|
|
64
|
+
return u.label if u else None
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _block_after(code, brace_off, limit=4000):
|
|
68
|
+
"""从 '{' 取到配对 '}' 的内容。"""
|
|
69
|
+
depth = 0
|
|
70
|
+
for i in range(brace_off, min(len(code), brace_off + limit)):
|
|
71
|
+
if code[i] == '{':
|
|
72
|
+
depth += 1
|
|
73
|
+
elif code[i] == '}':
|
|
74
|
+
depth -= 1
|
|
75
|
+
if depth == 0:
|
|
76
|
+
return code[brace_off:i]
|
|
77
|
+
return code[brace_off:brace_off + limit]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def check_csharp_traps(lens, ctx, out, add):
|
|
81
|
+
"""C# 的异步、异常、资源、时间精度这几类坑。"""
|
|
82
|
+
code = lens.code
|
|
83
|
+
|
|
84
|
+
for u in lens.functions():
|
|
85
|
+
mods = set(u.modifiers)
|
|
86
|
+
|
|
87
|
+
# ── async void:异常逃出方法后没有任何人能接住,直接终结进程 ──
|
|
88
|
+
# 测试文件里的 async void 另有一条规则(后果不同:不是崩进程,是用例假绿),
|
|
89
|
+
# 两条都报只会让人看到两遍同一件事。
|
|
90
|
+
if 'async' in mods and (u.ret_type or '').strip() == 'void' and not lens.is_test:
|
|
91
|
+
is_event_handler = bool(re.search(r'(EventArgs|sender)', ' '.join(u.params)))
|
|
92
|
+
add(out, ctx, 'logic.cs-async-void', 'error', lens, u.start,
|
|
93
|
+
f'`{u.label}` 是 `async void` —— 它返回的不是 Task,'
|
|
94
|
+
'调用方无法 await、无法 try/catch,方法内抛出的异常会直接**终结整个进程**',
|
|
95
|
+
('事件处理器确实只能是 async void,那就把整个方法体包在 try/catch 里,'
|
|
96
|
+
'一个异常都不许逃出去') if is_event_handler
|
|
97
|
+
else '改成 `async Task`。只有事件处理器例外,而那时必须自己兜住所有异常',
|
|
98
|
+
u.label)
|
|
99
|
+
|
|
100
|
+
# ── async 方法里一个 await 都没有:白加了状态机,还会误导调用方 ──
|
|
101
|
+
if 'async' in mods and not re.search(r'\bawait\b', u.body):
|
|
102
|
+
add(out, ctx, 'logic.cs-async-no-await', 'warn', lens, u.start,
|
|
103
|
+
f'`{u.label}` 标了 async 却没有任何 await —— 编译器会生成状态机但没有异步收益,'
|
|
104
|
+
'而调用方看到 Task 会以为它真的是异步的',
|
|
105
|
+
'没有异步操作就去掉 async(直接 `return Task.FromResult(x)`);'
|
|
106
|
+
'本该有 await 的话,检查是不是漏了', u.label)
|
|
107
|
+
|
|
108
|
+
# ── catch (Ex e) { … throw e; }:堆栈从这一行重新开始,原始出错位置丢失 ──
|
|
109
|
+
for cm in _RE_THROW_EX.finditer(u.body):
|
|
110
|
+
var = cm.group(1)
|
|
111
|
+
brace = u.body.find('{', cm.end())
|
|
112
|
+
if brace < 0:
|
|
113
|
+
continue
|
|
114
|
+
blk = _block_after(u.body, brace)
|
|
115
|
+
tm = re.search(rf'\bthrow\s+{re.escape(var)}\s*;', blk)
|
|
116
|
+
if not tm:
|
|
117
|
+
continue
|
|
118
|
+
ln = u.line_at(brace + tm.start())
|
|
119
|
+
add(out, ctx, 'logic.cs-throw-ex', 'error', lens, ln,
|
|
120
|
+
f'`throw {var};` —— 这会把异常的堆栈**从这一行重新开始**,'
|
|
121
|
+
'原始的出错位置就此丢失,线上只看得到这个 catch 在哪',
|
|
122
|
+
'要原样重抛就写 `throw;`(不带变量);'
|
|
123
|
+
f'要包一层就写 `throw new XxxException("…", {var});` 把原异常作为 innerException 传进去',
|
|
124
|
+
u.label)
|
|
125
|
+
|
|
126
|
+
# ── 循环里查库:典型的 N+1 ──
|
|
127
|
+
for lm in re.finditer(r'\b(?:foreach|for|while)\s*\([^)]*\)\s*\{', u.body):
|
|
128
|
+
blk = _block_after(u.body, lm.end() - 1)
|
|
129
|
+
qm = _RE_DB_QUERY.search(blk)
|
|
130
|
+
if not qm:
|
|
131
|
+
continue
|
|
132
|
+
if not re.search(r'\b(_?(?:db|context|ctx|repo|repository)\w*)\s*\.', blk, re.I):
|
|
133
|
+
continue
|
|
134
|
+
ln = u.line_at(lm.start())
|
|
135
|
+
add(out, ctx, 'logic.cs-query-in-loop', 'warn', lens, ln,
|
|
136
|
+
'循环体里在查数据库 —— 每转一圈发一次查询(N+1):'
|
|
137
|
+
'十条数据十次往返,一千条就是一千次,本地小数据量时完全看不出来',
|
|
138
|
+
'循环外一次性取回(`Where(x => ids.Contains(x.Id))`),'
|
|
139
|
+
'或者用 `Include` 把关联数据一起带出来,再在内存里组装', u.label)
|
|
140
|
+
|
|
141
|
+
# ── .Result / .Wait():在有同步上下文的环境里必然死锁 ──
|
|
142
|
+
for m in _RE_BLOCKING_ASYNC.finditer(code):
|
|
143
|
+
what = next((g for g in m.groups() if g), '')
|
|
144
|
+
ln = _line_of(lens, m.start())
|
|
145
|
+
line_txt = lens.line(ln)
|
|
146
|
+
if re.search(r'\b(Task\.Run|Main\s*\(|\.ConfigureAwait)', line_txt):
|
|
147
|
+
continue # Main 里同步等待、或已显式切上下文的,属于有意识的用法
|
|
148
|
+
add(out, ctx, 'logic.cs-blocking-async', 'error', lens, ln,
|
|
149
|
+
f'用 `.{what}` 同步等待异步结果 —— 在有同步上下文的环境'
|
|
150
|
+
'(ASP.NET 经典管线、WinForms、WPF)**必然死锁**:'
|
|
151
|
+
'当前线程被占住,而续体正等着回到这个线程',
|
|
152
|
+
'一路 await 到顶(`await FooAsync()`,调用方也改成 async)。'
|
|
153
|
+
'这个坑最难受的地方是:控制台程序和单元测试里跑得好好的,上了 Web 才死', _unit_of(lens, ln))
|
|
154
|
+
|
|
155
|
+
# ── DateTime.Now:带本地时区,换服务器/夏令时就全错 ──
|
|
156
|
+
for m in _RE_DATETIME_NOW.finditer(code):
|
|
157
|
+
ln = _line_of(lens, m.start())
|
|
158
|
+
add(out, ctx, 'logic.cs-datetime-now', 'warn', lens, ln,
|
|
159
|
+
f'`DateTime.{m.group(1)}` 取的是**服务器本地时间** —— '
|
|
160
|
+
'换个时区部署、或者夏令时切换那两天,所有时间计算与比较一起出错',
|
|
161
|
+
'存储与计算一律 `DateTime.UtcNow`(或 `DateTimeOffset.UtcNow`,它连偏移量一起带着),'
|
|
162
|
+
'只在展示给用户时才转本地时区', _unit_of(lens, ln))
|
|
163
|
+
|
|
164
|
+
# ── double/float 算钱 ──
|
|
165
|
+
for m in _RE_MONEY_TYPE.finditer(code):
|
|
166
|
+
ln = _line_of(lens, m.start())
|
|
167
|
+
add(out, ctx, 'logic.cs-double-for-money', 'error', lens, ln,
|
|
168
|
+
f'用 `{m.group(1)}` 声明金额字段 `{m.group(2)}` —— 二进制浮点存不下 0.1,'
|
|
169
|
+
'累加几次就会差出分,对账时查不出来源',
|
|
170
|
+
'金额一律用 `decimal`(C# 的十进制类型,正是为钱设计的);'
|
|
171
|
+
'与 Java 的 BigDecimal、Python 的 Decimal 是同一件事', _unit_of(lens, ln))
|
|
172
|
+
|
|
173
|
+
# ── lock(this) / lock(typeof(X)) / lock("字符串"):锁对象是公开的,外部也能锁它 ──
|
|
174
|
+
for m in _RE_LOCK_BAD.finditer(code):
|
|
175
|
+
ln = _line_of(lens, m.start())
|
|
176
|
+
target = m.group(1)
|
|
177
|
+
why = ('字符串是驻留的,全进程共享同一个实例,不相干的代码可能锁到同一把锁'
|
|
178
|
+
if target.startswith('"') else
|
|
179
|
+
'这个对象外部拿得到,别的代码也能锁它,很容易凑成死锁')
|
|
180
|
+
add(out, ctx, 'logic.cs-lock-public', 'error', lens, ln,
|
|
181
|
+
f'`lock({target})` —— {why}',
|
|
182
|
+
'用一个私有的专用锁对象:`private readonly object _gate = new();` 然后 `lock (_gate)`',
|
|
183
|
+
_unit_of(lens, ln))
|
|
184
|
+
|
|
185
|
+
# ── 静态 Random:它不是线程安全的,并发下会一起返回 0 ──
|
|
186
|
+
for f in lens.fields:
|
|
187
|
+
if 'static' not in f['modifiers']:
|
|
188
|
+
continue
|
|
189
|
+
base = f['type'].split('<')[0].strip()
|
|
190
|
+
if base == 'Random':
|
|
191
|
+
add(out, ctx, 'logic.cs-static-random', 'error', lens, f['line'],
|
|
192
|
+
f'`static Random {f["name"]}` —— Random 不是线程安全的,'
|
|
193
|
+
'多线程同时调用会破坏内部状态,典型症状是一起返回 0',
|
|
194
|
+
'.NET 6+ 直接用 `Random.Shared`(线程安全);'
|
|
195
|
+
'旧版本用 `[ThreadStatic]` 或 ThreadLocal 各线程一份', f['owner'])
|
|
196
|
+
|
|
197
|
+
# ── Parse 不带 IFormatProvider:小数点在某些区域设置下是逗号 ──
|
|
198
|
+
for m in _RE_PARSE_NO_CULTURE.finditer(code):
|
|
199
|
+
ln = _line_of(lens, m.start())
|
|
200
|
+
line_txt = lens.line(ln)
|
|
201
|
+
if re.search(r'(CultureInfo|InvariantCulture|IFormatProvider)', line_txt):
|
|
202
|
+
continue
|
|
203
|
+
add(out, ctx, 'logic.cs-parse-no-culture', 'info', lens, ln,
|
|
204
|
+
f'`{m.group(1)}` 没有指定 IFormatProvider —— 会跟随当前线程的区域设置,'
|
|
205
|
+
'而在德语、法语等地区小数点是逗号,`"1.5"` 会解析成 15',
|
|
206
|
+
'解析机器数据(配置、接口、文件)一律传 `CultureInfo.InvariantCulture`;'
|
|
207
|
+
'只有面向用户输入时才用当前区域', _unit_of(lens, ln))
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
# ── LINQ / EF Core / 资源 ──────────────────────────────────────────────────
|
|
211
|
+
# 需要显式释放的类型:不进 using 就等异常路径上的泄漏
|
|
212
|
+
_CS_DISPOSABLE = (
|
|
213
|
+
'FileStream', 'StreamReader', 'StreamWriter', 'MemoryStream', 'SqlConnection',
|
|
214
|
+
'SqlCommand', 'HttpResponseMessage', 'CancellationTokenSource', 'TransactionScope',
|
|
215
|
+
'BinaryReader', 'BinaryWriter', 'StringWriter', 'StringReader', 'Process',
|
|
216
|
+
)
|
|
217
|
+
_RE_NEW_DISPOSABLE = re.compile(
|
|
218
|
+
r'\b(?:var|[\w.<>]+)\s+(\w+)\s*=\s*new\s+(?:[\w.]+\.)?(' + '|'.join(_CS_DISPOSABLE) + r')\s*[({]')
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def check_csharp_linq_and_ef(lens, ctx, out, add):
|
|
222
|
+
"""LINQ 的延迟执行、EF Core 的用法、以及需要释放的资源。"""
|
|
223
|
+
code = lens.code
|
|
224
|
+
|
|
225
|
+
# ── .Count() > 0:为了知道「有没有」,把整个序列数了一遍 ──
|
|
226
|
+
for m in _RE_COUNT_COMPARE.finditer(code):
|
|
227
|
+
ln = _line_of(lens, m.start())
|
|
228
|
+
add(out, ctx, 'logic.cs-count-vs-any', 'info', lens, ln,
|
|
229
|
+
'`.Count() > 0` 只是想知道「有没有元素」,却把整个序列完整遍历了一遍 —— '
|
|
230
|
+
'数据库查询上更糟:会真的发一条 COUNT 出去',
|
|
231
|
+
'用 `.Any()`:它拿到第一个元素就返回。判空则用 `!collection.Any()`',
|
|
232
|
+
_unit_of(lens, ln))
|
|
233
|
+
|
|
234
|
+
# ── 延迟查询被多次枚举:每一次都重新执行一遍(数据库上就是多发几条 SQL) ──
|
|
235
|
+
for m in _RE_LINQ_DEFERRED.finditer(code):
|
|
236
|
+
var = m.group(1)
|
|
237
|
+
tail = code[m.end():m.end() + 2500]
|
|
238
|
+
# _RE_ENUMERATE 是从 `.` 开始匹配的,所以 x.start() 之前的那一段以**变量名**结尾,
|
|
239
|
+
# 不是以点结尾——判据里多要一个 `\.` 就永远匹配不上,规则静默不响。
|
|
240
|
+
uses = [x for x in _RE_ENUMERATE.finditer(tail)
|
|
241
|
+
if re.search(rf'(?<![\w.]){re.escape(var)}\s*$', tail[:x.start()])]
|
|
242
|
+
# 只在「明确枚举两次以上」时才报——猜不准就不报
|
|
243
|
+
if len(uses) < 2:
|
|
244
|
+
continue
|
|
245
|
+
ln = _line_of(lens, m.start())
|
|
246
|
+
add(out, ctx, 'logic.cs-linq-multiple-enumeration', 'warn', lens, ln,
|
|
247
|
+
f'`{var}` 是延迟执行的查询,后面被枚举了 {len(uses)} 次 —— '
|
|
248
|
+
'**每一次都会重新跑一遍**:内存集合是重复遍历,EF 查询则是重复发 SQL',
|
|
249
|
+
f'先物化一次:`var {var}List = {var}.ToList();`,之后都用它。'
|
|
250
|
+
'想保持延迟就确保只枚举一次', _unit_of(lens, ln))
|
|
251
|
+
|
|
252
|
+
# ── 需要释放的资源不用 using ──
|
|
253
|
+
for m in _RE_NEW_DISPOSABLE.finditer(code):
|
|
254
|
+
ln = _line_of(lens, m.start())
|
|
255
|
+
line_txt = lens.line(ln)
|
|
256
|
+
if re.search(r'\busing\b|\breturn\b', line_txt):
|
|
257
|
+
continue # using 声明/语句,或造出来交给调用方
|
|
258
|
+
u = lens.unit_at(ln)
|
|
259
|
+
if u and re.search(rf'\b{re.escape(m.group(1))}\s*\.\s*Dispose\s*\(', u.body):
|
|
260
|
+
continue # 显式 Dispose 了
|
|
261
|
+
add(out, ctx, 'logic.cs-disposable-no-using', 'warn', lens, ln,
|
|
262
|
+
f'`{m.group(2)}` 创建后没有 using —— 异常路径上不会被释放,'
|
|
263
|
+
'文件句柄、数据库连接、套接字都会一直占着',
|
|
264
|
+
f'写成 `using var {m.group(1)} = new {m.group(2)}(…);`(C# 8+ 的 using 声明),'
|
|
265
|
+
'或者用 `using (…) {{ … }}` 块', _unit_of(lens, ln))
|
|
266
|
+
|
|
267
|
+
# ── FromSqlRaw 拼接:参数被原样拼进 SQL ──
|
|
268
|
+
for m in _RE_SQL_RAW.finditer(code):
|
|
269
|
+
ln = _line_of(lens, m.start())
|
|
270
|
+
line_txt = lens.line(ln)
|
|
271
|
+
interpolated = bool(re.search(r'\$"', line_txt)) or bool(re.search(r'"\s*\+|\+\s*"', line_txt))
|
|
272
|
+
if not interpolated:
|
|
273
|
+
continue
|
|
274
|
+
add(out, ctx, 'logic.cs-sql-raw-concat', 'error', lens, ln,
|
|
275
|
+
f'`{m.group(1)}` 里拼接了变量 —— 参数被原样放进 SQL,这是注入入口',
|
|
276
|
+
'用参数化重载:`FromSqlRaw("… WHERE Id = {0}", id)`,'
|
|
277
|
+
'或者 `FromSqlInterpolated($"… WHERE Id = {id}")`(它会把插值转成参数,'
|
|
278
|
+
'注意与普通的 `$"..."` 拼接完全不同)', _unit_of(lens, ln))
|
|
279
|
+
|
|
280
|
+
# ── DbContext 注册成 Singleton:它不是线程安全的,还会一直持有变更跟踪 ──
|
|
281
|
+
for m in _RE_DI_SINGLETON.finditer(code):
|
|
282
|
+
types = m.group(1)
|
|
283
|
+
if not re.search(r'(DbContext|HttpClient)\b', types):
|
|
284
|
+
continue
|
|
285
|
+
ln = _line_of(lens, m.start())
|
|
286
|
+
what = 'DbContext' if 'DbContext' in types else 'HttpClient'
|
|
287
|
+
why = ('DbContext **不是线程安全的**,而且会一直攒着变更跟踪的实体,内存只增不减'
|
|
288
|
+
if what == 'DbContext' else
|
|
289
|
+
'HttpClient 应该用 IHttpClientFactory 管理,直接单例会拿不到 DNS 变更')
|
|
290
|
+
add(out, ctx, 'logic.cs-di-wrong-lifetime', 'error', lens, ln,
|
|
291
|
+
f'`{what}` 注册成了 Singleton —— {why}',
|
|
292
|
+
'DbContext 用 `AddDbContext`(默认 Scoped,每个请求一个);'
|
|
293
|
+
'HttpClient 用 `AddHttpClient`', _unit_of(lens, ln))
|
|
294
|
+
|
|
295
|
+
# ── 循环里字符串拼接 ──
|
|
296
|
+
for u in lens.functions():
|
|
297
|
+
for lm in re.finditer(r'\b(?:foreach|for|while)\s*\([^)]*\)\s*\{', u.body):
|
|
298
|
+
blk = _block_after(u.body, lm.end() - 1)
|
|
299
|
+
hit = re.search(r'\b(\w+)\s*\+=\s*(?!\d)', blk)
|
|
300
|
+
if not hit:
|
|
301
|
+
continue
|
|
302
|
+
name = hit.group(1)
|
|
303
|
+
# 只在能确认它是 string 时才报:`sum += x` 这种数值累加是完全正常的写法
|
|
304
|
+
declared = rf'\bstring\s+{re.escape(name)}\b|\bvar\s+{re.escape(name)}\s*=\s*(?:""|string\.Empty)'
|
|
305
|
+
if not re.search(declared, u.body[:lm.start()]):
|
|
306
|
+
continue
|
|
307
|
+
ln = u.line_at(lm.start())
|
|
308
|
+
add(out, ctx, 'logic.cs-string-concat-loop', 'warn', lens, ln,
|
|
309
|
+
f'循环里对 string `{name}` 做 `+=` —— C# 的 string 是不可变的,'
|
|
310
|
+
'每次拼接都新建一个对象并复制全部内容,循环一多就是 O(n²)',
|
|
311
|
+
'循环外建 `StringBuilder`,循环里 `Append`,出循环 `ToString()`', u.label)
|
|
312
|
+
|
|
313
|
+
# ── 测试方法写成 async void:测试框架不会等它,断言失败也不会让用例变红 ──
|
|
314
|
+
if lens.is_test:
|
|
315
|
+
for u in lens.functions():
|
|
316
|
+
attrs = ' '.join(str(a) for a in u.decorators)
|
|
317
|
+
if not re.search(r'\[(Fact|Theory|Test|TestMethod|TestCase)', attrs):
|
|
318
|
+
continue
|
|
319
|
+
if 'async' in set(u.modifiers) and (u.ret_type or '').strip() == 'void':
|
|
320
|
+
add(out, ctx, 'test.cs-async-void-test', 'error', lens, u.start,
|
|
321
|
+
f'测试 `{u.label}` 是 `async void` —— 测试框架拿不到 Task,'
|
|
322
|
+
'**不会等它跑完**:断言还没执行用例就算通过了',
|
|
323
|
+
'改成 `async Task`。这条特别隐蔽,因为用例会一直是绿的', u.label)
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
# ── 与 dev-csharp「常见坑 TOP / DI 生命周期」对齐的几条 ────────────────────
|
|
327
|
+
_RE_NEW_HTTPCLIENT = re.compile(r'\bnew\s+HttpClient\s*\(')
|
|
328
|
+
_RE_CASE_COMPARE = re.compile(
|
|
329
|
+
r'\.\s*To(?:Lower|Upper)(?:Invariant)?\s*\(\s*\)\s*(?:==|!=|\.\s*Equals\s*\()'
|
|
330
|
+
r'|(?:==|!=)\s*\w+\s*\.\s*To(?:Lower|Upper)(?:Invariant)?\s*\(\s*\)')
|
|
331
|
+
_RE_EVENT_SUB = re.compile(r'\b([\w.]+)\s*\+=\s*(?:new\s+\w+\s*\(\s*)?(\w+)\s*(?:\)\s*)?;')
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def check_csharp_pitfalls(lens, ctx, out, add):
|
|
335
|
+
"""dev-csharp 技能里「评审必查」的那几条,改由机器每次都查。"""
|
|
336
|
+
code = lens.code
|
|
337
|
+
|
|
338
|
+
# ── 手 new HttpClient:长持会耗尽 socket,频繁 new 则 DNS 不刷新,两头都是坑 ──
|
|
339
|
+
for m in _RE_NEW_HTTPCLIENT.finditer(code):
|
|
340
|
+
ln = _line_of(lens, m.start())
|
|
341
|
+
add(out, ctx, 'logic.cs-new-httpclient', 'warn', lens, ln,
|
|
342
|
+
'直接 `new HttpClient()` —— 长期持有会耗尽 socket(连接处于 TIME_WAIT 不释放),'
|
|
343
|
+
'每次新建又拿不到 DNS 变更,两头都是坑',
|
|
344
|
+
'用 `IHttpClientFactory`(`services.AddHttpClient<T>()` 后注入 HttpClient),'
|
|
345
|
+
'它替你管理连接池与生命周期', _unit_of(lens, ln))
|
|
346
|
+
|
|
347
|
+
# ── ToLower() 之后比较:多分配一个字符串,还踩土耳其 i ──
|
|
348
|
+
for m in _RE_CASE_COMPARE.finditer(code):
|
|
349
|
+
ln = _line_of(lens, m.start())
|
|
350
|
+
add(out, ctx, 'logic.cs-case-compare', 'info', lens, ln,
|
|
351
|
+
'先 `ToLower()/ToUpper()` 再比较 —— 每次都多分配一个字符串,'
|
|
352
|
+
'而且在土耳其语区域 `I` 的小写不是 `i`,这类比较会得到意外结果',
|
|
353
|
+
'用 `string.Equals(a, b, StringComparison.OrdinalIgnoreCase)`。'
|
|
354
|
+
'文件名、协议头、标识符一律用 Ordinal 系列,别跟随区域设置', _unit_of(lens, ln))
|
|
355
|
+
|
|
356
|
+
# ── 订阅事件却没有对应的退订:长命对象订阅短命对象的事件 = 泄漏 ──
|
|
357
|
+
subscribed = {}
|
|
358
|
+
for m in _RE_EVENT_SUB.finditer(code):
|
|
359
|
+
target, handler = m.group(1), m.group(2)
|
|
360
|
+
if not re.search(r'(Changed|Click|Closed|Loaded|Received|Completed|Elapsed|Tick|Event)$',
|
|
361
|
+
target.split('.')[-1]):
|
|
362
|
+
continue # 只认名字明确是事件的,`sum += x` 这类一律不碰
|
|
363
|
+
subscribed[(target, handler)] = _line_of(lens, m.start())
|
|
364
|
+
for (target, handler), ln in subscribed.items():
|
|
365
|
+
if re.search(rf'{re.escape(target)}\s*-=\s*', code):
|
|
366
|
+
continue
|
|
367
|
+
add(out, ctx, 'logic.cs-event-no-unsubscribe', 'warn', lens, ln,
|
|
368
|
+
f'订阅了事件 `{target} += {handler}` 但全文没有对应的 `-=` —— '
|
|
369
|
+
'事件源会一直持有订阅者的引用,订阅者永远不会被回收',
|
|
370
|
+
'在 Dispose / 页面卸载 / 组件销毁时 `-=` 退订;'
|
|
371
|
+
'订阅者比事件源短命时尤其要注意', _unit_of(lens, ln))
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
# 代码自动修复:只修「改法唯一、不改变行为、不需要理解业务」的那几类。
|
|
2
|
+
#
|
|
3
|
+
# ── 这份清单为什么这么短 ──
|
|
4
|
+
# 把「能标成自动修」的门槛放低,是这类工具最常见的坑:
|
|
5
|
+
#
|
|
6
|
+
# `a == b` → `a === b` 看着安全,其实**会改变行为**:"1" == 1 为真,"1" === 1 为假。
|
|
7
|
+
# 代码若正依赖松散比较,一键修完当场出故障。
|
|
8
|
+
# `count = count;` 删掉 看着无害,但它多半是「漏写了 this.」的现场证据。
|
|
9
|
+
# 删掉等于把 bug 的线索抹了,问题还在,只是再也查不到。
|
|
10
|
+
#
|
|
11
|
+
# 所以这两条都**不在**可修清单里——它们要报给人,由人判断。
|
|
12
|
+
# 一个会自动改坏代码的修复器,比没有修复器危险得多:人会停止复核它的改动。
|
|
13
|
+
#
|
|
14
|
+
# 真正收进来的只有五类,共同点是「改完行为要么不变、要么是恢复本该有的行为」:
|
|
15
|
+
# test.only-left 删 .only —— 恢复「跑全部测试」这个本该有的行为
|
|
16
|
+
# readability.trailing-space 去行尾空白 —— 不进字符串内部,纯空白
|
|
17
|
+
# comment.commented-code 删被注释掉的死代码 —— 注释不参与执行
|
|
18
|
+
# structure.debug-left 删调试输出 —— 仅限单行、参数里没有函数调用的那种
|
|
19
|
+
# logic.empty-finally 删空 finally 块 —— 空块本就没有任何作用
|
|
20
|
+
#
|
|
21
|
+
# 默认 dry-run(只打印将要做的改动),加 --write 才落盘。
|
|
22
|
+
|
|
23
|
+
import os
|
|
24
|
+
import re
|
|
25
|
+
import sys
|
|
26
|
+
|
|
27
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
28
|
+
import code_check as C # noqa: E402
|
|
29
|
+
import code_lens as L # noqa: E402
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _in_string(lens, lineno):
|
|
33
|
+
"""这一行的行尾是否落在字符串字面量内部。
|
|
34
|
+
|
|
35
|
+
去行尾空白唯一会出事的地方就是多行字符串——模板串/三引号里的尾随空格
|
|
36
|
+
是**内容**,删了就改了程序的输出。掩码视图能精确判断这件事。
|
|
37
|
+
"""
|
|
38
|
+
code_line = lens.code_line(lineno)
|
|
39
|
+
raw_line = lens.line(lineno)
|
|
40
|
+
# 掩码视图里该行尾部若被抹空而原文非空白,说明尾部在字符串/注释内
|
|
41
|
+
return len(code_line.rstrip()) < len(raw_line.rstrip()) and not raw_line.strip().startswith(('//', '#', '*'))
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# 单行、且参数里没有函数调用的调试输出才删——`console.log(compute())` 删掉会丢掉副作用
|
|
45
|
+
_RE_SIMPLE_DEBUG = re.compile(
|
|
46
|
+
r'^\s*(?:console\.(?:log|debug|dir|trace)|System\.out\.print\w*|System\.err\.print\w*|print)'
|
|
47
|
+
r'\s*\([^()]*\)\s*;?\s*$')
|
|
48
|
+
_RE_EMPTY_FINALLY = re.compile(r'\s*finally\s*\{\s*\}')
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def plan_fixes(lens, issues):
|
|
52
|
+
"""把可修的问题翻译成具体的行编辑。
|
|
53
|
+
|
|
54
|
+
返回 [(lineno, new_text_or_None, rule, why)];new_text 为 None 表示删除该行。
|
|
55
|
+
只规划不执行,便于 dry-run 先给人看。
|
|
56
|
+
"""
|
|
57
|
+
edits = []
|
|
58
|
+
for it in issues:
|
|
59
|
+
if it.rule not in C.FIXABLE:
|
|
60
|
+
continue
|
|
61
|
+
ln = it.line
|
|
62
|
+
raw = lens.line(ln)
|
|
63
|
+
|
|
64
|
+
if it.rule == 'readability.trailing-space':
|
|
65
|
+
# 整文件一起处理(规则只报了第一处)
|
|
66
|
+
for i, text in enumerate(lens.lines, 1):
|
|
67
|
+
if text != text.rstrip() and not _in_string(lens, i):
|
|
68
|
+
edits.append((i, text.rstrip(), it.rule, '去掉行尾空白'))
|
|
69
|
+
continue
|
|
70
|
+
|
|
71
|
+
if it.rule == 'test.only-left':
|
|
72
|
+
new = re.sub(r'\b(it|test|describe|context)\s*\.\s*only\s*\(', r'\1(', raw)
|
|
73
|
+
new = re.sub(r'\bfdescribe\s*\(', 'describe(', new)
|
|
74
|
+
new = re.sub(r'\bfit\s*\(', 'it(', new)
|
|
75
|
+
if new != raw:
|
|
76
|
+
edits.append((ln, new, it.rule, '删掉 .only,恢复跑全部用例'))
|
|
77
|
+
continue
|
|
78
|
+
|
|
79
|
+
if it.rule == 'structure.debug-left':
|
|
80
|
+
# 规则只报了首行,这里把该文件里所有「简单调试行」一起收掉
|
|
81
|
+
for i, text in enumerate(lens.lines, 1):
|
|
82
|
+
if _RE_SIMPLE_DEBUG.match(lens.code_line(i)) and text.strip():
|
|
83
|
+
edits.append((i, None, it.rule, '删掉调试输出'))
|
|
84
|
+
continue
|
|
85
|
+
|
|
86
|
+
if it.rule == 'logic.empty-finally':
|
|
87
|
+
new = _RE_EMPTY_FINALLY.sub('', raw)
|
|
88
|
+
if new.strip():
|
|
89
|
+
edits.append((ln, new.rstrip(), it.rule, '删掉空 finally 块'))
|
|
90
|
+
else:
|
|
91
|
+
edits.append((ln, None, it.rule, '删掉空 finally 块'))
|
|
92
|
+
continue
|
|
93
|
+
|
|
94
|
+
if it.rule == 'comment.commented-code':
|
|
95
|
+
m = re.search(r'第 (\d+)–(\d+) 行', it.msg)
|
|
96
|
+
if not m:
|
|
97
|
+
continue
|
|
98
|
+
for i in range(int(m.group(1)), int(m.group(2)) + 1):
|
|
99
|
+
text = lens.line(i).strip()
|
|
100
|
+
if text.startswith(('//', '#')) or text.startswith('*'):
|
|
101
|
+
edits.append((i, None, it.rule, '删掉被注释掉的死代码'))
|
|
102
|
+
continue
|
|
103
|
+
# 同一行只留一条编辑(删除优先),并按行号倒序——正序改会让后面的行号全部漂移
|
|
104
|
+
by_line = {}
|
|
105
|
+
for ln, new, rule, why in edits:
|
|
106
|
+
if ln not in by_line or new is None:
|
|
107
|
+
by_line[ln] = (ln, new, rule, why)
|
|
108
|
+
return sorted(by_line.values(), key=lambda x: -x[0])
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def apply_fixes(path, rules=None, write=False):
|
|
112
|
+
"""对一个文件跑「体检 → 规划 → 应用 → 复检」的闭环。
|
|
113
|
+
|
|
114
|
+
复检是这里的重点:修完必须再体检一遍,断言这些规则真的消失了。
|
|
115
|
+
只做不验的修复器,会在某次正则改动后悄悄变成空转,而报告照样说「已修复 N 处」。
|
|
116
|
+
"""
|
|
117
|
+
lens = L.read_lens(path)
|
|
118
|
+
issues, _, _ = C.check_files([path])
|
|
119
|
+
if rules:
|
|
120
|
+
issues = [i for i in issues if i.rule in set(rules)]
|
|
121
|
+
edits = plan_fixes(lens, issues)
|
|
122
|
+
if not edits:
|
|
123
|
+
return {'path': path, 'edits': [], 'changed': False, 'remaining': None}
|
|
124
|
+
|
|
125
|
+
lines = list(lens.lines)
|
|
126
|
+
for ln, new, _rule, _why in edits: # 已按行号倒序
|
|
127
|
+
if not (1 <= ln <= len(lines)):
|
|
128
|
+
continue
|
|
129
|
+
if new is None:
|
|
130
|
+
del lines[ln - 1]
|
|
131
|
+
else:
|
|
132
|
+
lines[ln - 1] = new
|
|
133
|
+
new_text = '\n'.join(lines)
|
|
134
|
+
|
|
135
|
+
result = {
|
|
136
|
+
'path': path,
|
|
137
|
+
'edits': [{'line': ln, 'rule': r, 'why': w, 'action': '删除' if n is None else '改写',
|
|
138
|
+
'before': lens.line(ln).strip()[:120], 'after': (n or '').strip()[:120]}
|
|
139
|
+
for ln, n, r, w in sorted(edits, key=lambda x: x[0])],
|
|
140
|
+
'changed': new_text != lens.text,
|
|
141
|
+
}
|
|
142
|
+
# 复检:在**修改后的内容**上重新体检(不是在旧 lens 上),确认这些规则确实没了
|
|
143
|
+
fixed_rules = {r for _, _, r, _ in edits}
|
|
144
|
+
lens2 = L.read_lens(path, text=new_text)
|
|
145
|
+
issues2 = []
|
|
146
|
+
ctx = C.Ctx()
|
|
147
|
+
for fn in (C.check_logic, C.check_comment, C.check_readability,
|
|
148
|
+
C.check_structure, C.check_test, C.check_security):
|
|
149
|
+
fn(lens2, ctx, issues2)
|
|
150
|
+
still = sorted({i.rule for i in issues2} & fixed_rules)
|
|
151
|
+
result['remaining'] = still
|
|
152
|
+
result['parse_ok_after'] = lens2.parse_ok
|
|
153
|
+
if write and result['changed']:
|
|
154
|
+
if not lens2.parse_ok and lens.parse_ok:
|
|
155
|
+
# 修完反而解析不了了:绝不落盘。宁可不修,也不能交出一个更坏的文件。
|
|
156
|
+
result['aborted'] = '修改后文件无法解析,已放弃写入'
|
|
157
|
+
return result
|
|
158
|
+
with open(path, 'w', encoding='utf-8') as f:
|
|
159
|
+
f.write(new_text)
|
|
160
|
+
result['written'] = True
|
|
161
|
+
return result
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def main(argv):
|
|
165
|
+
import json
|
|
166
|
+
args = [a for a in argv if not a.startswith('--')]
|
|
167
|
+
flags = {a.split('=')[0]: (a.split('=', 1)[1] if '=' in a else True) for a in argv if a.startswith('--')}
|
|
168
|
+
target = args[0] if args else '.'
|
|
169
|
+
paths = L.walk_files(target) if os.path.isdir(target) else [target]
|
|
170
|
+
rules = flags['--rules'].split(',') if isinstance(flags.get('--rules'), str) else None
|
|
171
|
+
write = bool(flags.get('--write'))
|
|
172
|
+
results = [apply_fixes(p, rules, write) for p in paths]
|
|
173
|
+
results = [r for r in results if r['edits']]
|
|
174
|
+
if flags.get('--json'):
|
|
175
|
+
print(json.dumps({'write': write, 'files': results,
|
|
176
|
+
'fixable_rules': sorted(C.FIXABLE)}, ensure_ascii=False))
|
|
177
|
+
return 0
|
|
178
|
+
if not results:
|
|
179
|
+
print('没有可自动修复的问题。\n'
|
|
180
|
+
'(可自动修的只有这几类:' + '、'.join(sorted(C.FIXABLE)) + ';\n'
|
|
181
|
+
' 其余问题——逻辑、注释与实现不符、缺测试、安全——都需要你自己判断怎么改。)')
|
|
182
|
+
return 0
|
|
183
|
+
total = sum(len(r['edits']) for r in results)
|
|
184
|
+
print(f'{"已修复" if write else "将修复"} {total} 处,涉及 {len(results)} 个文件'
|
|
185
|
+
+ ('' if write else '(dry-run,加 --write 才真正落盘)'))
|
|
186
|
+
for r in results:
|
|
187
|
+
print(f'\n{r["path"]}')
|
|
188
|
+
for e in r['edits']:
|
|
189
|
+
print(f' L{e["line"]} [{e["rule"]}] {e["why"]}')
|
|
190
|
+
print(f' - {e["before"]}')
|
|
191
|
+
if e['action'] != '删除':
|
|
192
|
+
print(f' + {e["after"]}')
|
|
193
|
+
if r.get('aborted'):
|
|
194
|
+
print(f' ⚠ {r["aborted"]}')
|
|
195
|
+
elif r['remaining']:
|
|
196
|
+
print(f' ⚠ 复检发现这些规则仍在响:{"、".join(r["remaining"])}(修复不完整,请人工确认)')
|
|
197
|
+
return 0
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
if __name__ == '__main__':
|
|
201
|
+
sys.exit(main(sys.argv[1:]))
|