@ohos-cpf/3rdloop 0.0.8 → 0.0.10
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/README.md +43 -0
- package/lib/cli.js +38 -5
- package/lib/doctor.js +111 -10
- package/lib/knowledge.js +440 -0
- package/lib/mcp.js +498 -0
- package/package.json +6 -2
- package/vendor/Archive/data/.index/meta.json +7 -0
- package/vendor/Archive/data/.index/search.db +0 -0
- package/vendor/Archive/data/docs/experiences/arkts-check-tool-pitfalls-baseline-comparison.md +13 -0
- package/vendor/Archive/data/docs/experiences/arkts-deep-path-import-recognition.md +31 -0
- package/vendor/Archive/data/docs/experiences/arkts-demo-coverage-entry-file-case-mismatch.md +25 -0
- package/vendor/Archive/data/docs/experiences/arkts-demo-coverage-import-trailing-slash.md +20 -0
- package/vendor/Archive/data/docs/experiences/arkts-library-source-files-may-be-ts.md +18 -0
- package/vendor/Archive/data/docs/experiences/arkts-typedarray-buffer-interface-cast-compile-fixes.md +36 -0
- package/vendor/Archive/data/docs/experiences/arkts-xts-compile-error-clustering-fix.md +24 -0
- package/vendor/Archive/data/docs/experiences/arkts-xts-doc-expected-value-runtime-probe.md +35 -0
- package/vendor/Archive/data/docs/experiences/arkts-xts-fixture-rawfile-testability-injection.md +33 -0
- package/vendor/Archive/data/docs/experiences/code-linter-chinese-comment-false-positive.md +16 -0
- package/vendor/Archive/data/docs/experiences/esobject-new-expression-variable-tracking.md +33 -0
- package/vendor/Archive/data/docs/experiences/filesystem-case-sensitivity-module-resolution.md +31 -0
- package/vendor/Archive/data/docs/experiences/gitcode-clone-redirect-warning-normal.md +13 -0
- package/vendor/Archive/data/docs/experiences/kn_msgvq1w585ec8319.md +77 -0
- package/vendor/Archive/data/docs/experiences/kn_msovnm6rd46de72c.md +98 -0
- package/vendor/Archive/data/docs/experiences/kn_mspt8vntfe708576.md +87 -0
- package/vendor/Archive/data/docs/experiences/kn_mspu1ei77adbbb48.md +133 -0
- package/vendor/Archive/data/docs/experiences/kn_mspubgak8d30bfce.md +128 -0
- package/vendor/Archive/data/docs/experiences/kn_mspug2de3cb1f7c3.md +115 -0
- package/vendor/Archive/data/docs/experiences/kn_skill_opt_001.md +38 -0
- package/vendor/Archive/data/docs/experiences/loop-artifacts-verification-evidence-triangle.md +19 -0
- package/vendor/Archive/data/docs/experiences/macos-bash-replace-build-ps1.md +19 -0
- package/vendor/Archive/data/docs/experiences/macos-bsd-diff-q-illegal-option.md +15 -0
- package/vendor/Archive/data/docs/experiences/migrate-cjs-compatible-api-version-map-limit.md +17 -0
- package/vendor/Archive/data/docs/experiences/mp3agic-arkts-library-behavior-quirks.md +21 -0
- package/vendor/Archive/data/docs/experiences/ohos-face-detection-scan-api-mapping.md +15 -0
- package/vendor/Archive/data/docs/experiences/pr-metadata-and-diff-integrity-verification.md +12 -0
- package/vendor/Archive/data/docs/experiences/rn-coverage-index-signature-pseudo-member-false-negative.md +32 -0
- package/vendor/Archive/data/docs/experiences/rn-memory-check-severity-calibration.md +21 -0
- package/vendor/Archive/data/docs/experiences/rn-platform-specific-api-exclusion-evidence.md +24 -0
- package/vendor/Archive/data/docs/experiences/rn-upstream-version-diff-pitfalls.md +17 -0
- package/vendor/Archive/data/docs/experiences/rn-version-gap-risk-rating-rules.md +13 -0
- package/vendor/Archive/data/docs/experiences/rn-view-lib-native-contract-diff-fabric-spec.md +16 -0
- package/vendor/Archive/data/docs/experiences/rnoh-adapter-package-version-field-semantics.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-arkts-sqlite-rows-array-format.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-dependency-residue-scan-strategy.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-example-local-lib-dep-use-tgz.md +53 -0
- package/vendor/Archive/data/docs/experiences/rnoh-example-prereserve-rntesterlist.md +17 -0
- package/vendor/Archive/data/docs/experiences/rnoh-example-template-compatiblesdkversion-upgrade.md +29 -0
- package/vendor/Archive/data/docs/experiences/rnoh-example-turbomodule-shell-adaptation-checklist.md +41 -0
- package/vendor/Archive/data/docs/experiences/rnoh-mcp-build-project-tool-risk.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-npm-devdependencies-version-precheck.md +29 -0
- package/vendor/Archive/data/docs/experiences/rnoh-repo-baseline-version-verification.md +13 -0
- package/vendor/Archive/data/docs/experiences/rnoh-repo-upstream-directory-mapping.md +18 -0
- package/vendor/Archive/data/docs/experiences/rnoh-stateless-library-zero-leak-verification.md +31 -0
- package/vendor/Archive/data/docs/experiences/rnoh-template-npmrc-credential-removal.md +15 -0
- package/vendor/Archive/data/docs/experiences/rnoh-turbomodule-codegen-wiring-verification.md +19 -0
- package/vendor/Archive/data/docs/experiences/rnoh-turbomodule-cpp-methodmap-compatibility-check.md +20 -0
- package/vendor/Archive/data/docs/experiences/scenario-doc-vs-source-signature-diff-four-patterns.md +24 -0
- package/vendor/Archive/data/docs/experiences/static-analysis-grep-cross-validation.md +29 -0
- package/vendor/Archive/demo.js +489 -0
- package/vendor/Archive/index.js +43 -0
- package/vendor/Archive/package.json +16 -0
- package/vendor/Archive/src/constants.js +83 -0
- package/vendor/Archive/src/data/stopwords.txt +110 -0
- package/vendor/Archive/src/data/synonyms.json +25 -0
- package/vendor/Archive/src/data/terms.txt +59 -0
- package/vendor/Archive/src/db/sqlite-backend.js +312 -0
- package/vendor/Archive/src/db/sqlite-schema.js +64 -0
- package/vendor/Archive/src/engine.js +501 -0
- package/vendor/Archive/src/index/index-manager.js +203 -0
- package/vendor/Archive/src/index/index-state.js +94 -0
- package/vendor/Archive/src/index/search-text-builder.js +106 -0
- package/vendor/Archive/src/index.js +22 -0
- package/vendor/Archive/src/parser/api-identifiers.js +121 -0
- package/vendor/Archive/src/parser/api-suffixes.js +48 -0
- package/vendor/Archive/src/parser/heading-normalize.js +40 -0
- package/vendor/Archive/src/parser/markdown-splitter.js +215 -0
- package/vendor/Archive/src/search/catalog-routing.js +87 -0
- package/vendor/Archive/src/search/fts-search.js +289 -0
- package/vendor/Archive/src/search/query-normalizer.js +159 -0
- package/vendor/Archive/src/search/query-rewriter.js +72 -0
- package/vendor/Archive/src/search/search-engine.js +72 -0
- package/vendor/Archive/src/search/snippet.js +64 -0
- package/vendor/Archive/src/store/dir-store.js +103 -0
- package/vendor/Archive/src/store/doc-store.js +36 -0
- package/vendor/Archive/src/tokenizer/lexicon.js +151 -0
- package/vendor/Archive/src/tokenizer/tokenizer.js +167 -0
- package/vendor/Archive/src/types.js +86 -0
- package/vendor/Archive/src/utils/hash.js +16 -0
- package/vendor/Archive/src/utils/lock.js +73 -0
- package/vendor/Archive/src/utils/path-safety.js +96 -0
- package/vendor/Archive/src/utils/paths.js +39 -0
- package/vendor/BadCase/rn/INDEX.md +127 -0
- package/vendor/BadCase/rn/cases/KI-001-webview/350/247/206/351/242/221/346/227/240/346/263/225/346/250/252/345/261/217.md +52 -0
- package/vendor/BadCase/rn/cases/KI-002-fastImage/347/203/255/346/233/264/346/226/260/345/233/276/347/211/207/345/244/261/350/264/245.md +37 -0
- package/vendor/BadCase/rn/cases/KI-003-wechat/347/274/251/347/225/245/345/233/276/346/234/254/345/234/260/345/233/276/347/211/207.md +62 -0
- package/vendor/BadCase/rn/cases/KI-004-wechat/345/233/236/350/260/203/346/227/266/345/272/217.md +47 -0
- package/vendor/BadCase/rn/cases/KI-005-geolocation/345/257/274/345/205/245/350/267/257/345/276/204/345/220/216/347/274/200.md +31 -0
- package/vendor/BadCase/rn/cases/KI-006-SmartRefreshLayout/346/231/272/350/203/275/346/214/207/351/222/210/345/264/251/346/272/203.md +55 -0
- package/vendor/BadCase/rn/cases/KI-007-orientation-locker/344/274/240/346/204/237/345/231/250/345/210/235/345/247/213/345/214/226.md +58 -0
- package/vendor/BadCase/rn/cases/KI-008-orientation-locker/344/274/240/346/204/237/345/231/250/345/274/225/347/224/250/350/256/241/346/225/260.md +47 -0
- package/vendor/BadCase/rn/cases/KI-009-pager-view/345/265/214/345/245/227/346/273/221/345/212/250.md +38 -0
- package/vendor/BadCase/rn/cases/KI-010-screens/346/211/213/345/212/277/345/206/262/347/252/201.md +48 -0
- package/vendor/BadCase/rn/cases/KI-011-spring-scrollview/346/211/213/345/212/277/351/207/215/345/244/215/347/273/221/345/256/232.md +49 -0
- package/vendor/BadCase/rn/cases/KI-012-qr-camera/346/250/252/345/261/217/351/200/202/351/205/215.md +59 -0
- package/vendor/BadCase/rn/cases/KI-013-webview/346/267/261/350/211/262/346/250/241/345/274/217/350/203/214/346/231/257/350/211/262.md +37 -0
- package/vendor/BadCase/rn/cases/KI-014-amap-geolocation/345/233/236/350/260/203/347/274/272/345/244/261.md +42 -0
- package/vendor/BadCase/rn/cases/KI-015-reanimated/350/277/220/350/241/214/346/227/266/346/236/220/346/236/204/345/264/251/346/272/203.md +54 -0
- package/vendor/BadCase/rn/cases/KI-016-fs/346/226/207/344/273/266/350/257/273/345/217/226/347/274/226/347/240/201.md +58 -0
- package/vendor/BadCase/rn/cases/KI-017-blob-util/345/217/202/346/225/260/347/274/226/347/240/201/351/224/231/350/257/257.md +39 -0
- package/vendor/BadCase/rn/cases/KI-018-modal/345/274/271/347/252/227/351/227/252/347/203/201.md +53 -0
- package/vendor/BadCase/rn/cases/KI-019-code-push/351/207/215/345/220/257/351/200/273/350/276/221/345/244/261/346/225/210.md +79 -0
- package/vendor/BadCase/rn/cases/KI-020-netinfo/346/225/217/346/204/237/344/277/241/346/201/257/346/263/204/351/234/262.md +60 -0
- package/vendor/BadCase/rn/cases/KI-021-video/346/234/254/345/234/260/350/247/206/351/242/221/346/222/255/346/224/276/345/274/202/345/270/270.md +79 -0
- package/vendor/BadCase/rn/cases/KI-022-audio-recorder-player/346/236/232/344/270/276/347/274/272/345/244/261.md +38 -0
- package/vendor/BadCase/rn/cases/KI-023-svg/346/270/220/345/217/230/347/273/247/346/211/277/345/244/261/346/225/210.md +62 -0
- package/vendor/BadCase/rn/generalization-log.md +55 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-SmartRefreshLayout-32.json +328 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-SmartRefreshLayout-32.md +382 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-amap-geolocation-27.json +1351 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-amap-geolocation-27.md +1176 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-audio-recorder-player-22.json +538 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-audio-recorder-player-22.md +1488 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-blob-util-23.json +211 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-blob-util-23.md +35 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-code-push-17.json +2048 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-code-push-17.md +3866 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-fast-image-102.json +198 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-fast-image-102.md +40 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-fs-38.json +751 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-fs-38.md +883 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-geolocation-24.json +1544 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-geolocation-24.md +2598 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-modal-20.json +246 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-modal-20.md +554 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-netinfo-31.json +211 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-netinfo-31.md +54 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-45.json +1626 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-45.md +2213 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-51.json +208 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-51.md +77 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-pager-view-32.json +377 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-pager-view-32.md +174 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-qr-decode-image-camera-37.json +232 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-qr-decode-image-camera-37.md +194 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-reanimated-82.json +198 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-reanimated-82.md +67 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-87.json +198 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-87.md +38 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-93.json +208 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-93.md +34 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-spring-scrollview-38.json +222 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-spring-scrollview-38.md +56 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-svg-117.json +463 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-svg-117.md +383 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-video-66.json +464 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-video-66.md +565 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-112.json +198 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-112.md +47 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-121.json +232 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-121.md +422 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-wechat-lib-15.json +295 -0
- package/vendor/BadCase/rn/pr-info/rntpc_react-native-wechat-lib-15.md +1038 -0
- package/vendor/BadCase/rn/rules/R-01-/345/205/250/345/261/217/347/252/227/345/217/243/347/212/266/346/200/201/347/256/241/347/220/206/345/256/214/346/225/264/346/200/247.md +20 -0
- package/vendor/BadCase/rn/rules/R-02-/350/265/204/346/272/220URI/345/275/242/346/200/201/345/210/206/346/224/257/350/246/206/347/233/226.md +21 -0
- package/vendor/BadCase/rn/rules/R-03-/351/231/204/345/261/236/346/223/215/344/275/234/345/244/261/350/264/245/351/230/273/346/226/255/344/270/273/346/265/201/347/250/213.md +20 -0
- package/vendor/BadCase/rn/rules/R-04-/345/274/202/346/255/245/345/233/236/350/260/203/346/263/250/345/206/214/346/227/266/345/272/217.md +20 -0
- package/vendor/BadCase/rn/rules/R-05-/346/250/241/345/235/227/345/257/274/345/205/245/350/267/257/345/276/204/350/247/204/350/214/203.md +19 -0
- package/vendor/BadCase/rn/rules/R-06-/344/274/240/346/204/237/345/231/250/347/241/254/344/273/266/346/235/203/351/231/220/346/214/211/351/234/200/350/216/267/345/217/226.md +21 -0
- package/vendor/BadCase/rn/rules/R-07-/346/231/272/350/203/275/346/214/207/351/222/210/347/224/237/345/221/275/345/221/250/346/234/237/347/256/241/347/220/206.md +20 -0
- package/vendor/BadCase/rn/rules/R-08-/346/211/213/345/212/277/344/272/213/344/273/266/346/263/250/345/206/214/344/270/216/345/206/262/347/252/201/345/244/204/347/220/206.md +22 -0
- package/vendor/BadCase/rn/rules/R-09-/346/250/252/347/253/226/345/261/217/345/244/232/345/275/242/346/200/201/345/270/203/345/261/200/351/200/202/351/205/215.md +20 -0
- package/vendor/BadCase/rn/rules/R-10-/346/267/261/350/211/262/346/250/241/345/274/217/344/270/273/351/242/230/351/242/234/350/211/262/351/200/202/351/205/215.md +19 -0
- package/vendor/BadCase/rn/rules/R-11-/346/216/245/345/217/243/345/233/236/350/260/203/345/256/236/347/216/260/345/256/214/346/225/264/346/200/247.md +20 -0
- package/vendor/BadCase/rn/rules/R-12-/345/216/237/347/224/237/350/277/220/350/241/214/346/227/266/350/265/204/346/272/220/346/236/220/346/236/204/346/227/266/345/272/217.md +20 -0
- package/vendor/BadCase/rn/rules/R-13-/346/226/207/344/273/266/346/225/260/346/215/256/347/274/226/347/240/201/344/270/216/346/240/274/345/274/217/345/205/274/345/256/271.md +21 -0
- package/vendor/BadCase/rn/rules/R-14-/345/212/250/347/224/273/345/210/235/345/247/213/345/214/226/346/227/266/345/272/217/344/270/216/347/212/266/346/200/201/345/220/214/346/255/245.md +20 -0
- package/vendor/BadCase/rn/rules/R-15-/347/212/266/346/200/201/346/214/201/344/271/205/345/214/226/344/270/216/351/207/215/345/220/257/351/200/273/350/276/221/344/270/200/350/207/264/346/200/247.md +20 -0
- package/vendor/BadCase/rn/rules/R-16-/346/225/217/346/204/237/344/277/241/346/201/257/346/227/245/345/277/227/346/263/204/351/234/262.md +19 -0
- package/vendor/BadCase/rn/rules/R-17-/345/252/222/344/275/223/350/265/204/346/272/220/347/224/237/345/221/275/345/221/250/346/234/237/344/270/216/351/224/231/350/257/257/345/244/204/347/220/206.md +21 -0
- package/vendor/BadCase/rn/rules/R-18-/350/203/275/345/212/233/346/236/232/344/270/276/351/205/215/347/275/256/345/256/214/346/225/264/346/200/247.md +19 -0
- package/vendor/BadCase/rn/rules/R-19-/345/233/276/345/275/242/346/270/262/346/237/223/345/261/236/346/200/247/347/273/247/346/211/277/344/270/216/351/207/215/347/275/256.md +19 -0
- package/vendor/MCP/README.md +490 -0
- package/vendor/MCP/config.json +29 -0
- package/vendor/MCP/package.json +23 -0
- package/vendor/MCP/scripts/analyze-demo-coverage.cjs +1203 -0
- package/vendor/MCP/scripts/analyze-xts-coverage.cjs +1245 -0
- package/vendor/MCP/scripts/deveco_docs.js +247 -0
- package/vendor/MCP/scripts/extract-interfaces.cjs +1487 -0
- package/vendor/MCP/scripts/flutter-analyze-demo-coverage.cjs +666 -0
- package/vendor/MCP/scripts/flutter-analyze-xts-coverage.cjs +684 -0
- package/vendor/MCP/scripts/flutter-extract-interfaces.cjs +1251 -0
- package/vendor/MCP/scripts/flutter-render-spec-doc.cjs +351 -0
- package/vendor/MCP/scripts/render-spec-doc.cjs +347 -0
- package/vendor/MCP/scripts/rn-analyze-demo-coverage.cjs +645 -0
- package/vendor/MCP/scripts/rn-analyze-test-coverage.cjs +614 -0
- package/vendor/MCP/scripts/rn-extract-interfaces.cjs +1462 -0
- package/vendor/MCP/scripts/rn-render-spec-doc.cjs +327 -0
- package/vendor/MCP/scripts/rn-scan-exports.cjs +1336 -0
- package/vendor/MCP/src/client/MCPClientPool.js +220 -0
- package/vendor/MCP/src/config/ConfigLoader.js +189 -0
- package/vendor/MCP/src/executor/ScriptExecutor.js +110 -0
- package/vendor/MCP/src/gateway/Gateway.js +451 -0
- package/vendor/MCP/src/index.js +87 -0
- package/vendor/MCP/src/library/KBToolProvider.js +229 -0
- package/vendor/MCP/src/library/logger.js +60 -0
- package/vendor/MCP/src/registry/ScriptRegistry.js +238 -0
- package/vendor/MCP/src/registry/ToolRegistry.js +111 -0
- package/vendor/MCP/src/types.js +115 -0
- package/vendor/Server/Skills/arkts-library-test-coverage-check/references/MCP_TOOL_USAGE.md +4 -2
- package/vendor/Server/Skills/arkts-library-xts-coverage/SKILL.md +3 -3
- package/vendor/Server/Skills/knowledge-import/SKILL.md +1 -1
- package/vendor/Server/Skills/knowledge-import/references/MCP_TOOL_USAGE.md +12 -4
- package/vendor/Server/Skills/ohos-lib-pr-push/SKILL.md +5 -4
- package/vendor/Server/Skills/rn-library-issue-generalizer/SKILL.md +2 -2
- package/vendor/Server/Skills/rn-library-known-issue-check/SKILL.md +2 -2
- package/vendor/Server/Skills/rnoh-lib-demo-coverage/SKILL.md +1 -2
- package/vendor/Server/Skills/rnoh-lib-test-align/references/port-test-demo-guide.md +1 -1
- package/vendor/Server/Skills/rnoh-lib-xts-coverage/SKILL.md +1 -2
- package/vendor/Server/package.json +5 -1
- package/vendor/VERSION +3 -3
|
@@ -0,0 +1,1251 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* @tool flutter_extract_interfaces
|
|
4
|
+
* @description 从 Flutter 三方库源码目录提取接口规格。基于 Dart 正则解析,提取类/枚举/mixin/extension/函数/变量的公开接口,输出 JSON 或直接传给下游覆盖率分析。用于分析 Flutter 库的完整 API 表面。
|
|
5
|
+
* @param libroot: string - 库源码根目录路径(包含 lib/ 子目录和 pubspec.yaml)
|
|
6
|
+
* @param output: string - 输出 JSON 文件路径(可选,不指定则输出 JSON 到 stdout)
|
|
7
|
+
* @hidden
|
|
8
|
+
* flutter-extract-interfaces.cjs — Flutter 三方库接口提取引擎
|
|
9
|
+
*
|
|
10
|
+
* 基于 Dart 正则解析,提取类/枚举/mixin/extension/函数/变量的公开接口。
|
|
11
|
+
* 输出与 ArkTS extract-interfaces.cjs 兼容的 interface-spec.json 数据模型,
|
|
12
|
+
* 供 flutter-render-spec-doc.cjs、flutter-analyze-demo-coverage.cjs、
|
|
13
|
+
* flutter-analyze-xts-coverage.cjs 消费。
|
|
14
|
+
*
|
|
15
|
+
* 核心策略:
|
|
16
|
+
* 1. pubspec.yaml → 包名 → lib/<package>.dart 入口定位
|
|
17
|
+
* 2. export 链递归追踪(src/ 相对路径 + package: 跨包路径)
|
|
18
|
+
* 3. export hide/show 过滤
|
|
19
|
+
* 4. 预处理移除字符串/注释(保留行号),大括号块匹配提取类体
|
|
20
|
+
* 5. 正则解析类/枚举/mixin/extension 成员 + 顶层函数/变量/typedef
|
|
21
|
+
* 6. /// 文档注释提取
|
|
22
|
+
* 7. _ 前缀私有成员过滤
|
|
23
|
+
* 8. monorepo 多子包支持
|
|
24
|
+
*
|
|
25
|
+
* 用法:
|
|
26
|
+
* node flutter-extract-interfaces.cjs --repoDir <库源码目录> --output <interface-spec.json>
|
|
27
|
+
*
|
|
28
|
+
* 使用示例:
|
|
29
|
+
* node flutter-extract-interfaces.cjs --repoDir /path/to/advertising_id --output advertising_id-interface-spec.json
|
|
30
|
+
* node flutter-extract-interfaces.cjs --repoDir /path/to/flutter_inappwebview
|
|
31
|
+
*
|
|
32
|
+
* 库源码目录结构约定:
|
|
33
|
+
* /path/to/lib/
|
|
34
|
+
* ├── <package>.dart ← 入口文件
|
|
35
|
+
* ├── src/ ← 库源码
|
|
36
|
+
* └── constants.dart
|
|
37
|
+
*
|
|
38
|
+
* # monorepo (如 flutter_inappwebview):
|
|
39
|
+
* /path/to/flutter_inappwebview/
|
|
40
|
+
* ├── flutter_inappwebview/ ← 主包
|
|
41
|
+
* │ ├── lib/
|
|
42
|
+
* │ └── pubspec.yaml
|
|
43
|
+
* └── flutter_inappwebview_platform_interface/
|
|
44
|
+
* └── lib/
|
|
45
|
+
*
|
|
46
|
+
* 模块导出:
|
|
47
|
+
* const { generateSpec } = require('./flutter-extract-interfaces.cjs');
|
|
48
|
+
* const spec = generateSpec('/path/to/lib_root');
|
|
49
|
+
*/
|
|
50
|
+
|
|
51
|
+
const fs = require('fs');
|
|
52
|
+
const path = require('path');
|
|
53
|
+
|
|
54
|
+
// ── 参数解析 ──────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
function parseArgs() {
|
|
57
|
+
const args = process.argv.slice(2);
|
|
58
|
+
// MCP Gateway 模式:第一个参数是 JSON 字符串
|
|
59
|
+
if (args.length > 0 && args[0].startsWith('{')) {
|
|
60
|
+
try { return JSON.parse(args[0]); } catch (e) { /* 降级到 CLI 模式 */ }
|
|
61
|
+
}
|
|
62
|
+
const parsed = {};
|
|
63
|
+
for (let i = 0; i < args.length; i++) {
|
|
64
|
+
if (args[i].startsWith('--')) {
|
|
65
|
+
const key = args[i].substring(2);
|
|
66
|
+
if (i + 1 < args.length && !args[i + 1].startsWith('--')) {
|
|
67
|
+
parsed[key] = args[i + 1];
|
|
68
|
+
i++;
|
|
69
|
+
} else {
|
|
70
|
+
parsed[key] = true;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return parsed;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// ── 工具函数 ──────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
function readText(file) {
|
|
80
|
+
return fs.readFileSync(file, 'utf-8').replace(/^\uFEFF/, '');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function rel(filePath, repoDir) {
|
|
84
|
+
let r = path.relative(repoDir, filePath).split(path.sep).join('/');
|
|
85
|
+
if (!r.startsWith('.')) r = './' + r;
|
|
86
|
+
return r;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function escapeRegex(str) {
|
|
90
|
+
return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** 获取字符索引对应的行号 (1-indexed) */
|
|
94
|
+
function getLineNum(content, index) {
|
|
95
|
+
if (index < 0 || index > content.length) return 0;
|
|
96
|
+
let line = 1;
|
|
97
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
98
|
+
if (content[i] === '\n') line++;
|
|
99
|
+
}
|
|
100
|
+
return line;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ── pubspec.yaml 解析 ──────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
function parsePubspec(repoDir) {
|
|
106
|
+
const pubspecPath = path.join(repoDir, 'pubspec.yaml');
|
|
107
|
+
if (!fs.existsSync(pubspecPath)) return { name: path.basename(repoDir), version: '' };
|
|
108
|
+
const raw = readText(pubspecPath);
|
|
109
|
+
const nameMatch = raw.match(/^name:\s*(.+)$/m);
|
|
110
|
+
const versionMatch = raw.match(/^version:\s*(.+)$/m);
|
|
111
|
+
return {
|
|
112
|
+
name: nameMatch ? nameMatch[1].trim().replace(/['"]/g, '') : path.basename(repoDir),
|
|
113
|
+
version: versionMatch ? versionMatch[1].trim().replace(/['"]/g, '') : '',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ── monorepo 检测 ──────────────────────────────────────────
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 检测是否为 monorepo(根目录无 lib/,但有多个子包)
|
|
121
|
+
* 返回主包目录路径
|
|
122
|
+
* 支持: flutter_inappwebview(子包在根目录下)、flutter_blue_plus(子包在 packages/ 下)
|
|
123
|
+
*/
|
|
124
|
+
function detectMainPackage(repoDir) {
|
|
125
|
+
if (fs.existsSync(path.join(repoDir, 'lib')) && fs.existsSync(path.join(repoDir, 'pubspec.yaml'))) {
|
|
126
|
+
return repoDir;
|
|
127
|
+
}
|
|
128
|
+
const dirName = path.basename(repoDir);
|
|
129
|
+
const candidates = [];
|
|
130
|
+
|
|
131
|
+
// 递归搜索(最多 3 层),收集所有含 pubspec.yaml + lib/ 的子目录
|
|
132
|
+
function walk(dir, depth) {
|
|
133
|
+
if (depth > 3) return;
|
|
134
|
+
try {
|
|
135
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
136
|
+
if (!entry.isDirectory()) continue;
|
|
137
|
+
if (entry.name.startsWith('.') || entry.name === 'node_modules' || entry.name === 'build') continue;
|
|
138
|
+
const subDir = path.join(dir, entry.name);
|
|
139
|
+
if (fs.existsSync(path.join(subDir, 'pubspec.yaml')) && fs.existsSync(path.join(subDir, 'lib'))) {
|
|
140
|
+
candidates.push(subDir);
|
|
141
|
+
// 精确匹配根目录名,直接返回
|
|
142
|
+
if (entry.name === dirName) return subDir;
|
|
143
|
+
// 同名去掉前缀也匹配(如 fluttertpc_flutter_blue_plus → flutter_blue_plus)
|
|
144
|
+
if (entry.name === dirName.replace(/^fluttertpc_/, '')) return subDir;
|
|
145
|
+
}
|
|
146
|
+
// 继续递归搜索(如 packages/flutter_blue_plus)
|
|
147
|
+
const found = walk(subDir, depth + 1);
|
|
148
|
+
if (found) return found;
|
|
149
|
+
}
|
|
150
|
+
} catch (e) { /* ignore */ }
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const exact = walk(repoDir, 0);
|
|
155
|
+
if (exact) return exact;
|
|
156
|
+
|
|
157
|
+
// 没有 exact match,从候选中选最合适的主包
|
|
158
|
+
if (candidates.length > 0) {
|
|
159
|
+
candidates.sort((a, b) => {
|
|
160
|
+
const aName = path.basename(a);
|
|
161
|
+
const bName = path.basename(b);
|
|
162
|
+
// 排除 platform_interface / android / ios / darwin / linux / web / ohos 等辅助包
|
|
163
|
+
const auxPatterns = ['platform_interface', '_android', '_ios', '_darwin', '_linux', '_web', '_ohos', '_windows', '_macos'];
|
|
164
|
+
const aAux = auxPatterns.some(p => aName.includes(p));
|
|
165
|
+
const bAux = auxPatterns.some(p => bName.includes(p));
|
|
166
|
+
if (aAux && !bAux) return 1;
|
|
167
|
+
if (!aAux && bAux) return -1;
|
|
168
|
+
return aName.localeCompare(bName);
|
|
169
|
+
});
|
|
170
|
+
return candidates[0];
|
|
171
|
+
}
|
|
172
|
+
// fallback: 根目录下有 lib/ 但无 pubspec.yaml(如纯源码目录)
|
|
173
|
+
if (fs.existsSync(path.join(repoDir, 'lib'))) return repoDir;
|
|
174
|
+
return repoDir;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ── 入口文件定位 ────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function locateEntryFile(repoDir, packageName) {
|
|
180
|
+
const candidates = [
|
|
181
|
+
path.join(repoDir, 'lib', `${packageName}.dart`),
|
|
182
|
+
path.join(repoDir, 'lib', 'main.dart'),
|
|
183
|
+
path.join(repoDir, 'lib', 'index.dart'),
|
|
184
|
+
];
|
|
185
|
+
for (const c of candidates) {
|
|
186
|
+
if (fs.existsSync(c)) return c;
|
|
187
|
+
}
|
|
188
|
+
// fallback: 找 lib/ 下第一个 .dart 文件
|
|
189
|
+
const libDir = path.join(repoDir, 'lib');
|
|
190
|
+
if (fs.existsSync(libDir)) {
|
|
191
|
+
for (const entry of fs.readdirSync(libDir)) {
|
|
192
|
+
if (entry.endsWith('.dart') && !entry.startsWith('_')) {
|
|
193
|
+
return path.join(libDir, entry);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// ── export 链追踪 ──────────────────────────────────────────
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 解析 export 语句,返回 { path, hide, show } 列表
|
|
204
|
+
*/
|
|
205
|
+
function parseExportStatements(content) {
|
|
206
|
+
const exports = [];
|
|
207
|
+
const regex = /^export\s+['"]([^'"]+)['"](?:\s+(hide|show)\s+([\w,\s]+))?/gm;
|
|
208
|
+
let m;
|
|
209
|
+
while ((m = regex.exec(content)) !== null) {
|
|
210
|
+
exports.push({
|
|
211
|
+
path: m[1],
|
|
212
|
+
combinator: m[2] || null, // 'hide' | 'show' | null
|
|
213
|
+
names: m[3] ? m[3].split(',').map(s => s.trim()).filter(Boolean) : [],
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
return exports;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* 解析 part 语句
|
|
221
|
+
*/
|
|
222
|
+
function parsePartStatements(content) {
|
|
223
|
+
const parts = [];
|
|
224
|
+
const regex = /^part\s+['"]([^'"]+)['"]/gm;
|
|
225
|
+
let m;
|
|
226
|
+
while ((m = regex.exec(content)) !== null) {
|
|
227
|
+
parts.push(m[1]);
|
|
228
|
+
}
|
|
229
|
+
return parts;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* 查找 package 的根目录(用于跨包 export)
|
|
234
|
+
*/
|
|
235
|
+
function findPackageRoot(pkgName, currentFile, repoDir) {
|
|
236
|
+
// 从当前文件向上找包根目录
|
|
237
|
+
let dir = path.dirname(currentFile);
|
|
238
|
+
let pkgRoot = dir;
|
|
239
|
+
while (dir !== '/' && dir !== repoDir) {
|
|
240
|
+
if (fs.existsSync(path.join(dir, 'pubspec.yaml'))) {
|
|
241
|
+
pkgRoot = dir;
|
|
242
|
+
break;
|
|
243
|
+
}
|
|
244
|
+
dir = path.dirname(dir);
|
|
245
|
+
}
|
|
246
|
+
if (dir === repoDir && fs.existsSync(path.join(repoDir, 'pubspec.yaml'))) {
|
|
247
|
+
pkgRoot = repoDir;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// 策略1: 同级目录找同名包
|
|
251
|
+
const parentDir = path.dirname(pkgRoot);
|
|
252
|
+
const siblingDir = path.join(parentDir, pkgName);
|
|
253
|
+
if (fs.existsSync(path.join(siblingDir, 'pubspec.yaml')) && fs.existsSync(path.join(siblingDir, 'lib'))) {
|
|
254
|
+
return siblingDir;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// 策略2: 在 repoDir 下递归查找
|
|
258
|
+
const found = [];
|
|
259
|
+
function walk(dir, depth) {
|
|
260
|
+
if (depth > 3) return;
|
|
261
|
+
try {
|
|
262
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
263
|
+
if (!entry.isDirectory() || entry.name.startsWith('.') || entry.name === 'node_modules') continue;
|
|
264
|
+
const full = path.join(dir, entry.name);
|
|
265
|
+
if (entry.name === pkgName && fs.existsSync(path.join(full, 'pubspec.yaml')) && fs.existsSync(path.join(full, 'lib'))) {
|
|
266
|
+
found.push(full);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
walk(full, depth + 1);
|
|
270
|
+
}
|
|
271
|
+
} catch (e) { /* ignore */ }
|
|
272
|
+
}
|
|
273
|
+
walk(repoDir, 0);
|
|
274
|
+
if (found.length > 0) return found[0];
|
|
275
|
+
|
|
276
|
+
// 策略3: .dart_tool/package_config.json
|
|
277
|
+
const configFile = path.join(pkgRoot, '.dart_tool', 'package_config.json');
|
|
278
|
+
if (fs.existsSync(configFile)) {
|
|
279
|
+
try {
|
|
280
|
+
const config = JSON.parse(readText(configFile));
|
|
281
|
+
const pkg = (config.packages || []).find(p => p.name === pkgName);
|
|
282
|
+
if (pkg && pkg.rootUri) {
|
|
283
|
+
const rootPath = pkg.rootUri.replace('file://', '').replace('file:', '');
|
|
284
|
+
if (fs.existsSync(rootPath)) return rootPath;
|
|
285
|
+
}
|
|
286
|
+
} catch (e) { /* ignore */ }
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return null;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/**
|
|
293
|
+
* 解析 export 路径为实际文件路径
|
|
294
|
+
*/
|
|
295
|
+
function resolveExportPath(exportPath, currentFile, repoDir) {
|
|
296
|
+
// package:xxx/file.dart → 跨包
|
|
297
|
+
if (exportPath.startsWith('package:')) {
|
|
298
|
+
const match = exportPath.match(/^package:([^/]+)\/(.+)$/);
|
|
299
|
+
if (!match) return null;
|
|
300
|
+
const pkgName = match[1];
|
|
301
|
+
const filePath = match[2];
|
|
302
|
+
const pkgRoot = findPackageRoot(pkgName, currentFile, repoDir);
|
|
303
|
+
if (!pkgRoot) return null;
|
|
304
|
+
const resolved = path.join(pkgRoot, 'lib', filePath);
|
|
305
|
+
return fs.existsSync(resolved) ? resolved : null;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// dart:xxx → 系统库,跳过
|
|
309
|
+
if (exportPath.startsWith('dart:')) return null;
|
|
310
|
+
|
|
311
|
+
// 相对路径 src/xxx.dart
|
|
312
|
+
const baseDir = path.dirname(currentFile);
|
|
313
|
+
const resolved = path.resolve(baseDir, exportPath);
|
|
314
|
+
return fs.existsSync(resolved) ? resolved : null;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* 递归收集所有通过 export 链可达的源文件
|
|
319
|
+
*/
|
|
320
|
+
function collectExportedFiles(entryFile, repoDir, visited = new Set(), hideShowMap = new Map()) {
|
|
321
|
+
if (visited.has(entryFile)) return [];
|
|
322
|
+
visited.add(entryFile);
|
|
323
|
+
|
|
324
|
+
const files = [entryFile];
|
|
325
|
+
const content = readText(entryFile);
|
|
326
|
+
|
|
327
|
+
// 追踪 export
|
|
328
|
+
const exports = parseExportStatements(content);
|
|
329
|
+
for (const exp of exports) {
|
|
330
|
+
const resolvedPath = resolveExportPath(exp.path, entryFile, repoDir);
|
|
331
|
+
if (!resolvedPath) continue;
|
|
332
|
+
|
|
333
|
+
// 记录 hide/show 过滤器
|
|
334
|
+
if (exp.combinator) {
|
|
335
|
+
if (!hideShowMap.has(resolvedPath)) hideShowMap.set(resolvedPath, []);
|
|
336
|
+
hideShowMap.get(resolvedPath).push({ type: exp.combinator, names: exp.names });
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
files.push(...collectExportedFiles(resolvedPath, repoDir, visited, hideShowMap));
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// 追踪 part 文件
|
|
343
|
+
const parts = parsePartStatements(content);
|
|
344
|
+
for (const partPath of parts) {
|
|
345
|
+
const resolved = path.resolve(path.dirname(entryFile), partPath);
|
|
346
|
+
if (fs.existsSync(resolved) && !visited.has(resolved)) {
|
|
347
|
+
visited.add(resolved);
|
|
348
|
+
files.push(resolved);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
return files;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* 检查名称是否被 hide/show 过滤掉
|
|
357
|
+
*/
|
|
358
|
+
function isHiddenByExport(name, hideShowMap, filePath) {
|
|
359
|
+
const filters = hideShowMap.get(filePath);
|
|
360
|
+
if (!filters) return false;
|
|
361
|
+
|
|
362
|
+
for (const f of filters) {
|
|
363
|
+
if (f.type === 'hide' && f.names.includes(name)) return true;
|
|
364
|
+
if (f.type === 'show' && !f.names.includes(name)) return true;
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// ── 源文件收集 ──────────────────────────────────────────────
|
|
370
|
+
|
|
371
|
+
function collectAllSourceFiles(repoDir) {
|
|
372
|
+
const sources = [];
|
|
373
|
+
const libDir = path.join(repoDir, 'lib');
|
|
374
|
+
if (!fs.existsSync(libDir)) return sources;
|
|
375
|
+
|
|
376
|
+
function walk(dir) {
|
|
377
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
378
|
+
const fullPath = path.join(dir, entry.name);
|
|
379
|
+
if (entry.isDirectory()) {
|
|
380
|
+
if (entry.name === '.dart_tool' || entry.name === 'node_modules') continue;
|
|
381
|
+
walk(fullPath);
|
|
382
|
+
} else if (entry.isFile() && entry.name.endsWith('.dart') && !entry.name.endsWith('.g.dart') && !entry.name.endsWith('.freezed.dart')) {
|
|
383
|
+
sources.push(fullPath);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
walk(libDir);
|
|
388
|
+
return sources;
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
// ── 预处理:移除字符串/注释(保留行号)──────────────────────────
|
|
392
|
+
|
|
393
|
+
function preprocess(content) {
|
|
394
|
+
let result = content;
|
|
395
|
+
|
|
396
|
+
// 1. 移除块注释 /* ... */ (保留换行)
|
|
397
|
+
result = result.replace(/\/\*[\s\S]*?\*\//g, (match) => {
|
|
398
|
+
return match.replace(/[^\n]/g, ' ');
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// 2. 移除多行字符串 ''' ... ''' 和 """ ... """
|
|
402
|
+
result = result.replace(/'''[\s\S]*?'''/g, (match) => match.replace(/[^\n]/g, ' '));
|
|
403
|
+
result = result.replace(/"""[\s\S]*?"""/g, (match) => match.replace(/[^\n]/g, ' '));
|
|
404
|
+
|
|
405
|
+
// 3. 逐行处理:保留 /// 文档注释行,其他行先移除字符串再移除注释
|
|
406
|
+
const lines = result.split('\n');
|
|
407
|
+
const processedLines = lines.map(line => {
|
|
408
|
+
const trimmed = line.trimStart();
|
|
409
|
+
// 保留 /// 文档注释行(不移除其中的字符串)
|
|
410
|
+
if (trimmed.startsWith('///')) {
|
|
411
|
+
// 但需要移除 /// 行中的 { 和 },避免 {@template} {@endtemplate} 等
|
|
412
|
+
// Dartdoc 注解的大括号干扰 extractBraceBlock 的深度计数
|
|
413
|
+
return line.replace(/[{}]/g, ' ');
|
|
414
|
+
}
|
|
415
|
+
// 其他行:先移除字符串,再移除注释
|
|
416
|
+
let processed = line;
|
|
417
|
+
// 移除双引号字符串
|
|
418
|
+
processed = processed.replace(/"(?:[^"\\]|\\.)*"/g, '""');
|
|
419
|
+
// 移除单引号字符串
|
|
420
|
+
processed = processed.replace(/'(?:[^'\\]|\\.)*'/g, "''");
|
|
421
|
+
// 移除单行注释 //(字符串已移除,剩余的 // 一定是注释;但跳过 ///)
|
|
422
|
+
const commentIdx = processed.indexOf('//');
|
|
423
|
+
if (commentIdx >= 0) {
|
|
424
|
+
// 检查是否为 /// (三个斜线)
|
|
425
|
+
if (commentIdx + 2 < processed.length && processed[commentIdx + 2] === '/') {
|
|
426
|
+
// /// 文档注释,保留
|
|
427
|
+
return processed;
|
|
428
|
+
}
|
|
429
|
+
// 普通 // 注释,用空格替换注释部分(保留换行)
|
|
430
|
+
processed = processed.substring(0, commentIdx) + ' '.repeat(line.length - commentIdx);
|
|
431
|
+
}
|
|
432
|
+
return processed;
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
return processedLines.join('\n');
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// ── 文档注释提取 ──────────────────────────────────────────
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 从原始内容的指定行号向上回溯提取 /// 文档注释
|
|
442
|
+
*/
|
|
443
|
+
function extractDocComment(content, lineNum) {
|
|
444
|
+
const lines = content.split('\n');
|
|
445
|
+
const docLines = [];
|
|
446
|
+
let j = lineNum - 2; // 0-indexed, 上一行
|
|
447
|
+
while (j >= 0) {
|
|
448
|
+
const line = lines[j].trim();
|
|
449
|
+
if (line.startsWith('///')) {
|
|
450
|
+
docLines.unshift(line.replace(/^\/\/\/\s*/, ''));
|
|
451
|
+
j--;
|
|
452
|
+
} else if (line.startsWith('*') && j > 0 && lines[j - 1].trim().startsWith('/*')) {
|
|
453
|
+
// /* */ 风格文档注释
|
|
454
|
+
docLines.unshift(line.replace(/^\*\s*/, ''));
|
|
455
|
+
j--;
|
|
456
|
+
} else {
|
|
457
|
+
break;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
const text = docLines.join(' ').replace(/\s+/g, ' ').trim();
|
|
461
|
+
return text || '-';
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// ── 大括号块提取 ──────────────────────────────────────────
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* 从 startIndex 位置(`{` 的位置)开始,匹配大括号块
|
|
468
|
+
* 返回 { body: 块体内容, end: 闭合大括号位置 }
|
|
469
|
+
*/
|
|
470
|
+
function extractBraceBlock(cleanContent, startIndex) {
|
|
471
|
+
let depth = 1;
|
|
472
|
+
let i = startIndex + 1;
|
|
473
|
+
while (i < cleanContent.length && depth > 0) {
|
|
474
|
+
const c = cleanContent[i];
|
|
475
|
+
if (c === '{') depth++;
|
|
476
|
+
else if (c === '}') depth--;
|
|
477
|
+
i++;
|
|
478
|
+
}
|
|
479
|
+
if (depth !== 0) return { body: '', end: startIndex };
|
|
480
|
+
return {
|
|
481
|
+
body: cleanContent.substring(startIndex + 1, i - 1),
|
|
482
|
+
end: i,
|
|
483
|
+
};
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// ── 参数解析 ──────────────────────────────────────────────
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* 按逗号分割参数列表(处理嵌套的 <> 和 ())
|
|
490
|
+
*/
|
|
491
|
+
function splitByComma(str) {
|
|
492
|
+
const parts = [];
|
|
493
|
+
let depth = 0;
|
|
494
|
+
let start = 0;
|
|
495
|
+
for (let i = 0; i < str.length; i++) {
|
|
496
|
+
const c = str[i];
|
|
497
|
+
if (c === '<' || c === '(' || c === '[' || c === '{') depth++;
|
|
498
|
+
else if (c === '>' || c === ')' || c === ']' || c === '}') depth--;
|
|
499
|
+
else if (c === ',' && depth === 0) {
|
|
500
|
+
parts.push(str.substring(start, i));
|
|
501
|
+
start = i + 1;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
parts.push(str.substring(start));
|
|
505
|
+
return parts.filter(s => s.trim());
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
/**
|
|
509
|
+
* 解析 Dart 参数列表
|
|
510
|
+
* 支持: 位置参数、可选位置参数 [x]、命名参数 {x}、required 命名参数
|
|
511
|
+
*/
|
|
512
|
+
function parseParameters(paramStr) {
|
|
513
|
+
if (!paramStr || !paramStr.trim()) return [];
|
|
514
|
+
|
|
515
|
+
// 检测可选/命名参数区域
|
|
516
|
+
let inOptional = false;
|
|
517
|
+
let inNamed = false;
|
|
518
|
+
// 去掉标记符号但记录存在
|
|
519
|
+
const hasOptional = /\[.*\]/.test(paramStr);
|
|
520
|
+
const hasNamed = /\{.*\}/.test(paramStr);
|
|
521
|
+
const cleanStr = paramStr.replace(/[\[\]{}]/g, '');
|
|
522
|
+
|
|
523
|
+
const parts = splitByComma(cleanStr);
|
|
524
|
+
const params = [];
|
|
525
|
+
|
|
526
|
+
for (const part of parts) {
|
|
527
|
+
const trimmed = part.trim();
|
|
528
|
+
if (!trimmed) continue;
|
|
529
|
+
|
|
530
|
+
// 去掉 required / covariant 修饰符
|
|
531
|
+
const isRequired = /^required\s+/.test(trimmed);
|
|
532
|
+
const cleanPart = trimmed.replace(/^(required|covariant)\s+/, '');
|
|
533
|
+
|
|
534
|
+
// 先检查 this.name 简写形式(Dart 构造函数参数简写)
|
|
535
|
+
const thisMatch = cleanPart.match(/^this\.(\w+)\s*(?:=\s*(.+))?$/);
|
|
536
|
+
if (thisMatch) {
|
|
537
|
+
params.push({
|
|
538
|
+
name: thisMatch[1],
|
|
539
|
+
type: 'dynamic',
|
|
540
|
+
optional: !isRequired && (hasOptional || hasNamed || !!thisMatch[2]),
|
|
541
|
+
description: '-',
|
|
542
|
+
});
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
// 匹配: [final/const] Type name [= default]
|
|
547
|
+
// Type 可以是: String, String?, List<int>, Map<String, dynamic>, Future<List<T>?>, etc.
|
|
548
|
+
let m = cleanPart.match(/^(?:(final|const)\s+)?([\w<>\?,\s@.]+?)\s+(\w+)\s*(?:=\s*(.+))?$/);
|
|
549
|
+
if (m) {
|
|
550
|
+
const typeStr = m[2].trim();
|
|
551
|
+
const nameStr = m[3].trim();
|
|
552
|
+
params.push({
|
|
553
|
+
name: nameStr,
|
|
554
|
+
type: typeStr,
|
|
555
|
+
optional: !isRequired && (hasOptional || hasNamed || !!m[4]),
|
|
556
|
+
description: '-',
|
|
557
|
+
});
|
|
558
|
+
} else {
|
|
559
|
+
// 简单变量名(如 var name 或 dynamic name)
|
|
560
|
+
m = cleanPart.match(/^(?:var\s+)?(\w+)\s*(?:=\s*(.+))?$/);
|
|
561
|
+
if (m) {
|
|
562
|
+
params.push({
|
|
563
|
+
name: m[1],
|
|
564
|
+
type: 'dynamic',
|
|
565
|
+
optional: !isRequired && (hasOptional || hasNamed || !!m[2]),
|
|
566
|
+
description: '-',
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
return params;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// ── 移除大括号块(方法体)──────────────────────────────────
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* 将内容中所有顶层大括号块替换为等长空格(保留行号)
|
|
579
|
+
*/
|
|
580
|
+
function removeAllBraceBlocks(content) {
|
|
581
|
+
let result = content;
|
|
582
|
+
const blocks = [];
|
|
583
|
+
let depth = 0;
|
|
584
|
+
let start = -1;
|
|
585
|
+
for (let i = 0; i < result.length; i++) {
|
|
586
|
+
if (result[i] === '{') {
|
|
587
|
+
if (depth === 0) start = i;
|
|
588
|
+
depth++;
|
|
589
|
+
} else if (result[i] === '}') {
|
|
590
|
+
depth--;
|
|
591
|
+
if (depth === 0 && start >= 0) {
|
|
592
|
+
blocks.push({ start, end: i + 1 });
|
|
593
|
+
start = -1;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
for (let i = blocks.length - 1; i >= 0; i--) {
|
|
598
|
+
const b = blocks[i];
|
|
599
|
+
result = result.substring(0, b.start) + ' '.repeat(b.end - b.start) + result.substring(b.end);
|
|
600
|
+
}
|
|
601
|
+
return result;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// ── 类成员提取 ──────────────────────────────────────────────
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* 在清理后的类体内提取成员
|
|
608
|
+
* 策略:先匹配方法/getter/构造函数(行首锚定),然后移除方法体后再匹配字段
|
|
609
|
+
*/
|
|
610
|
+
function extractClassMembers(classBody, className, classType, filePath, repoDir, bodyStartLine, rawContent, exportedNames, hideShowMap, onType) {
|
|
611
|
+
const members = [];
|
|
612
|
+
const DART_KEYWORDS = ['class','extends','implements','with','void','return','final','const','late','static','var','get','set','operator','if','else','for','while','switch','case','break','continue','try','catch','finally','throw','new','import','export','part','library','show','hide','as','is','enum','mixin','extension','typedef','abstract','factory','this','super','do','in','yield','sync','async','await','dynamic','covariant','required','external','native','assert','default'];
|
|
613
|
+
|
|
614
|
+
// Object 基类内置成员:所有 Dart 对象自动继承,不属于库自身公开 API
|
|
615
|
+
// 即使有 @override 也不计入接口覆盖率
|
|
616
|
+
const OBJECT_BUILTIN_MEMBERS = ['hashCode', 'toString', 'noSuchMethod', 'runtimeType'];
|
|
617
|
+
|
|
618
|
+
// 正则模式(行首锚定 + 类型不含空格,避免回溯匹配 static)
|
|
619
|
+
const patterns = {
|
|
620
|
+
// 方法: [static] [async] ReturnType methodName(params) [async*] { ... } | => ... | ;
|
|
621
|
+
// 类型用 [^\s{}()]+ 匹配(不含空格/大括号/括号),避免回溯
|
|
622
|
+
method: /^[ \t]*(?:(static)\s+)?(?:(async)\s+)?([^\s{}()]+)\s+(\w+)\s*\(([^)]*)\)\s*(?:async\*?|sync\*?)?\s*(?:\{|=>|;)/gm,
|
|
623
|
+
// getter: [static] Type get name [async] { ... } | => ...
|
|
624
|
+
getter: /^[ \t]*(?:(static)\s+)?([^\s{}()]+)\s+get\s+(\w+)\s*(?:async\s*)?(?:\{|=>|;)/gm,
|
|
625
|
+
// 构造函数: [factory] ClassName[.named](params) [: init] { ... } | => ... | ;
|
|
626
|
+
constructor: /^[ \t]*(?:(factory)\s+)?(\w+)(?:\.(\w+))?\s*\(([^)]*)\)\s*(?::\s*[^{;]+)?\s*(?:\{|=>|;)/gm,
|
|
627
|
+
// 字段: [static] [final/const/late] Type fieldName [= value]; (在移除方法体后匹配)
|
|
628
|
+
field: /^[ \t]*(?:(static)\s+)?(?:(final|const|late)\s+)?([^\s{}();=]+)\s+(\w+)\s*(?:=\s*[^;]+)?;/gm,
|
|
629
|
+
};
|
|
630
|
+
|
|
631
|
+
// 1. 提取方法
|
|
632
|
+
let m;
|
|
633
|
+
while ((m = patterns.method.exec(classBody)) !== null) {
|
|
634
|
+
const isStatic = !!m[1];
|
|
635
|
+
const returnType = (m[3] || '').trim();
|
|
636
|
+
const memberName = m[4];
|
|
637
|
+
const paramStr = m[5] || '';
|
|
638
|
+
|
|
639
|
+
if (memberName.startsWith('_')) continue;
|
|
640
|
+
if (memberName === className) continue;
|
|
641
|
+
if (DART_KEYWORDS.includes(memberName)) continue;
|
|
642
|
+
if (DART_KEYWORDS.includes(returnType)) continue;
|
|
643
|
+
if (returnType.endsWith(':')) continue;
|
|
644
|
+
if (OBJECT_BUILTIN_MEMBERS.includes(memberName)) continue;
|
|
645
|
+
|
|
646
|
+
const charIdx = m.index;
|
|
647
|
+
const lineNum = bodyStartLine + getLineNum(classBody, charIdx);
|
|
648
|
+
const isEvent = /^on[A-Z]/.test(memberName);
|
|
649
|
+
|
|
650
|
+
members.push({
|
|
651
|
+
className, memberName, fullName: `${className}.${memberName}`,
|
|
652
|
+
kind: 'method', isStatic, isReadonly: false, isOptional: false, isEvent,
|
|
653
|
+
type: classType, onType: onType || null,
|
|
654
|
+
signature: `${isStatic ? 'static ' : ''}${returnType} ${memberName}(${paramStr})`,
|
|
655
|
+
params: parseParameters(paramStr), returnType: returnType || 'void',
|
|
656
|
+
description: extractDocComment(rawContent, lineNum),
|
|
657
|
+
source: rel(filePath, repoDir), startLine: lineNum,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
// 2. 提取 getter(属性)
|
|
662
|
+
while ((m = patterns.getter.exec(classBody)) !== null) {
|
|
663
|
+
const isStatic = !!m[1];
|
|
664
|
+
const returnType = (m[2] || '').trim();
|
|
665
|
+
const memberName = m[3];
|
|
666
|
+
if (memberName.startsWith('_')) continue;
|
|
667
|
+
if (DART_KEYWORDS.includes(memberName)) continue;
|
|
668
|
+
if (OBJECT_BUILTIN_MEMBERS.includes(memberName)) continue;
|
|
669
|
+
if (members.some(mem => mem.memberName === memberName)) continue;
|
|
670
|
+
|
|
671
|
+
const charIdx = m.index;
|
|
672
|
+
const lineNum = bodyStartLine + getLineNum(classBody, charIdx);
|
|
673
|
+
|
|
674
|
+
members.push({
|
|
675
|
+
className, memberName, fullName: `${className}.${memberName}`,
|
|
676
|
+
kind: 'property', isStatic, isReadonly: true, isOptional: false, isEvent: false,
|
|
677
|
+
type: classType, onType: onType || null,
|
|
678
|
+
signature: `${isStatic ? 'static ' : ''}${returnType} get ${memberName}`,
|
|
679
|
+
params: [], returnType,
|
|
680
|
+
description: extractDocComment(rawContent, lineNum),
|
|
681
|
+
source: rel(filePath, repoDir), startLine: lineNum,
|
|
682
|
+
});
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
// 3. 提取构造函数
|
|
686
|
+
// 跳过无参默认构造函数 ClassName()(非 factory、非命名、无参数),因为:
|
|
687
|
+
// - Dart 会隐式提供默认构造函数,不含业务逻辑
|
|
688
|
+
// - Demo 中即使使用也难以正则匹配,且无测试意义
|
|
689
|
+
patterns.constructor.lastIndex = 0;
|
|
690
|
+
while ((m = patterns.constructor.exec(classBody)) !== null) {
|
|
691
|
+
const isFactory = !!m[1];
|
|
692
|
+
const ctorClassName = m[2];
|
|
693
|
+
const namedCtor = m[3];
|
|
694
|
+
const paramStr = (m[4] || '').trim();
|
|
695
|
+
if (ctorClassName !== className) continue;
|
|
696
|
+
|
|
697
|
+
// 跳过无参默认构造函数:非 factory、非命名、无参数
|
|
698
|
+
if (!isFactory && !namedCtor && paramStr === '') continue;
|
|
699
|
+
|
|
700
|
+
const memberName = namedCtor || 'constructor';
|
|
701
|
+
if (memberName.startsWith('_')) continue;
|
|
702
|
+
if (members.some(mem => mem.memberName === memberName)) continue;
|
|
703
|
+
|
|
704
|
+
const charIdx = m.index;
|
|
705
|
+
const lineNum = bodyStartLine + getLineNum(classBody, charIdx);
|
|
706
|
+
|
|
707
|
+
members.push({
|
|
708
|
+
className, memberName,
|
|
709
|
+
fullName: namedCtor ? `${className}.${namedCtor}` : `${className}.constructor`,
|
|
710
|
+
kind: 'constructor', isStatic: false, isReadonly: false, isOptional: false, isEvent: false,
|
|
711
|
+
type: classType, onType: onType || null,
|
|
712
|
+
signature: `${isFactory ? 'factory ' : ''}${ctorClassName}${namedCtor ? '.' + namedCtor : ''}(${paramStr})`,
|
|
713
|
+
params: parseParameters(paramStr), returnType: className,
|
|
714
|
+
description: extractDocComment(rawContent, lineNum),
|
|
715
|
+
source: rel(filePath, repoDir), startLine: lineNum,
|
|
716
|
+
});
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// 4. 移除所有大括号块(方法体),在剩余内容上匹配字段
|
|
720
|
+
// 类内字段分类:
|
|
721
|
+
// const → kind='constant'(编译时常量)
|
|
722
|
+
// final → kind='field'(运行时只读字段,如 Widget 配置参数,用户可通过构造函数传值)
|
|
723
|
+
// 无修饰符/late → kind='field'
|
|
724
|
+
const fieldContent = removeAllBraceBlocks(classBody);
|
|
725
|
+
patterns.field.lastIndex = 0;
|
|
726
|
+
while ((m = patterns.field.exec(fieldContent)) !== null) {
|
|
727
|
+
const isStatic = !!m[1];
|
|
728
|
+
const modifier = m[2];
|
|
729
|
+
const fieldType = (m[3] || '').trim();
|
|
730
|
+
const memberName = m[4];
|
|
731
|
+
|
|
732
|
+
if (memberName.startsWith('_')) continue;
|
|
733
|
+
if (members.some(mem => mem.memberName === memberName)) continue;
|
|
734
|
+
if (DART_KEYWORDS.includes(memberName)) continue;
|
|
735
|
+
if (DART_KEYWORDS.includes(fieldType)) continue;
|
|
736
|
+
if (fieldType.length === 0) continue;
|
|
737
|
+
if (fieldType.startsWith('_')) continue;
|
|
738
|
+
|
|
739
|
+
const charIdx = m.index;
|
|
740
|
+
const lineNum = bodyStartLine + getLineNum(classBody, charIdx);
|
|
741
|
+
const isConstant = modifier === 'const';
|
|
742
|
+
|
|
743
|
+
members.push({
|
|
744
|
+
className, memberName, fullName: `${className}.${memberName}`,
|
|
745
|
+
kind: isConstant ? 'constant' : 'field', isStatic, isReadonly: modifier === 'const' || modifier === 'final', isOptional: false, isEvent: false,
|
|
746
|
+
type: classType, onType: onType || null,
|
|
747
|
+
signature: `${isStatic ? 'static ' : ''}${modifier ? modifier + ' ' : ''}${fieldType} ${memberName}`,
|
|
748
|
+
params: [], returnType: fieldType,
|
|
749
|
+
description: extractDocComment(rawContent, lineNum),
|
|
750
|
+
source: rel(filePath, repoDir), startLine: lineNum,
|
|
751
|
+
});
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
// 5. 过滤 export hide/show
|
|
755
|
+
return members.filter(m => {
|
|
756
|
+
const nameToCheck = m.kind === 'constructor' ? className : m.memberName;
|
|
757
|
+
return !isHiddenByExport(nameToCheck, hideShowMap, filePath);
|
|
758
|
+
});
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
// ── 枚举成员提取 ──────────────────────────────────────────────
|
|
762
|
+
|
|
763
|
+
function extractEnumMembers(enumBody, enumName, filePath, repoDir, bodyStartLine, rawContent) {
|
|
764
|
+
const members = [];
|
|
765
|
+
|
|
766
|
+
// Dart 2 简单枚举: { val1, val2, val3 }
|
|
767
|
+
// Dart 3 增强枚举: { val1, val2(args); Type field; const Name(this.field); }
|
|
768
|
+
|
|
769
|
+
// 先提取枚举值(在第一个 ; 之前的部分)
|
|
770
|
+
const semicolonIdx = enumBody.indexOf(';');
|
|
771
|
+
const enumValuesPart = semicolonIdx >= 0 ? enumBody.substring(0, semicolonIdx) : enumBody;
|
|
772
|
+
|
|
773
|
+
// 按逗号分割枚举值
|
|
774
|
+
const valuePattern = /(\w+)(?:\s*\(([^)]*)\))?/g;
|
|
775
|
+
let m;
|
|
776
|
+
while ((m = valuePattern.exec(enumValuesPart)) !== null) {
|
|
777
|
+
const name = m[1];
|
|
778
|
+
if (name.startsWith('_')) continue;
|
|
779
|
+
if (['const', 'final', 'static', 'factory', 'class', 'enum', 'mixin'].includes(name)) continue;
|
|
780
|
+
|
|
781
|
+
const charIdx = m.index;
|
|
782
|
+
const lineNum = bodyStartLine + getLineNum(enumBody, charIdx);
|
|
783
|
+
|
|
784
|
+
members.push({
|
|
785
|
+
className: enumName,
|
|
786
|
+
memberName: name,
|
|
787
|
+
fullName: `${enumName}.${name}`,
|
|
788
|
+
kind: 'enumValue',
|
|
789
|
+
isStatic: true,
|
|
790
|
+
isReadonly: true,
|
|
791
|
+
isOptional: false,
|
|
792
|
+
isEvent: false,
|
|
793
|
+
type: 'enum',
|
|
794
|
+
signature: m[2] ? `${name}(${m[2]})` : name,
|
|
795
|
+
params: [],
|
|
796
|
+
returnType: name,
|
|
797
|
+
description: extractDocComment(rawContent, lineNum),
|
|
798
|
+
source: rel(filePath, repoDir),
|
|
799
|
+
startLine: lineNum,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
// 如果是增强枚举,也提取成员方法/字段
|
|
804
|
+
if (semicolonIdx >= 0) {
|
|
805
|
+
const memberPart = enumBody.substring(semicolonIdx + 1);
|
|
806
|
+
const classMembers = extractClassMembers(memberPart, enumName, 'enum', filePath, repoDir, bodyStartLine + getLineNum(enumBody, semicolonIdx + 1), rawContent, null, new Map());
|
|
807
|
+
members.push(...classMembers);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
return members;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
// ── 顶层声明提取 ──────────────────────────────────────────────
|
|
814
|
+
|
|
815
|
+
function extractTopLevelDeclarations(cleanContent, rawContent, filePath, repoDir, exportedNames, hideShowMap) {
|
|
816
|
+
const members = [];
|
|
817
|
+
const fullContent = cleanContent;
|
|
818
|
+
// 控制流关键字(不能作为返回类型)
|
|
819
|
+
const FLOW_KEYWORDS = ['return', 'if', 'else', 'for', 'while', 'switch', 'case', 'break', 'continue', 'try', 'catch', 'finally', 'throw', 'new', 'import', 'export', 'part', 'library', 'show', 'hide', 'as', 'is', 'class', 'extends', 'implements', 'with', 'enum', 'mixin', 'extension', 'typedef', 'abstract', 'factory', 'this', 'super', 'do', 'in', 'yield', 'sync', 'await', 'covariant', 'required', 'external', 'native', 'assert', 'default', 'operator', 'get', 'set', 'static', 'late', 'const', 'final', 'var'];
|
|
820
|
+
|
|
821
|
+
// 顶层函数: [async] ReturnType functionName(params) [async] { ... } | => ...
|
|
822
|
+
// 行首锚定 + 类型不含空格,避免回溯
|
|
823
|
+
const funcPattern = /^[ \t]*(?:(async)\s+)?([^\s{}()]+)\s+(\w+)\s*\(([^)]*)\)\s*(?:async\*?|sync\*?)?\s*(?:\{|=>|;)/gm;
|
|
824
|
+
let m;
|
|
825
|
+
while ((m = funcPattern.exec(fullContent)) !== null) {
|
|
826
|
+
const returnType = (m[2] || '').trim();
|
|
827
|
+
const funcName = m[3];
|
|
828
|
+
const paramStr = m[4] || '';
|
|
829
|
+
|
|
830
|
+
if (funcName.startsWith('_')) continue;
|
|
831
|
+
// 跳过关键字函数名
|
|
832
|
+
if (FLOW_KEYWORDS.includes(funcName)) continue;
|
|
833
|
+
// 跳过关键字返回类型(如 return X(...) 被误匹配为函数)
|
|
834
|
+
if (FLOW_KEYWORDS.includes(returnType)) continue;
|
|
835
|
+
|
|
836
|
+
const lineNum = getLineNum(rawContent, m.index);
|
|
837
|
+
|
|
838
|
+
// 检查是否在 export 链中
|
|
839
|
+
if (exportedNames && !exportedNames.has(funcName) && !isExported(filePath, hideShowMap)) continue;
|
|
840
|
+
|
|
841
|
+
members.push({
|
|
842
|
+
className: null,
|
|
843
|
+
memberName: funcName,
|
|
844
|
+
fullName: funcName,
|
|
845
|
+
kind: 'function',
|
|
846
|
+
isStatic: false,
|
|
847
|
+
isReadonly: false,
|
|
848
|
+
isOptional: false,
|
|
849
|
+
isEvent: false,
|
|
850
|
+
type: 'function',
|
|
851
|
+
signature: `${returnType} ${funcName}(${paramStr})`,
|
|
852
|
+
params: parseParameters(paramStr),
|
|
853
|
+
returnType: returnType || 'void',
|
|
854
|
+
description: extractDocComment(rawContent, lineNum),
|
|
855
|
+
source: rel(filePath, repoDir),
|
|
856
|
+
startLine: lineNum,
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
// 顶层常量/变量: const/final/var Type name = value; 或 const name = value;
|
|
861
|
+
// 行首锚定,类型不含空格
|
|
862
|
+
// const → kind='constant', final → kind='constant', var → kind='variable'
|
|
863
|
+
const constPattern = /^[ \t]*(?:(const|final|var)\s+)(?:([^\s;=]+)\s+)?(\w+)\s*=\s*[^;]+;/gm;
|
|
864
|
+
while ((m = constPattern.exec(fullContent)) !== null) {
|
|
865
|
+
const modifier = m[1];
|
|
866
|
+
const typeStr = (m[2] || '').trim();
|
|
867
|
+
const varName = m[3];
|
|
868
|
+
|
|
869
|
+
if (varName.startsWith('_')) continue;
|
|
870
|
+
if (FLOW_KEYWORDS.includes(varName)) continue;
|
|
871
|
+
if (typeStr && FLOW_KEYWORDS.includes(typeStr)) continue;
|
|
872
|
+
|
|
873
|
+
// 跳过已在函数中匹配的
|
|
874
|
+
if (members.some(mem => mem.memberName === varName)) continue;
|
|
875
|
+
|
|
876
|
+
const lineNum = getLineNum(rawContent, m.index);
|
|
877
|
+
|
|
878
|
+
if (exportedNames && !exportedNames.has(varName) && !isExported(filePath, hideShowMap)) continue;
|
|
879
|
+
|
|
880
|
+
const isConstant = modifier === 'const' || modifier === 'final';
|
|
881
|
+
members.push({
|
|
882
|
+
className: null,
|
|
883
|
+
memberName: varName,
|
|
884
|
+
fullName: varName,
|
|
885
|
+
kind: isConstant ? 'constant' : 'variable',
|
|
886
|
+
isStatic: false,
|
|
887
|
+
isReadonly: isConstant,
|
|
888
|
+
isOptional: false,
|
|
889
|
+
isEvent: false,
|
|
890
|
+
type: isConstant ? 'constant' : 'variable',
|
|
891
|
+
signature: `${modifier} ${typeStr || 'var'} ${varName}`,
|
|
892
|
+
params: [],
|
|
893
|
+
returnType: typeStr || 'dynamic',
|
|
894
|
+
description: extractDocComment(rawContent, lineNum),
|
|
895
|
+
source: rel(filePath, repoDir),
|
|
896
|
+
startLine: lineNum,
|
|
897
|
+
});
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
// typedef: typedef ReturnType AliasName = Type; 或 typedef AliasName = Type;
|
|
901
|
+
const typedefPattern = /^typedef\s+(\w+)\s*=\s*([^;]+);/gm;
|
|
902
|
+
while ((m = typedefPattern.exec(fullContent)) !== null) {
|
|
903
|
+
const typedefName = m[1];
|
|
904
|
+
const typeValue = (m[2] || '').trim();
|
|
905
|
+
if (typedefName.startsWith('_')) continue;
|
|
906
|
+
|
|
907
|
+
const lineNum = getLineNum(rawContent, m.index);
|
|
908
|
+
if (exportedNames && !exportedNames.has(typedefName) && !isExported(filePath, hideShowMap)) continue;
|
|
909
|
+
|
|
910
|
+
members.push({
|
|
911
|
+
className: typedefName,
|
|
912
|
+
memberName: typedefName,
|
|
913
|
+
fullName: typedefName,
|
|
914
|
+
kind: 'type',
|
|
915
|
+
isStatic: false,
|
|
916
|
+
isReadonly: false,
|
|
917
|
+
isOptional: false,
|
|
918
|
+
isEvent: false,
|
|
919
|
+
type: 'typedef',
|
|
920
|
+
signature: `typedef ${typedefName} = ${typeValue}`,
|
|
921
|
+
params: [],
|
|
922
|
+
returnType: typeValue,
|
|
923
|
+
description: extractDocComment(rawContent, lineNum),
|
|
924
|
+
source: rel(filePath, repoDir),
|
|
925
|
+
startLine: lineNum,
|
|
926
|
+
});
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
// 旧式 typedef: typedef ReturnType AliasName(params);
|
|
930
|
+
const oldTypedefPattern = /^typedef\s+([\w<>\?,\s@]+?)\s+(\w+)\s*\(([^)]*)\)/gm;
|
|
931
|
+
while ((m = oldTypedefPattern.exec(fullContent)) !== null) {
|
|
932
|
+
const returnType = (m[1] || '').trim();
|
|
933
|
+
const typedefName = m[2];
|
|
934
|
+
const paramStr = m[3] || '';
|
|
935
|
+
if (typedefName.startsWith('_')) continue;
|
|
936
|
+
if (members.some(mem => mem.memberName === typedefName)) continue;
|
|
937
|
+
|
|
938
|
+
const lineNum = getLineNum(rawContent, m.index);
|
|
939
|
+
if (exportedNames && !exportedNames.has(typedefName) && !isExported(filePath, hideShowMap)) continue;
|
|
940
|
+
|
|
941
|
+
members.push({
|
|
942
|
+
className: typedefName,
|
|
943
|
+
memberName: typedefName,
|
|
944
|
+
fullName: typedefName,
|
|
945
|
+
kind: 'type',
|
|
946
|
+
isStatic: false,
|
|
947
|
+
isReadonly: false,
|
|
948
|
+
isOptional: false,
|
|
949
|
+
isEvent: false,
|
|
950
|
+
type: 'typedef',
|
|
951
|
+
signature: `typedef ${returnType} ${typedefName}(${paramStr})`,
|
|
952
|
+
params: parseParameters(paramStr),
|
|
953
|
+
returnType,
|
|
954
|
+
description: extractDocComment(rawContent, lineNum),
|
|
955
|
+
source: rel(filePath, repoDir),
|
|
956
|
+
startLine: lineNum,
|
|
957
|
+
});
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
return members;
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
function isExported(filePath, hideShowMap) {
|
|
964
|
+
return hideShowMap.size === 0 || hideShowMap.has(filePath) || true; // 如果在 export 链中则已导出
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
// ── 类/枚举/mixin/extension 声明查找 ───────────────────────────
|
|
968
|
+
|
|
969
|
+
/**
|
|
970
|
+
* 检查某位置是否在 /// 文档注释行内
|
|
971
|
+
*/
|
|
972
|
+
function isInsideDocComment(cleanContent, index) {
|
|
973
|
+
const lineStart = cleanContent.lastIndexOf('\n', index) + 1;
|
|
974
|
+
const linePrefix = cleanContent.substring(lineStart, index).trimStart();
|
|
975
|
+
// 如果行以 /// 开头(去除前导空格后),则是在文档注释内
|
|
976
|
+
return linePrefix.startsWith('///');
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
function findDeclarations(cleanContent) {
|
|
980
|
+
const declarations = [];
|
|
981
|
+
const seen = new Set();
|
|
982
|
+
|
|
983
|
+
// class / abstract class / final class / sealed class / base class / interface class / mixin class
|
|
984
|
+
const classPattern = /(?:(abstract|final|sealed|base|interface)\s+)*(mixin\s+)?class\s+(\w+)/g;
|
|
985
|
+
let m;
|
|
986
|
+
while ((m = classPattern.exec(cleanContent)) !== null) {
|
|
987
|
+
// 跳过 /// 文档注释内的误匹配(如 "///This class is used...")
|
|
988
|
+
if (isInsideDocComment(cleanContent, m.index)) continue;
|
|
989
|
+
const name = m[3];
|
|
990
|
+
if (seen.has(name)) continue;
|
|
991
|
+
const isAbstract = m[1] === 'abstract';
|
|
992
|
+
const isMixin = !!m[2];
|
|
993
|
+
let type = 'class';
|
|
994
|
+
if (isMixin) type = 'mixin_class';
|
|
995
|
+
else if (isAbstract) type = 'abstract_class';
|
|
996
|
+
|
|
997
|
+
// 找到 { 的位置
|
|
998
|
+
const braceIdx = cleanContent.indexOf('{', m.index);
|
|
999
|
+
if (braceIdx < 0) continue;
|
|
1000
|
+
|
|
1001
|
+
declarations.push({
|
|
1002
|
+
type,
|
|
1003
|
+
name,
|
|
1004
|
+
declIndex: m.index,
|
|
1005
|
+
braceIndex: braceIdx,
|
|
1006
|
+
});
|
|
1007
|
+
seen.add(name);
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
// mixin Name { ... } — 跳过 "mixin class" (Dart 3 mixin class 修饰符)
|
|
1011
|
+
const mixinPattern = /\bmixin\s+(?!class\b)(\w+)\b/g;
|
|
1012
|
+
while ((m = mixinPattern.exec(cleanContent)) !== null) {
|
|
1013
|
+
if (isInsideDocComment(cleanContent, m.index)) continue;
|
|
1014
|
+
const name = m[1];
|
|
1015
|
+
if (seen.has(name)) continue;
|
|
1016
|
+
// 排除 "mixin class" 已被上面匹配
|
|
1017
|
+
const after = cleanContent.substring(m.index + m[0].length).trimStart();
|
|
1018
|
+
if (/^class\b/.test(after)) continue;
|
|
1019
|
+
|
|
1020
|
+
const braceIdx = cleanContent.indexOf('{', m.index);
|
|
1021
|
+
if (braceIdx < 0) continue;
|
|
1022
|
+
|
|
1023
|
+
declarations.push({
|
|
1024
|
+
type: 'mixin',
|
|
1025
|
+
name,
|
|
1026
|
+
declIndex: m.index,
|
|
1027
|
+
braceIndex: braceIdx,
|
|
1028
|
+
});
|
|
1029
|
+
seen.add(name);
|
|
1030
|
+
}
|
|
1031
|
+
|
|
1032
|
+
// enum Name { ... }
|
|
1033
|
+
const enumPattern = /\benum\s+(\w+)\b/g;
|
|
1034
|
+
while ((m = enumPattern.exec(cleanContent)) !== null) {
|
|
1035
|
+
if (isInsideDocComment(cleanContent, m.index)) continue;
|
|
1036
|
+
const name = m[1];
|
|
1037
|
+
if (seen.has(name)) continue;
|
|
1038
|
+
|
|
1039
|
+
const braceIdx = cleanContent.indexOf('{', m.index);
|
|
1040
|
+
if (braceIdx < 0) continue;
|
|
1041
|
+
|
|
1042
|
+
declarations.push({
|
|
1043
|
+
type: 'enum',
|
|
1044
|
+
name,
|
|
1045
|
+
declIndex: m.index,
|
|
1046
|
+
braceIndex: braceIdx,
|
|
1047
|
+
});
|
|
1048
|
+
seen.add(name);
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// extension [Name] on Type { ... }
|
|
1052
|
+
const extPattern = /\bextension\s+(\w+)?\s*(?:on\s+([\w<>\?,\s@\[\]{}.]+?))?\s*\{/g;
|
|
1053
|
+
while ((m = extPattern.exec(cleanContent)) !== null) {
|
|
1054
|
+
if (isInsideDocComment(cleanContent, m.index)) continue;
|
|
1055
|
+
const name = m[1] || `(ext_${m.index})`;
|
|
1056
|
+
if (seen.has(name)) continue;
|
|
1057
|
+
|
|
1058
|
+
declarations.push({
|
|
1059
|
+
type: 'extension',
|
|
1060
|
+
name: m[1] || '(anonymous)',
|
|
1061
|
+
onType: m[2] ? m[2].trim() : null,
|
|
1062
|
+
declIndex: m.index,
|
|
1063
|
+
braceIndex: m.index + m[0].length - 1, // { 在正则匹配的最后一个字符
|
|
1064
|
+
});
|
|
1065
|
+
seen.add(name);
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
declarations.sort((a, b) => a.declIndex - b.declIndex);
|
|
1069
|
+
return declarations;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
// ── 主解析函数 ──────────────────────────────────────────────
|
|
1073
|
+
|
|
1074
|
+
function parseLibrary(repoDir) {
|
|
1075
|
+
const mainPkgDir = detectMainPackage(repoDir);
|
|
1076
|
+
const pubspec = parsePubspec(mainPkgDir);
|
|
1077
|
+
const packageName = pubspec.name;
|
|
1078
|
+
const libraryName = packageName;
|
|
1079
|
+
|
|
1080
|
+
const entryFile = locateEntryFile(mainPkgDir, packageName);
|
|
1081
|
+
if (!entryFile) {
|
|
1082
|
+
throw new Error(`无法定位入口文件: ${mainPkgDir}/lib/${packageName}.dart`);
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// 收集 export 链可达的文件
|
|
1086
|
+
const hideShowMap = new Map();
|
|
1087
|
+
const exportedFiles = collectExportedFiles(entryFile, repoDir, new Set(), hideShowMap);
|
|
1088
|
+
|
|
1089
|
+
// 也收集所有 lib/ 下的源文件(用于 fallback)
|
|
1090
|
+
const allSources = collectAllSourceFiles(mainPkgDir);
|
|
1091
|
+
|
|
1092
|
+
// 合并去重
|
|
1093
|
+
const sourceSet = new Set([...exportedFiles, ...allSources]);
|
|
1094
|
+
|
|
1095
|
+
// 收集所有跨包子包的源文件(monorepo)
|
|
1096
|
+
for (const [filePath] of hideShowMap) {
|
|
1097
|
+
if (!sourceSet.has(filePath)) sourceSet.add(filePath);
|
|
1098
|
+
// 递归收集被 export 的文件的源码
|
|
1099
|
+
const subSources = collectAllSourceFiles(path.dirname(path.dirname(filePath)));
|
|
1100
|
+
for (const s of subSources) sourceSet.add(s);
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
const sources = [...sourceSet];
|
|
1104
|
+
const interfaces = [];
|
|
1105
|
+
const visitedClassNames = new Set();
|
|
1106
|
+
|
|
1107
|
+
for (const filePath of sources) {
|
|
1108
|
+
if (!fs.existsSync(filePath)) continue;
|
|
1109
|
+
const rawContent = readText(filePath);
|
|
1110
|
+
const cleanContent = preprocess(rawContent);
|
|
1111
|
+
|
|
1112
|
+
// 查找类/枚举/mixin/extension 声明
|
|
1113
|
+
const declarations = findDeclarations(cleanContent);
|
|
1114
|
+
|
|
1115
|
+
for (const decl of declarations) {
|
|
1116
|
+
if (visitedClassNames.has(decl.name)) continue;
|
|
1117
|
+
// 跳过私有类/枚举/mixin/extension(_ 前缀)
|
|
1118
|
+
if (decl.name.startsWith('_')) continue;
|
|
1119
|
+
visitedClassNames.add(decl.name);
|
|
1120
|
+
|
|
1121
|
+
// 提取大括号块
|
|
1122
|
+
const block = extractBraceBlock(cleanContent, decl.braceIndex);
|
|
1123
|
+
if (!block.body) continue;
|
|
1124
|
+
|
|
1125
|
+
const bodyStartLine = getLineNum(rawContent, decl.braceIndex + 1);
|
|
1126
|
+
|
|
1127
|
+
if (decl.type === 'enum') {
|
|
1128
|
+
interfaces.push(...extractEnumMembers(block.body, decl.name, filePath, repoDir, bodyStartLine, rawContent));
|
|
1129
|
+
} else {
|
|
1130
|
+
// 过滤 export hide/show
|
|
1131
|
+
const filtered = isHiddenByExport(decl.name, hideShowMap, filePath);
|
|
1132
|
+
if (filtered) continue;
|
|
1133
|
+
|
|
1134
|
+
interfaces.push(...extractClassMembers(
|
|
1135
|
+
block.body, decl.name, decl.type, filePath, repoDir,
|
|
1136
|
+
bodyStartLine, rawContent, null, hideShowMap, decl.onType
|
|
1137
|
+
));
|
|
1138
|
+
}
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// 顶层声明:先移除所有类/枚举/mixin/extension 块体,避免类内方法被误匹配为顶层函数
|
|
1142
|
+
const isExportedFile = exportedFiles.includes(filePath) || allSources.includes(filePath);
|
|
1143
|
+
if (isExportedFile) {
|
|
1144
|
+
// 收集所有声明块的范围,从 cleanContent 中移除
|
|
1145
|
+
let topLevelContent = cleanContent;
|
|
1146
|
+
const rangesToRemove = [];
|
|
1147
|
+
for (const decl of declarations) {
|
|
1148
|
+
const block = extractBraceBlock(cleanContent, decl.braceIndex);
|
|
1149
|
+
rangesToRemove.push({ start: decl.braceIndex, end: block.end });
|
|
1150
|
+
}
|
|
1151
|
+
// 从后往前移除
|
|
1152
|
+
rangesToRemove.sort((a, b) => b.start - a.start);
|
|
1153
|
+
for (const r of rangesToRemove) {
|
|
1154
|
+
topLevelContent = topLevelContent.substring(0, r.start) + ' '.repeat(r.end - r.start) + topLevelContent.substring(r.end);
|
|
1155
|
+
}
|
|
1156
|
+
interfaces.push(...extractTopLevelDeclarations(topLevelContent, rawContent, filePath, repoDir, null, hideShowMap));
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
// 去重(同一个 fullName 可能从多个文件提取)
|
|
1161
|
+
const seenFullNames = new Set();
|
|
1162
|
+
const dedupedInterfaces = [];
|
|
1163
|
+
for (const iface of interfaces) {
|
|
1164
|
+
if (!seenFullNames.has(iface.fullName)) {
|
|
1165
|
+
seenFullNames.add(iface.fullName);
|
|
1166
|
+
dedupedInterfaces.push(iface);
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
// 统计
|
|
1171
|
+
const classCount = new Set(dedupedInterfaces.map(i => i.className).filter(n => n !== null)).size;
|
|
1172
|
+
const methodCount = dedupedInterfaces.filter(i => i.kind === 'method').length;
|
|
1173
|
+
const propertyCount = dedupedInterfaces.filter(i => i.kind === 'property').length;
|
|
1174
|
+
const fieldCount = dedupedInterfaces.filter(i => i.kind === 'field').length;
|
|
1175
|
+
const constantCount = dedupedInterfaces.filter(i => i.kind === 'constant').length;
|
|
1176
|
+
|
|
1177
|
+
return {
|
|
1178
|
+
libraryName,
|
|
1179
|
+
entryFile: rel(entryFile, repoDir),
|
|
1180
|
+
nativeDPath: null,
|
|
1181
|
+
extractedAt: new Date().toISOString(),
|
|
1182
|
+
summary: {
|
|
1183
|
+
S: classCount,
|
|
1184
|
+
N: dedupedInterfaces.length,
|
|
1185
|
+
M: 0,
|
|
1186
|
+
methods: methodCount,
|
|
1187
|
+
properties: propertyCount,
|
|
1188
|
+
fields: fieldCount,
|
|
1189
|
+
constants: constantCount,
|
|
1190
|
+
},
|
|
1191
|
+
sources: sources.map(s => rel(s, repoDir)),
|
|
1192
|
+
interfaces: dedupedInterfaces,
|
|
1193
|
+
};
|
|
1194
|
+
}
|
|
1195
|
+
|
|
1196
|
+
// ── 模块导出 ───────────────────────────────────────────────
|
|
1197
|
+
|
|
1198
|
+
function generateSpec(repoDir) {
|
|
1199
|
+
if (!fs.existsSync(repoDir)) {
|
|
1200
|
+
throw new Error(`目录不存在: ${repoDir}`);
|
|
1201
|
+
}
|
|
1202
|
+
return parseLibrary(repoDir);
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
module.exports = {
|
|
1206
|
+
generateSpec,
|
|
1207
|
+
parseLibrary,
|
|
1208
|
+
detectMainPackage,
|
|
1209
|
+
locateEntryFile,
|
|
1210
|
+
parsePubspec,
|
|
1211
|
+
collectExportedFiles,
|
|
1212
|
+
preprocess,
|
|
1213
|
+
parseParameters,
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
// ── 入口 ──────────────────────────────────────────────────
|
|
1217
|
+
|
|
1218
|
+
if (require.main === module) {
|
|
1219
|
+
const args = parseArgs();
|
|
1220
|
+
const repoDir = args.repoDir || args.libroot || '';
|
|
1221
|
+
const outputPath = args.output || '';
|
|
1222
|
+
|
|
1223
|
+
if (!repoDir) {
|
|
1224
|
+
console.error('用法: node flutter-extract-interfaces.cjs --repoDir <库源码目录> --output <interface-spec.json>');
|
|
1225
|
+
process.exit(1);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
console.log(`库源码目录: ${repoDir}`);
|
|
1229
|
+
|
|
1230
|
+
try {
|
|
1231
|
+
const result = generateSpec(repoDir);
|
|
1232
|
+
|
|
1233
|
+
console.log(`解析完成: ${result.interfaces.length} 个接口`);
|
|
1234
|
+
console.log(` 库名: ${result.libraryName}`);
|
|
1235
|
+
console.log(` 入口文件: ${result.entryFile}`);
|
|
1236
|
+
console.log(` 源文件数: ${result.sources.length}`);
|
|
1237
|
+
console.log(` 类级: ${result.summary.S} | 方法: ${result.summary.methods} | 属性: ${result.summary.properties} | 字段: ${result.summary.fields} | 常量: ${result.summary.constants}`);
|
|
1238
|
+
|
|
1239
|
+
if (outputPath) {
|
|
1240
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1241
|
+
fs.writeFileSync(outputPath, JSON.stringify(result, null, 2), 'utf-8');
|
|
1242
|
+
console.log(` 输出: ${outputPath}`);
|
|
1243
|
+
} else {
|
|
1244
|
+
console.log(JSON.stringify(result, null, 2));
|
|
1245
|
+
}
|
|
1246
|
+
} catch (e) {
|
|
1247
|
+
console.error(`错误: ${e.message}`);
|
|
1248
|
+
console.error(e.stack);
|
|
1249
|
+
process.exit(1);
|
|
1250
|
+
}
|
|
1251
|
+
}
|