@ohos-cpf/3rdloop 0.0.8 → 0.0.9

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.
Files changed (219) hide show
  1. package/README.md +25 -0
  2. package/lib/cli.js +22 -5
  3. package/lib/doctor.js +111 -10
  4. package/lib/mcp.js +498 -0
  5. package/package.json +6 -2
  6. package/vendor/Archive/data/.index/meta.json +7 -0
  7. package/vendor/Archive/data/.index/search.db +0 -0
  8. package/vendor/Archive/data/docs/experiences/arkts-check-tool-pitfalls-baseline-comparison.md +13 -0
  9. package/vendor/Archive/data/docs/experiences/arkts-deep-path-import-recognition.md +31 -0
  10. package/vendor/Archive/data/docs/experiences/arkts-demo-coverage-entry-file-case-mismatch.md +25 -0
  11. package/vendor/Archive/data/docs/experiences/arkts-demo-coverage-import-trailing-slash.md +20 -0
  12. package/vendor/Archive/data/docs/experiences/arkts-library-source-files-may-be-ts.md +18 -0
  13. package/vendor/Archive/data/docs/experiences/arkts-typedarray-buffer-interface-cast-compile-fixes.md +36 -0
  14. package/vendor/Archive/data/docs/experiences/arkts-xts-compile-error-clustering-fix.md +24 -0
  15. package/vendor/Archive/data/docs/experiences/arkts-xts-doc-expected-value-runtime-probe.md +35 -0
  16. package/vendor/Archive/data/docs/experiences/arkts-xts-fixture-rawfile-testability-injection.md +33 -0
  17. package/vendor/Archive/data/docs/experiences/code-linter-chinese-comment-false-positive.md +16 -0
  18. package/vendor/Archive/data/docs/experiences/esobject-new-expression-variable-tracking.md +33 -0
  19. package/vendor/Archive/data/docs/experiences/filesystem-case-sensitivity-module-resolution.md +31 -0
  20. package/vendor/Archive/data/docs/experiences/gitcode-clone-redirect-warning-normal.md +13 -0
  21. package/vendor/Archive/data/docs/experiences/kn_msgvq1w585ec8319.md +77 -0
  22. package/vendor/Archive/data/docs/experiences/kn_msovnm6rd46de72c.md +98 -0
  23. package/vendor/Archive/data/docs/experiences/kn_mspt8vntfe708576.md +87 -0
  24. package/vendor/Archive/data/docs/experiences/kn_mspu1ei77adbbb48.md +133 -0
  25. package/vendor/Archive/data/docs/experiences/kn_mspubgak8d30bfce.md +128 -0
  26. package/vendor/Archive/data/docs/experiences/kn_mspug2de3cb1f7c3.md +115 -0
  27. package/vendor/Archive/data/docs/experiences/kn_skill_opt_001.md +38 -0
  28. package/vendor/Archive/data/docs/experiences/loop-artifacts-verification-evidence-triangle.md +19 -0
  29. package/vendor/Archive/data/docs/experiences/macos-bash-replace-build-ps1.md +19 -0
  30. package/vendor/Archive/data/docs/experiences/macos-bsd-diff-q-illegal-option.md +15 -0
  31. package/vendor/Archive/data/docs/experiences/migrate-cjs-compatible-api-version-map-limit.md +17 -0
  32. package/vendor/Archive/data/docs/experiences/mp3agic-arkts-library-behavior-quirks.md +21 -0
  33. package/vendor/Archive/data/docs/experiences/ohos-face-detection-scan-api-mapping.md +15 -0
  34. package/vendor/Archive/data/docs/experiences/pr-metadata-and-diff-integrity-verification.md +12 -0
  35. package/vendor/Archive/data/docs/experiences/rn-coverage-index-signature-pseudo-member-false-negative.md +32 -0
  36. package/vendor/Archive/data/docs/experiences/rn-memory-check-severity-calibration.md +21 -0
  37. package/vendor/Archive/data/docs/experiences/rn-platform-specific-api-exclusion-evidence.md +24 -0
  38. package/vendor/Archive/data/docs/experiences/rn-upstream-version-diff-pitfalls.md +17 -0
  39. package/vendor/Archive/data/docs/experiences/rn-version-gap-risk-rating-rules.md +13 -0
  40. package/vendor/Archive/data/docs/experiences/rn-view-lib-native-contract-diff-fabric-spec.md +16 -0
  41. package/vendor/Archive/data/docs/experiences/rnoh-adapter-package-version-field-semantics.md +18 -0
  42. package/vendor/Archive/data/docs/experiences/rnoh-arkts-sqlite-rows-array-format.md +18 -0
  43. package/vendor/Archive/data/docs/experiences/rnoh-dependency-residue-scan-strategy.md +18 -0
  44. package/vendor/Archive/data/docs/experiences/rnoh-example-local-lib-dep-use-tgz.md +53 -0
  45. package/vendor/Archive/data/docs/experiences/rnoh-example-prereserve-rntesterlist.md +17 -0
  46. package/vendor/Archive/data/docs/experiences/rnoh-example-template-compatiblesdkversion-upgrade.md +29 -0
  47. package/vendor/Archive/data/docs/experiences/rnoh-example-turbomodule-shell-adaptation-checklist.md +41 -0
  48. package/vendor/Archive/data/docs/experiences/rnoh-mcp-build-project-tool-risk.md +18 -0
  49. package/vendor/Archive/data/docs/experiences/rnoh-npm-devdependencies-version-precheck.md +29 -0
  50. package/vendor/Archive/data/docs/experiences/rnoh-repo-baseline-version-verification.md +13 -0
  51. package/vendor/Archive/data/docs/experiences/rnoh-repo-upstream-directory-mapping.md +18 -0
  52. package/vendor/Archive/data/docs/experiences/rnoh-stateless-library-zero-leak-verification.md +31 -0
  53. package/vendor/Archive/data/docs/experiences/rnoh-template-npmrc-credential-removal.md +15 -0
  54. package/vendor/Archive/data/docs/experiences/rnoh-turbomodule-codegen-wiring-verification.md +19 -0
  55. package/vendor/Archive/data/docs/experiences/rnoh-turbomodule-cpp-methodmap-compatibility-check.md +20 -0
  56. package/vendor/Archive/data/docs/experiences/scenario-doc-vs-source-signature-diff-four-patterns.md +24 -0
  57. package/vendor/Archive/data/docs/experiences/static-analysis-grep-cross-validation.md +29 -0
  58. package/vendor/Archive/demo.js +489 -0
  59. package/vendor/Archive/index.js +43 -0
  60. package/vendor/Archive/package.json +16 -0
  61. package/vendor/Archive/src/constants.js +83 -0
  62. package/vendor/Archive/src/data/stopwords.txt +110 -0
  63. package/vendor/Archive/src/data/synonyms.json +25 -0
  64. package/vendor/Archive/src/data/terms.txt +59 -0
  65. package/vendor/Archive/src/db/sqlite-backend.js +312 -0
  66. package/vendor/Archive/src/db/sqlite-schema.js +64 -0
  67. package/vendor/Archive/src/engine.js +501 -0
  68. package/vendor/Archive/src/index/index-manager.js +203 -0
  69. package/vendor/Archive/src/index/index-state.js +94 -0
  70. package/vendor/Archive/src/index/search-text-builder.js +106 -0
  71. package/vendor/Archive/src/index.js +22 -0
  72. package/vendor/Archive/src/parser/api-identifiers.js +121 -0
  73. package/vendor/Archive/src/parser/api-suffixes.js +48 -0
  74. package/vendor/Archive/src/parser/heading-normalize.js +40 -0
  75. package/vendor/Archive/src/parser/markdown-splitter.js +215 -0
  76. package/vendor/Archive/src/search/catalog-routing.js +87 -0
  77. package/vendor/Archive/src/search/fts-search.js +289 -0
  78. package/vendor/Archive/src/search/query-normalizer.js +159 -0
  79. package/vendor/Archive/src/search/query-rewriter.js +72 -0
  80. package/vendor/Archive/src/search/search-engine.js +72 -0
  81. package/vendor/Archive/src/search/snippet.js +64 -0
  82. package/vendor/Archive/src/store/dir-store.js +103 -0
  83. package/vendor/Archive/src/store/doc-store.js +36 -0
  84. package/vendor/Archive/src/tokenizer/lexicon.js +151 -0
  85. package/vendor/Archive/src/tokenizer/tokenizer.js +167 -0
  86. package/vendor/Archive/src/types.js +86 -0
  87. package/vendor/Archive/src/utils/hash.js +16 -0
  88. package/vendor/Archive/src/utils/lock.js +73 -0
  89. package/vendor/Archive/src/utils/path-safety.js +96 -0
  90. package/vendor/Archive/src/utils/paths.js +39 -0
  91. package/vendor/BadCase/rn/INDEX.md +127 -0
  92. 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
  93. 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
  94. 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
  95. package/vendor/BadCase/rn/cases/KI-004-wechat/345/233/236/350/260/203/346/227/266/345/272/217.md +47 -0
  96. 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
  97. 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
  98. 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
  99. 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
  100. package/vendor/BadCase/rn/cases/KI-009-pager-view/345/265/214/345/245/227/346/273/221/345/212/250.md +38 -0
  101. package/vendor/BadCase/rn/cases/KI-010-screens/346/211/213/345/212/277/345/206/262/347/252/201.md +48 -0
  102. 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
  103. package/vendor/BadCase/rn/cases/KI-012-qr-camera/346/250/252/345/261/217/351/200/202/351/205/215.md +59 -0
  104. 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
  105. package/vendor/BadCase/rn/cases/KI-014-amap-geolocation/345/233/236/350/260/203/347/274/272/345/244/261.md +42 -0
  106. 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
  107. 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
  108. 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
  109. package/vendor/BadCase/rn/cases/KI-018-modal/345/274/271/347/252/227/351/227/252/347/203/201.md +53 -0
  110. 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
  111. 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
  112. 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
  113. 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
  114. 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
  115. package/vendor/BadCase/rn/generalization-log.md +55 -0
  116. package/vendor/BadCase/rn/pr-info/rntpc_react-native-SmartRefreshLayout-32.json +328 -0
  117. package/vendor/BadCase/rn/pr-info/rntpc_react-native-SmartRefreshLayout-32.md +382 -0
  118. package/vendor/BadCase/rn/pr-info/rntpc_react-native-amap-geolocation-27.json +1351 -0
  119. package/vendor/BadCase/rn/pr-info/rntpc_react-native-amap-geolocation-27.md +1176 -0
  120. package/vendor/BadCase/rn/pr-info/rntpc_react-native-audio-recorder-player-22.json +538 -0
  121. package/vendor/BadCase/rn/pr-info/rntpc_react-native-audio-recorder-player-22.md +1488 -0
  122. package/vendor/BadCase/rn/pr-info/rntpc_react-native-blob-util-23.json +211 -0
  123. package/vendor/BadCase/rn/pr-info/rntpc_react-native-blob-util-23.md +35 -0
  124. package/vendor/BadCase/rn/pr-info/rntpc_react-native-code-push-17.json +2048 -0
  125. package/vendor/BadCase/rn/pr-info/rntpc_react-native-code-push-17.md +3866 -0
  126. package/vendor/BadCase/rn/pr-info/rntpc_react-native-fast-image-102.json +198 -0
  127. package/vendor/BadCase/rn/pr-info/rntpc_react-native-fast-image-102.md +40 -0
  128. package/vendor/BadCase/rn/pr-info/rntpc_react-native-fs-38.json +751 -0
  129. package/vendor/BadCase/rn/pr-info/rntpc_react-native-fs-38.md +883 -0
  130. package/vendor/BadCase/rn/pr-info/rntpc_react-native-geolocation-24.json +1544 -0
  131. package/vendor/BadCase/rn/pr-info/rntpc_react-native-geolocation-24.md +2598 -0
  132. package/vendor/BadCase/rn/pr-info/rntpc_react-native-modal-20.json +246 -0
  133. package/vendor/BadCase/rn/pr-info/rntpc_react-native-modal-20.md +554 -0
  134. package/vendor/BadCase/rn/pr-info/rntpc_react-native-netinfo-31.json +211 -0
  135. package/vendor/BadCase/rn/pr-info/rntpc_react-native-netinfo-31.md +54 -0
  136. package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-45.json +1626 -0
  137. package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-45.md +2213 -0
  138. package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-51.json +208 -0
  139. package/vendor/BadCase/rn/pr-info/rntpc_react-native-orientation-locker-51.md +77 -0
  140. package/vendor/BadCase/rn/pr-info/rntpc_react-native-pager-view-32.json +377 -0
  141. package/vendor/BadCase/rn/pr-info/rntpc_react-native-pager-view-32.md +174 -0
  142. package/vendor/BadCase/rn/pr-info/rntpc_react-native-qr-decode-image-camera-37.json +232 -0
  143. package/vendor/BadCase/rn/pr-info/rntpc_react-native-qr-decode-image-camera-37.md +194 -0
  144. package/vendor/BadCase/rn/pr-info/rntpc_react-native-reanimated-82.json +198 -0
  145. package/vendor/BadCase/rn/pr-info/rntpc_react-native-reanimated-82.md +67 -0
  146. package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-87.json +198 -0
  147. package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-87.md +38 -0
  148. package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-93.json +208 -0
  149. package/vendor/BadCase/rn/pr-info/rntpc_react-native-screens-93.md +34 -0
  150. package/vendor/BadCase/rn/pr-info/rntpc_react-native-spring-scrollview-38.json +222 -0
  151. package/vendor/BadCase/rn/pr-info/rntpc_react-native-spring-scrollview-38.md +56 -0
  152. package/vendor/BadCase/rn/pr-info/rntpc_react-native-svg-117.json +463 -0
  153. package/vendor/BadCase/rn/pr-info/rntpc_react-native-svg-117.md +383 -0
  154. package/vendor/BadCase/rn/pr-info/rntpc_react-native-video-66.json +464 -0
  155. package/vendor/BadCase/rn/pr-info/rntpc_react-native-video-66.md +565 -0
  156. package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-112.json +198 -0
  157. package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-112.md +47 -0
  158. package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-121.json +232 -0
  159. package/vendor/BadCase/rn/pr-info/rntpc_react-native-webview-121.md +422 -0
  160. package/vendor/BadCase/rn/pr-info/rntpc_react-native-wechat-lib-15.json +295 -0
  161. package/vendor/BadCase/rn/pr-info/rntpc_react-native-wechat-lib-15.md +1038 -0
  162. 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
  163. 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
  164. 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
  165. 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
  166. 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
  167. 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
  168. 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
  169. 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
  170. 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
  171. 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
  172. 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
  173. 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
  174. 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
  175. 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
  176. 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
  177. 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
  178. 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
  179. 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
  180. 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
  181. package/vendor/MCP/README.md +490 -0
  182. package/vendor/MCP/config.json +29 -0
  183. package/vendor/MCP/package.json +23 -0
  184. package/vendor/MCP/scripts/analyze-demo-coverage.cjs +1203 -0
  185. package/vendor/MCP/scripts/analyze-xts-coverage.cjs +1245 -0
  186. package/vendor/MCP/scripts/deveco_docs.js +247 -0
  187. package/vendor/MCP/scripts/extract-interfaces.cjs +1487 -0
  188. package/vendor/MCP/scripts/flutter-analyze-demo-coverage.cjs +666 -0
  189. package/vendor/MCP/scripts/flutter-analyze-xts-coverage.cjs +684 -0
  190. package/vendor/MCP/scripts/flutter-extract-interfaces.cjs +1251 -0
  191. package/vendor/MCP/scripts/flutter-render-spec-doc.cjs +351 -0
  192. package/vendor/MCP/scripts/render-spec-doc.cjs +347 -0
  193. package/vendor/MCP/scripts/rn-analyze-demo-coverage.cjs +645 -0
  194. package/vendor/MCP/scripts/rn-analyze-test-coverage.cjs +614 -0
  195. package/vendor/MCP/scripts/rn-extract-interfaces.cjs +1462 -0
  196. package/vendor/MCP/scripts/rn-render-spec-doc.cjs +327 -0
  197. package/vendor/MCP/scripts/rn-scan-exports.cjs +1336 -0
  198. package/vendor/MCP/src/client/MCPClientPool.js +220 -0
  199. package/vendor/MCP/src/config/ConfigLoader.js +189 -0
  200. package/vendor/MCP/src/executor/ScriptExecutor.js +110 -0
  201. package/vendor/MCP/src/gateway/Gateway.js +451 -0
  202. package/vendor/MCP/src/index.js +87 -0
  203. package/vendor/MCP/src/library/KBToolProvider.js +229 -0
  204. package/vendor/MCP/src/library/logger.js +60 -0
  205. package/vendor/MCP/src/registry/ScriptRegistry.js +238 -0
  206. package/vendor/MCP/src/registry/ToolRegistry.js +111 -0
  207. package/vendor/MCP/src/types.js +115 -0
  208. package/vendor/Server/Skills/arkts-library-test-coverage-check/references/MCP_TOOL_USAGE.md +4 -2
  209. package/vendor/Server/Skills/arkts-library-xts-coverage/SKILL.md +3 -3
  210. package/vendor/Server/Skills/knowledge-import/SKILL.md +1 -1
  211. package/vendor/Server/Skills/knowledge-import/references/MCP_TOOL_USAGE.md +12 -4
  212. package/vendor/Server/Skills/ohos-lib-pr-push/SKILL.md +5 -4
  213. package/vendor/Server/Skills/rn-library-issue-generalizer/SKILL.md +2 -2
  214. package/vendor/Server/Skills/rn-library-known-issue-check/SKILL.md +2 -2
  215. package/vendor/Server/Skills/rnoh-lib-demo-coverage/SKILL.md +1 -2
  216. package/vendor/Server/Skills/rnoh-lib-test-align/references/port-test-demo-guide.md +1 -1
  217. package/vendor/Server/Skills/rnoh-lib-xts-coverage/SKILL.md +1 -2
  218. package/vendor/Server/package.json +5 -1
  219. package/vendor/VERSION +3 -3
@@ -0,0 +1,1245 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @tool arkts_analyze_xts_coverage
4
+ * @description 分析鸿蒙 ArkTS 三方库的 XTS 测试覆盖率。从库源码提取接口规格,扫描 entry/src/ohosTest 下的 XTS 测试源码,执行 AST/正则双层覆盖率匹配,输出 Markdown 覆盖率报告。用于评估 XTS 测试用例对库 API 的覆盖程度。
5
+ * @param libroot: string - 库源码根目录路径(包含 library/ 和 entry/ 子目录)
6
+ * @param spec: string - 已有的 interface-spec.json 路径(可选,与 libroot 二选一)
7
+ * @hidden
8
+ * @param output: string - 输出 Markdown 报告路径(可选,默认 03-xxx-XTS覆盖率报告.md)
9
+ * @param mode: string - 匹配模式: auto(默认) | ast | regex(可选)
10
+ *
11
+ * analyze-xts-coverage.cjs — XTS 覆盖率分析专用脚本
12
+ *
13
+ * 从库源码提取接口规格,针对 XTS 测试源码目录执行 AST/正则双层覆盖率匹配,
14
+ * 直接渲染输出 Markdown 覆盖率报告。
15
+ *
16
+ * 用法:
17
+ * 方式一(读 JSON 中间件):
18
+ * node analyze-xts-coverage.cjs --spec <interface-spec.json> --libroot <库根目录> \
19
+ * [--output <report.md>] [--title <标题>] [--mode auto|ast|regex]
20
+ *
21
+ * 方式二(直接从源码提取,无需 JSON 中间件):
22
+ * node analyze-xts-coverage.cjs --libroot <库根目录> \
23
+ * [--output <report.md>] [--title <标题>] [--mode auto|ast|regex]
24
+ *
25
+ * 参数:
26
+ * --spec interface-spec.json 路径(由 extract-interfaces.cjs 生成)
27
+ * --libroot 库根目录(XTS 源码默认取 <libroot>/entry/src/ohosTest)
28
+ * --output (可选)输出 Markdown 报告路径,默认 03-xxx-XTS覆盖率报告.md
29
+ * --title (可选)报告标题
30
+ * --mode (可选)匹配模式: auto(默认) | ast | regex
31
+ * --lib (可选)库名覆盖,默认从 spec 中读取
32
+ *
33
+ * 使用示例:
34
+ * # 直接从源码生成 XTS 覆盖率报告(推荐,一步到位)
35
+ * node analyze-xts-coverage.cjs --libroot /path/to/lottie_turbo --output 03-lottie_turbo-XTS覆盖率报告.md
36
+ *
37
+ * # 从已有 JSON 中间件生成(需先用 extract-interfaces.cjs 生成 JSON)
38
+ * node analyze-xts-coverage.cjs --spec lottie_turbo-interface-spec.json --libroot /path/to/lottie_turbo
39
+ *
40
+ * # 库源码目录结构约定:
41
+ * # /path/to/lottie_turbo/
42
+ * # ├── library/ ← 库源码(Index.ets 入口)
43
+ * # │ ├── Index.ets
44
+ * # │ ├── src/main/ets/
45
+ * # │ └── src/main/cpp/types/.../Index.d.ts ← Native .so 声明
46
+ * # └── entry/src/ohosTest/ ← XTS 测试源码(被分析的目录)
47
+ */
48
+
49
+ const fs = require('fs');
50
+ const path = require('path');
51
+ const { generateSpec } = require('./extract-interfaces.cjs');
52
+
53
+ // ════════════════════════════════════════════════════════════
54
+ // Part 0: TypeScript 加载 + 参数解析 + 工具函数
55
+ // ════════════════════════════════════════════════════════════
56
+
57
+ function loadTypeScript() {
58
+ // 最优先:libraryTestScan.js 通过环境变量注入的精确路径
59
+ if (process.env.TYPESCRIPT_PATH) {
60
+ try {
61
+ const ts = require(process.env.TYPESCRIPT_PATH);
62
+ if (ts && typeof ts.createProgram === 'function') return ts;
63
+ } catch (e) { /* continue */ }
64
+ }
65
+ const candidates = [
66
+ path.join(__dirname, '..', 'node_modules', 'typescript'),
67
+ path.join(__dirname, 'node_modules', 'typescript'),
68
+ path.join(__dirname, '..', '..', '3rdLibraryLoop', 'Server', 'Skills', 'ArkTsLibraryXTSCoverageSkill', 'node_modules', 'typescript'),
69
+ path.join(__dirname, '..', 'Server', 'Skills', 'ArkTsLibraryXTSCoverageSkill', 'node_modules', 'typescript'),
70
+ ];
71
+ for (const p of candidates) {
72
+ try {
73
+ const ts = require(p);
74
+ if (ts && typeof ts.createProgram === 'function') return ts;
75
+ } catch (e) { /* continue */ }
76
+ }
77
+ try {
78
+ const ts = require('typescript');
79
+ if (ts && typeof ts.createProgram === 'function') return ts;
80
+ } catch (e) { /* continue */ }
81
+ return null;
82
+ }
83
+
84
+ const ts = loadTypeScript();
85
+
86
+ function parseArgs() {
87
+ const args = process.argv.slice(2);
88
+ // MCP Gateway 模式:第一个参数是 JSON 字符串
89
+ if (args.length > 0 && args[0].startsWith('{')) {
90
+ try { return JSON.parse(args[0]); } catch (e) { /* 降级到 CLI 模式 */ }
91
+ }
92
+ const parsed = {};
93
+ for (let i = 0; i < args.length; i++) {
94
+ if (args[i].startsWith('--')) {
95
+ parsed[args[i].substring(2)] = args[i + 1];
96
+ i++;
97
+ }
98
+ }
99
+ return parsed;
100
+ }
101
+
102
+ function escapeRegex(str) {
103
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
104
+ }
105
+
106
+ function escapeMd(text) {
107
+ if (!text || text === '-') return '-';
108
+ return text.replace(/\|/g, '\\|').replace(/\n/g, ' ');
109
+ }
110
+
111
+ function formatParams(params) {
112
+ if (!params || params.length === 0) return '';
113
+ return params.map(p => {
114
+ const opt = p.optional ? '?' : '';
115
+ return `${p.name}${opt}: ${escapeMd(p.type)}`;
116
+ }).join(', ');
117
+ }
118
+
119
+ function coverageBadge(rate) {
120
+ const r = parseFloat(rate);
121
+ if (r >= 80) return '高';
122
+ if (r >= 50) return '中';
123
+ if (r >= 20) return '低';
124
+ return '极低';
125
+ }
126
+
127
+ // ════════════════════════════════════════════════════════════
128
+ // Part A: 文件扫描 + 正则匹配引擎
129
+ // ════════════════════════════════════════════════════════════
130
+
131
+ const EXCLUDE_TOP_DIRS = ['entryability', 'entrybackupability', 'model', 'util', 'ohosTest'];
132
+
133
+ function walkDir(dir, excludeTopDirs, srcRoot) {
134
+ const results = [];
135
+ if (!fs.existsSync(dir)) return results;
136
+ const relDir = path.relative(srcRoot, dir);
137
+ const topDir = relDir.split(path.sep)[0] || '';
138
+ if (relDir !== '' && excludeTopDirs.includes(topDir)) return results;
139
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
140
+ for (const entry of entries) {
141
+ const fullPath = path.join(dir, entry.name);
142
+ if (entry.isDirectory()) {
143
+ results.push(...walkDir(fullPath, excludeTopDirs, srcRoot));
144
+ } else if (entry.isFile() && (entry.name.endsWith('.ets') || entry.name.endsWith('.ts'))) {
145
+ if (entry.name === 'Index.ets' || entry.name === 'index.ts') {
146
+ const relPath = path.relative(srcRoot, fullPath);
147
+ const depth = relPath.split(path.sep).length;
148
+ if (depth === 1) continue;
149
+ }
150
+ results.push(fullPath);
151
+ }
152
+ }
153
+ return results;
154
+ }
155
+
156
+ /** 提取 import 的具名符号 */
157
+ function extractImportsRegex(content, libName) {
158
+ const importedSymbols = [];
159
+ const importRegex = /import\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?\{([^}]*?)\}\s*from\s*['"]([^'"]+)['"]/gs;
160
+ let match;
161
+ while ((match = importRegex.exec(content)) !== null) {
162
+ const symbolsStr = match[1];
163
+ const fromLib = match[2];
164
+ // 归一化:兼容尾部斜杠写法(如 '@ohos/xxx/')与深层路径导入(如 '@ohos/xxx/src/...')
165
+ const fromNorm = fromLib.replace(/\/+$/, '');
166
+ if (fromNorm === libName || fromNorm.endsWith('/' + libName.split('/').pop()) || fromNorm.startsWith(libName + '/')) {
167
+ const symbols = symbolsStr.split(',').map(s => {
168
+ const trimmed = s.trim().replace(/\n/g, '').replace(/\s+/g, ' ');
169
+ const aliasMatch = trimmed.match(/(\w+)\s+as\s+(\w+)/);
170
+ if (aliasMatch) return { name: aliasMatch[1], alias: aliasMatch[2] };
171
+ return { name: trimmed, alias: trimmed };
172
+ }).filter(s => s.name.length > 0);
173
+ importedSymbols.push(...symbols);
174
+ }
175
+ }
176
+ return importedSymbols;
177
+ }
178
+
179
+ /** 检测纯默认导入 */
180
+ function extractDefaultImportRegex(content, libName) {
181
+ const libSuffix = libName.split('/').pop();
182
+ const defaultRegex = new RegExp(
183
+ `import\\s+(?:type\\s+)?([A-Za-z_$][\\w$]*)\\s+from\\s*['"](?:${escapeRegex(libName)}|[^'"]*${escapeRegex(libSuffix)})(?:/[^'"]*)?['"]`,
184
+ 'g'
185
+ );
186
+ let match;
187
+ while ((match = defaultRegex.exec(content)) !== null) {
188
+ const afterMatch = content.slice(match.index + match[0].length);
189
+ if (!/^\s*,\s*\{/.test(afterMatch)) return match[1];
190
+ }
191
+ return null;
192
+ }
193
+
194
+ /** 追踪实例变量 → 类名映射 */
195
+ function trackInstanceVariablesRegex(content, importedSymbols, defaultAlias, allClassNames) {
196
+ const varMap = {};
197
+ const classSet = new Set(allClassNames);
198
+
199
+ for (const sym of importedSymbols) {
200
+ const className = sym.alias;
201
+ const patterns = [
202
+ new RegExp(`(?:let|const|var)\\s+(\\w+)\\s*:\\s*${className}\\s*=`, 'g'),
203
+ new RegExp(`(?:let|const|var)\\s+(\\w+)\\s*=\\s*new\\s+${className}\\s*\\(`, 'g'),
204
+ new RegExp(`(?:let|const|var)\\s+(\\w+)\\s*=\\s*${className}\\s*\\.\\s*getInstance\\s*\\(`, 'g'),
205
+ new RegExp(`(?:private|public|protected|@State)\\s+(?:\\w+\\s+)?(\\w+)\\s*:\\s*${className}\\b`, 'g'),
206
+ new RegExp(`(?:private|public|protected)\\s+(\\w+)\\s*:\\s*${className}\\s*\\|`, 'g'),
207
+ new RegExp(`\\(\\s*(\\w+)\\s*:\\s*${className}\\s*\\)`, 'g'),
208
+ new RegExp(`\\(\\s*(\\w+)\\s*:\\s*${className}\\s*,`, 'g'),
209
+ ];
210
+ for (const regex of patterns) {
211
+ let m;
212
+ while ((m = regex.exec(content)) !== null) {
213
+ const varName = m[1];
214
+ if (varName && !varMap[varName]) varMap[varName] = [];
215
+ if (varName && !varMap[varName].includes(className)) varMap[varName].push(className);
216
+ }
217
+ }
218
+ }
219
+
220
+ if (defaultAlias) {
221
+ const nsPatterns = [
222
+ new RegExp(`(?:let|const|var)\\s+(\\w+)\\s*=\\s*${escapeRegex(defaultAlias)}\\s*\\.\\s*(${[...classSet].map(escapeRegex).join('|')})\\s*\\.\\s*\\w+\\s*\\(`, 'g'),
223
+ new RegExp(`(?:let|const|var)\\s+(\\w+)\\s*=\\s*new\\s+${escapeRegex(defaultAlias)}\\s*\\.\\s*(${[...classSet].map(escapeRegex).join('|')})\\s*\\(`, 'g'),
224
+ new RegExp(`(?:let|const|var)\\s+(\\w+)\\s*:\\s*${escapeRegex(defaultAlias)}\\s*\\.\\s*(${[...classSet].map(escapeRegex).join('|')})\\b`, 'g'),
225
+ ];
226
+ for (const regex of nsPatterns) {
227
+ let m;
228
+ while ((m = regex.exec(content)) !== null) {
229
+ const varName = m[1];
230
+ const cls = m[2];
231
+ if (varName && !varMap[varName]) varMap[varName] = [];
232
+ if (varName && !varMap[varName].includes(cls)) varMap[varName].push(cls);
233
+ }
234
+ }
235
+ }
236
+
237
+ return varMap;
238
+ }
239
+
240
+ /** 正则覆盖率匹配核心 */
241
+ function matchCoverageRegex(interfaces, files, libName) {
242
+ const fileCoverageMap = {};
243
+ const demoFileCount = files.length;
244
+ const allClassNames = [...new Set(interfaces.map(i => i.className).filter(n => n !== null))];
245
+ let importedFileCount = 0;
246
+
247
+ for (const filePath of files) {
248
+ const content = fs.readFileSync(filePath, 'utf-8');
249
+ const fileName = path.basename(filePath);
250
+ const importedSymbols = extractImportsRegex(content, libName);
251
+ const defaultAlias = extractDefaultImportRegex(content, libName);
252
+
253
+ if (importedSymbols.length === 0 && !defaultAlias) continue;
254
+ importedFileCount++;
255
+
256
+ const varMap = trackInstanceVariablesRegex(content, importedSymbols, defaultAlias, allClassNames);
257
+ const fileCovered = [];
258
+
259
+ for (const iface of interfaces) {
260
+ if (iface.className === null) {
261
+ if (iface.kind === 'function') {
262
+ const regex = new RegExp(`\\b${escapeRegex(iface.memberName)}\\s*\\(`, 'g');
263
+ if (regex.test(content)) fileCovered.push(iface.fullName);
264
+ } else if (iface.kind === 'variable') {
265
+ const regex = new RegExp(`\\b${escapeRegex(iface.memberName)}\\b`, 'g');
266
+ if (regex.test(content)) fileCovered.push(iface.fullName);
267
+ }
268
+ continue;
269
+ }
270
+
271
+ const classNameUsed = importedSymbols.find(s => s.name === iface.className || s.alias === iface.className);
272
+
273
+ if (defaultAlias && !fileCovered.includes(iface.fullName)) {
274
+ const ns = escapeRegex(defaultAlias);
275
+ const cls = escapeRegex(iface.className);
276
+ const mem = escapeRegex(iface.memberName);
277
+
278
+ if (iface.isStatic) {
279
+ if (iface.kind === 'method') {
280
+ const nsStaticMethod = new RegExp(`${ns}\\s*\\.\\s*${cls}\\s*\\.\\s*${mem}\\s*\\(`, 'g');
281
+ if (nsStaticMethod.test(content)) { fileCovered.push(iface.fullName); continue; }
282
+ } else if (iface.kind === 'property') {
283
+ const nsStaticProp = new RegExp(`${ns}\\s*\\.\\s*${cls}\\s*\\.\\s*${mem}\\b`, 'g');
284
+ if (nsStaticProp.test(content)) { fileCovered.push(iface.fullName); continue; }
285
+ }
286
+ } else {
287
+ if (iface.kind === 'method') {
288
+ const nsInstMethod = new RegExp(`${ns}\\s*\\.\\s*${cls}\\s*\\.\\s*${mem}\\s*\\(`, 'g');
289
+ if (nsInstMethod.test(content)) { fileCovered.push(iface.fullName); continue; }
290
+ } else if (iface.kind === 'property') {
291
+ const nsInstProp = new RegExp(`${ns}\\s*\\.\\s*${cls}\\s*\\.\\s*${mem}\\b`, 'g');
292
+ if (nsInstProp.test(content)) { fileCovered.push(iface.fullName); continue; }
293
+ }
294
+ }
295
+ if (iface.kind === 'method' || iface.kind === 'property') {
296
+ for (const [varName, classNames] of Object.entries(varMap)) {
297
+ if (classNames.includes(iface.className)) {
298
+ if (iface.kind === 'method') {
299
+ const instRegex = new RegExp(`(?:this\\s*\\.\\s*)?${escapeRegex(varName)}\\s*\\.\\s*${mem}\\s*\\(`, 'g');
300
+ if (instRegex.test(content)) { fileCovered.push(iface.fullName); break; }
301
+ } else if (iface.kind === 'property') {
302
+ const instRegex = new RegExp(`(?:this\\s*\\.\\s*)?${escapeRegex(varName)}\\s*\\.\\s*${mem}\\b`, 'g');
303
+ if (instRegex.test(content)) { fileCovered.push(iface.fullName); break; }
304
+ }
305
+ }
306
+ }
307
+ if (fileCovered.includes(iface.fullName)) continue;
308
+ }
309
+ }
310
+
311
+ if (!classNameUsed) continue;
312
+
313
+ const ca = classNameUsed.alias;
314
+
315
+ if (iface.kind === 'enumValue') {
316
+ const regex = new RegExp(`${escapeRegex(ca)}\\s*\\.\\s*${escapeRegex(iface.memberName)}\\b`, 'g');
317
+ if (regex.test(content)) fileCovered.push(iface.fullName);
318
+ continue;
319
+ }
320
+
321
+ if (iface.isStatic) {
322
+ if (iface.kind === 'method') {
323
+ const regex = new RegExp(`${escapeRegex(ca)}\\s*\\.\\s*${escapeRegex(iface.memberName)}\\s*\\(`, 'g');
324
+ if (regex.test(content)) fileCovered.push(iface.fullName);
325
+ } else if (iface.kind === 'property') {
326
+ const regex = new RegExp(`${escapeRegex(ca)}\\s*\\.\\s*${escapeRegex(iface.memberName)}\\b`, 'g');
327
+ if (regex.test(content)) fileCovered.push(iface.fullName);
328
+ }
329
+ continue;
330
+ }
331
+
332
+ if (iface.kind === 'method' || iface.kind === 'property') {
333
+ const chainRegex = new RegExp(
334
+ `${escapeRegex(ca)}\\s*\\.\\s*getInstance\\s*\\(\\s*\\)[\\s\\S]*?\\.\\s*${escapeRegex(iface.memberName)}\\s*\\(`,
335
+ 'g'
336
+ );
337
+ if (chainRegex.test(content)) { fileCovered.push(iface.fullName); continue; }
338
+
339
+ for (const [varName, classNames] of Object.entries(varMap)) {
340
+ if (classNames.includes(ca)) {
341
+ if (iface.kind === 'method') {
342
+ const instRegex = new RegExp(`(?:this\\s*\\.\\s*)?${escapeRegex(varName)}\\s*\\.\\s*${escapeRegex(iface.memberName)}\\s*\\(`, 'g');
343
+ if (instRegex.test(content)) { fileCovered.push(iface.fullName); break; }
344
+ } else if (iface.kind === 'property') {
345
+ const instRegex = new RegExp(`(?:this\\s*\\.\\s*)?${escapeRegex(varName)}\\s*\\.\\s*${escapeRegex(iface.memberName)}\\b`, 'g');
346
+ if (instRegex.test(content)) { fileCovered.push(iface.fullName); break; }
347
+ }
348
+ }
349
+ }
350
+
351
+ const directRegex = new RegExp(`${escapeRegex(ca)}\\s*\\.\\s*${escapeRegex(iface.memberName)}\\s*\\(`, 'g');
352
+ if (directRegex.test(content)) { fileCovered.push(iface.fullName); continue; }
353
+ }
354
+
355
+ if (iface.kind === 'field') {
356
+ const regex = new RegExp(`\\b${escapeRegex(iface.memberName)}\\b`, 'g');
357
+ if (regex.test(content)) fileCovered.push(iface.fullName);
358
+ continue;
359
+ }
360
+ }
361
+
362
+ // 对象字面量属性检测
363
+ for (const iface of interfaces) {
364
+ if ((iface.kind === 'property' || iface.kind === 'field' || iface.kind === 'method') && !fileCovered.includes(iface.fullName)) {
365
+ if (iface.memberName && iface.memberName.length >= 2) {
366
+ const objLitRegex = new RegExp(`\\b${escapeRegex(iface.memberName)}\\??\\s*:`, 'g');
367
+ if (objLitRegex.test(content)) fileCovered.push(iface.fullName);
368
+ }
369
+ }
370
+ }
371
+
372
+ // 默认导入命名空间的宽松兜底匹配
373
+ if (defaultAlias) {
374
+ for (const iface of interfaces) {
375
+ if (fileCovered.includes(iface.fullName)) continue;
376
+ if (!iface.className) continue;
377
+ if (iface.isStatic) continue;
378
+ const mem = escapeRegex(iface.memberName);
379
+ if (iface.kind === 'method') {
380
+ const looseMethodRegex = new RegExp(`\\.${mem}\\s*\\(`, 'g');
381
+ if (looseMethodRegex.test(content)) fileCovered.push(iface.fullName);
382
+ } else if (iface.kind === 'property' || iface.kind === 'field') {
383
+ const loosePropRegex = new RegExp(`\\.${mem}\\b`, 'g');
384
+ if (loosePropRegex.test(content)) fileCovered.push(iface.fullName);
385
+ }
386
+ }
387
+ }
388
+
389
+ for (const fullName of fileCovered) {
390
+ if (!fileCoverageMap[fileName]) fileCoverageMap[fileName] = { count: 0, interfaces: [] };
391
+ fileCoverageMap[fileName].count++;
392
+ fileCoverageMap[fileName].interfaces.push(fullName);
393
+ }
394
+ }
395
+
396
+ const regexResult = aggregateResults(interfaces, fileCoverageMap, {}, libName, demoFileCount);
397
+ regexResult.importedFileCount = importedFileCount;
398
+ return regexResult;
399
+ }
400
+
401
+ // ════════════════════════════════════════════════════════════
402
+ // Part B: AST 匹配引擎
403
+ // ════════════════════════════════════════════════════════════
404
+
405
+ function extractImportedSymbolsAST(sourceFile, libName) {
406
+ const symbols = new Map();
407
+ for (const stmt of sourceFile.statements) {
408
+ if (!ts.isImportDeclaration(stmt)) continue;
409
+ const moduleSpec = stmt.moduleSpecifier;
410
+ if (!moduleSpec || !ts.isStringLiteral(moduleSpec)) continue;
411
+ const fromLib = moduleSpec.text;
412
+ // 归一化:兼容尾部斜杠写法(如 '@ohos/xxx/',既不等于库名也不以 '/xxx' 结尾,
413
+ // 原样比较会导致所有 import 被静默跳过 → 0% 误报)与深层路径导入
414
+ const fromNorm = fromLib.replace(/\/+$/, '');
415
+ if (fromNorm !== libName && !fromNorm.endsWith('/' + libName.split('/').pop()) && !fromNorm.startsWith(libName + '/')) continue;
416
+ const importClause = stmt.importClause;
417
+ if (!importClause) continue;
418
+ if (importClause.name) {
419
+ symbols.set('default', importClause.name.text);
420
+ }
421
+ if (importClause.namedBindings && ts.isNamedImports(importClause.namedBindings)) {
422
+ for (const element of importClause.namedBindings.elements) {
423
+ const name = element.propertyName ? element.propertyName.text : element.name.text;
424
+ const alias = element.name.text;
425
+ symbols.set(name, alias);
426
+ }
427
+ }
428
+ }
429
+ return symbols;
430
+ }
431
+
432
+ function trackInstanceVariablesAST(sourceFile, checker, importedSymbols) {
433
+ const varMap = {};
434
+
435
+ function visit(node) {
436
+ if (ts.isVariableDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
437
+ trackVar(node.name.text, node);
438
+ // 语法级兜底:不依赖 checker 类型推断,直接从 new 表达式的 callee 解析类名
439
+ // 覆盖 new X<...>() 泛型构造与 let x: ESObject = new X() 声明类型非类名的场景
440
+ if (node.initializer && ts.isNewExpression(node.initializer)) {
441
+ const callee = node.initializer.expression;
442
+ if (callee && ts.isIdentifier(callee) && importedSymbols.has(callee.text)) {
443
+ const displayName = importedSymbols.get(callee.text) || callee.text;
444
+ if (!varMap[node.name.text]) varMap[node.name.text] = [];
445
+ if (!varMap[node.name.text].includes(displayName)) varMap[node.name.text].push(displayName);
446
+ }
447
+ }
448
+ }
449
+ if (ts.isPropertyDeclaration(node) && node.name && ts.isIdentifier(node.name)) {
450
+ trackVar(node.name.text, node);
451
+ }
452
+ if (ts.isParameter(node) && node.name && ts.isIdentifier(node.name)) {
453
+ trackVar(node.name.text, node);
454
+ }
455
+ ts.forEachChild(node, visit);
456
+ }
457
+
458
+ function trackVar(varName, node) {
459
+ try {
460
+ const type = checker.getTypeAtLocation(node);
461
+ const typeStr = checker.typeToString(type);
462
+ for (const [className, alias] of importedSymbols) {
463
+ if (className === 'default') continue;
464
+ if (typeStr === className || typeStr === alias ||
465
+ typeStr.startsWith(className + ' |') || typeStr.startsWith(alias + ' |') ||
466
+ typeStr.includes(className) || typeStr.includes(alias)) {
467
+ if (!varMap[varName]) varMap[varName] = [];
468
+ const displayName = importedSymbols.get(className) || className;
469
+ if (!varMap[varName].includes(displayName)) varMap[varName].push(displayName);
470
+ }
471
+ }
472
+ } catch (e) { /* ignore */ }
473
+ }
474
+
475
+ visit(sourceFile);
476
+ return varMap;
477
+ }
478
+
479
+ function collectCoverageAST(sourceFile, checker, interfaces, importedSymbols, varMap) {
480
+ const covered = new Set();
481
+
482
+ const ifaceByFullName = new Map();
483
+ const ifaceByMemberName = new Map();
484
+ for (const iface of interfaces) {
485
+ ifaceByFullName.set(iface.fullName, iface);
486
+ if (!ifaceByMemberName.has(iface.memberName)) ifaceByMemberName.set(iface.memberName, []);
487
+ ifaceByMemberName.get(iface.memberName).push(iface);
488
+ }
489
+
490
+ const importedClassNames = new Set();
491
+ for (const [, alias] of importedSymbols) {
492
+ if (alias !== 'default') importedClassNames.add(alias);
493
+ }
494
+ const defaultAlias = importedSymbols.get('default');
495
+
496
+ function visit(node) {
497
+ if (ts.isPropertyAccessExpression(node)) {
498
+ const propName = node.name.text;
499
+ const expr = node.expression;
500
+
501
+ if (ts.isIdentifier(expr) || (ts.isNonNullExpression(expr) && ts.isIdentifier(expr.expression))) {
502
+ const varName = ts.isIdentifier(expr) ? expr.text : expr.expression.text;
503
+ const classNames = varMap[varName];
504
+ if (classNames) {
505
+ for (const cn of classNames) {
506
+ const fullName = `${cn}.${propName}`;
507
+ if (ifaceByFullName.has(fullName)) covered.add(fullName);
508
+ }
509
+ }
510
+ }
511
+
512
+ if (ts.isIdentifier(expr) && importedClassNames.has(expr.text)) {
513
+ const className = expr.text;
514
+ const fullName = `${className}.${propName}`;
515
+ if (ifaceByFullName.has(fullName)) covered.add(fullName);
516
+ }
517
+
518
+ try {
519
+ const exprType = checker.getTypeAtLocation(expr);
520
+ const receiverType = checker.typeToString(exprType);
521
+ if (receiverType) {
522
+ for (const [className, alias] of importedSymbols) {
523
+ if (className === 'default') continue;
524
+ if (receiverType === alias || receiverType === className) {
525
+ const fullName = `${alias}.${propName}`;
526
+ if (ifaceByFullName.has(fullName)) covered.add(fullName);
527
+ }
528
+ }
529
+ }
530
+ } catch (e) { /* ignore */ }
531
+ }
532
+
533
+ if (ts.isCallExpression(node)) {
534
+ const expr = node.expression;
535
+ if (ts.isIdentifier(expr)) {
536
+ const funcName = expr.text;
537
+ const candidates = ifaceByMemberName.get(funcName);
538
+ if (candidates) {
539
+ for (const c of candidates) {
540
+ if (c.className === null && (c.kind === 'function' || c.kind === 'variable')) {
541
+ covered.add(c.fullName);
542
+ }
543
+ }
544
+ }
545
+ }
546
+ }
547
+
548
+ if (ts.isIdentifier(node)) {
549
+ const name = node.text;
550
+ const candidates = ifaceByMemberName.get(name);
551
+ if (candidates) {
552
+ for (const c of candidates) {
553
+ if (c.className === null && c.kind === 'variable') covered.add(c.fullName);
554
+ }
555
+ }
556
+ }
557
+
558
+ if (ts.isObjectLiteralExpression(node)) {
559
+ let contextualType = null;
560
+ try {
561
+ const objType = checker.getContextualType(node);
562
+ if (objType) contextualType = checker.typeToString(objType);
563
+ } catch (e) { /* ignore */ }
564
+
565
+ for (const prop of node.properties) {
566
+ if (ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop)) {
567
+ let propName = null;
568
+ if (ts.isIdentifier(prop.name)) propName = prop.name.text;
569
+ else if (ts.isStringLiteral(prop.name)) propName = prop.name.text;
570
+ if (!propName) continue;
571
+
572
+ if (contextualType) {
573
+ for (const [className, alias] of importedSymbols) {
574
+ if (className === 'default') continue;
575
+ if (contextualType === className || contextualType === alias ||
576
+ contextualType.startsWith(className) || contextualType.startsWith(alias)) {
577
+ const fullName = `${alias}.${propName}`;
578
+ if (ifaceByFullName.has(fullName)) covered.add(fullName);
579
+ }
580
+ }
581
+ }
582
+
583
+ const candidates = ifaceByMemberName.get(propName);
584
+ if (candidates) {
585
+ for (const c of candidates) {
586
+ if (c.kind === 'property' || c.kind === 'field') covered.add(c.fullName);
587
+ }
588
+ }
589
+ }
590
+ }
591
+ }
592
+
593
+ if (defaultAlias && ts.isPropertyAccessExpression(node)) {
594
+ const expr = node.expression;
595
+ if (ts.isPropertyAccessExpression(expr) && ts.isIdentifier(expr.expression) && expr.expression.text === defaultAlias) {
596
+ const className = expr.name.text;
597
+ const memberName = node.name.text;
598
+ const fullName = `${className}.${memberName}`;
599
+ if (ifaceByFullName.has(fullName)) covered.add(fullName);
600
+ }
601
+ }
602
+
603
+ ts.forEachChild(node, visit);
604
+ }
605
+
606
+ visit(sourceFile);
607
+ return covered;
608
+ }
609
+
610
+ function matchCoverageAST(interfaces, testFiles, libName, libRoot) {
611
+ if (!ts) {
612
+ console.warn('TypeScript 模块不可用,降级到正则模式');
613
+ return null;
614
+ }
615
+
616
+ // Windows 兼容:TS 编译器内部使用正斜杠 / 作为路径分隔符
617
+ const toPosix = (p) => p.replace(/\\/g, '/');
618
+
619
+ // 定位库模块根目录(兼容非标准结构,如 ohos_mqtt 用 ohos_Mqtt/ 而非 library/)
620
+ const { locateEntryFile } = require('./extract-interfaces.cjs');
621
+ const entryFile = locateEntryFile(libRoot);
622
+ let libraryDir = path.join(libRoot, 'library');
623
+ if (!fs.existsSync(libraryDir) && entryFile) {
624
+ let dir = path.dirname(entryFile);
625
+ for (let i = 0; i < 6 && dir && dir.startsWith(libRoot); i++) {
626
+ if (fs.existsSync(path.join(dir, 'oh-package.json5')) ||
627
+ fs.existsSync(path.join(dir, 'oh-package.json'))) {
628
+ libraryDir = dir;
629
+ break;
630
+ }
631
+ const parent = path.dirname(dir);
632
+ if (parent === dir) break;
633
+ dir = parent;
634
+ }
635
+ if (!fs.existsSync(libraryDir) && entryFile) libraryDir = path.dirname(entryFile);
636
+ }
637
+
638
+ const libSourceFiles = [];
639
+ function collectLibFiles(dir) {
640
+ if (!fs.existsSync(dir)) return;
641
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
642
+ const fullPath = path.join(dir, entry.name);
643
+ if (entry.isDirectory()) {
644
+ if (entry.name === 'node_modules' || entry.name === 'oh_modules' || entry.name === 'build') continue;
645
+ collectLibFiles(fullPath);
646
+ } else if (entry.name.endsWith('.d.ts') || entry.name.endsWith('.ets') || entry.name.endsWith('.ts')) {
647
+ libSourceFiles.push(fullPath);
648
+ }
649
+ }
650
+ }
651
+ collectLibFiles(libraryDir);
652
+
653
+ const virtualPaths = new Map();
654
+ const allSourceFiles = [...libSourceFiles, ...testFiles];
655
+ for (const realPath of allSourceFiles) {
656
+ let virtualPath = realPath.endsWith('.ets') ? realPath.replace(/\.ets$/, '.ts') : realPath;
657
+ virtualPath = toPosix(virtualPath);
658
+ virtualPaths.set(virtualPath, realPath);
659
+ }
660
+
661
+ const fileContents = {};
662
+ for (const [vPath, rPath] of virtualPaths) {
663
+ let content = fs.readFileSync(rPath, 'utf-8');
664
+ if (rPath.endsWith('.ets')) content = content.replace(/\b(struct)\s+(\w+)/g, 'class $2');
665
+ fileContents[vPath] = content;
666
+ }
667
+
668
+ const compilerOptions = {
669
+ target: ts.ScriptTarget.ES2021, module: ts.ModuleKind.ESNext,
670
+ strict: false, skipLibCheck: true, noEmit: true,
671
+ allowSyntheticDefaultImports: true, esModuleInterop: true,
672
+ };
673
+
674
+ const host = {
675
+ getSourceFile: function(fileName, languageVersion) {
676
+ const content = fileContents[fileName] || fileContents[toPosix(fileName)];
677
+ if (content) return ts.createSourceFile(fileName, content, languageVersion, true, ts.ScriptKind.TS);
678
+ if (!fileName.endsWith('.ets')) {
679
+ try { const c = ts.sys.readFile(fileName); if (c !== undefined) return ts.createSourceFile(fileName, c, languageVersion, true); } catch (e) {}
680
+ }
681
+ return undefined;
682
+ },
683
+ getDefaultLibFileName: function() { return ts.getDefaultLibFilePath(compilerOptions); },
684
+ writeFile: function() {},
685
+ getCurrentDirectory: function() { return process.cwd(); },
686
+ getCanonicalFileName: function(fileName) { return fileName; },
687
+ useCaseSensitiveFileNames: function() { return true; },
688
+ getNewLine: function() { return '\n'; },
689
+ fileExists: function(fileName) { return !!fileContents[fileName] || !!fileContents[toPosix(fileName)] || ts.sys.fileExists(fileName); },
690
+ readFile: function(fileName) { return fileContents[fileName] || fileContents[toPosix(fileName)] || ts.sys.readFile(fileName); },
691
+ resolveModuleNames: function(moduleNames, containingFile) {
692
+ const resolved = [];
693
+ for (const rawModuleName of moduleNames) {
694
+ // 归一化:去除模块说明符尾部斜杠(Demo/测试源码存在 from '@ohos/xxx/' 写法,
695
+ // 否则 moduleName === libName 与深层路径解析分支均失配,模块解析静默失败)
696
+ const moduleName = rawModuleName.replace(/\/+$/, '');
697
+ let resolvedPath = null;
698
+ const baseDir = path.dirname(containingFile);
699
+ if (moduleName.startsWith('.')) {
700
+ for (const c of [path.resolve(baseDir, moduleName + '.ts'), path.resolve(baseDir, moduleName + '.d.ts'), path.resolve(baseDir, moduleName, 'Index.ts'), path.resolve(baseDir, moduleName, 'Index.d.ts')]) {
701
+ const pc = toPosix(c);
702
+ if (fileContents[pc] || ts.sys.fileExists(c)) { resolvedPath = pc; break; }
703
+ }
704
+ }
705
+ if (moduleName.endsWith('.so') && !resolvedPath) {
706
+ const soName = moduleName.replace(/\.so$/, '');
707
+ for (const sf of libSourceFiles) {
708
+ if (sf.includes('types/' + soName + '/Index.d.ts') || sf.includes('types/' + soName.replace(/-/g, '_') + '/Index.d.ts')) { resolvedPath = toPosix(sf); break; }
709
+ }
710
+ }
711
+ if (moduleName === libName && !resolvedPath) {
712
+ // 大小写兼容:大小写不敏感文件系统(macOS/Windows)上 entryFile 可能与
713
+ // readdirSync 真实文件名大小写不一致,导致虚拟路径在 fileContents 中查不到(模块解析静默失败)
714
+ const indexCandidates = [];
715
+ if (entryFile) indexCandidates.push(entryFile);
716
+ indexCandidates.push(path.join(libraryDir, 'Index.ets'), path.join(libraryDir, 'index.ets'));
717
+ for (const cand of indexCandidates) {
718
+ const v = toPosix(cand.replace(/\.ets$/, '.ts'));
719
+ if (fileContents[v]) { resolvedPath = v; break; }
720
+ }
721
+ if (!resolvedPath) {
722
+ const lowerIndexMap = new Map(Object.keys(fileContents).map(k => [k.toLowerCase(), k]));
723
+ for (const cand of indexCandidates) {
724
+ const v = toPosix(cand.replace(/\.ets$/, '.ts')).toLowerCase();
725
+ if (lowerIndexMap.has(v)) { resolvedPath = lowerIndexMap.get(v); break; }
726
+ }
727
+ }
728
+ }
729
+ // 深层路径导入解析:'@ohos/mp3agic/src/main/ets/...' → library/src/main/ets/...
730
+ if (moduleName.startsWith(libName + '/') && !resolvedPath) {
731
+ const rest = moduleName.substring(libName.length + 1);
732
+ const base = toPosix(path.join(libraryDir, rest));
733
+ const lowerDeepMap = new Map(Object.keys(fileContents).map(k => [k.toLowerCase(), k]));
734
+ for (const cand of [base + '.ts', base]) {
735
+ if (fileContents[cand]) { resolvedPath = cand; break; }
736
+ if (lowerDeepMap.has(cand.toLowerCase())) { resolvedPath = lowerDeepMap.get(cand.toLowerCase()); break; }
737
+ }
738
+ }
739
+ resolved.push(resolvedPath ? { resolvedFileName: resolvedPath, extension: '.ts' } : undefined);
740
+ }
741
+ return resolved;
742
+ },
743
+ };
744
+
745
+ const program = ts.createProgram(Array.from(virtualPaths.keys()), compilerOptions, host);
746
+ const checker = program.getTypeChecker();
747
+
748
+ // ── 静默失败检测:库模块解析失败 ──────────────────────────
749
+ // 若测试代码 import 了库但 TS 编译器报 Cannot find module '<库名>',
750
+ // 库类型将坍缩为 any,基于类型的匹配会严重低估覆盖率。此时放弃 AST
751
+ // 结果并返回 null,由上层降级正则模式,避免静默输出错误报告。
752
+ let libModuleNotFound = false;
753
+ try {
754
+ const libSeg = libName.split('/').pop();
755
+ for (const diag of program.getSemanticDiagnostics()) {
756
+ if (diag.code !== 2307) continue; // TS 2307 = Cannot find module
757
+ const msg = ts.flattenDiagnosticMessageText(diag.messageText, ' ');
758
+ const m = msg.match(/Cannot find module '([^']+)'/);
759
+ if (!m) continue;
760
+ const mod = m[1].replace(/\/+$/, '');
761
+ if (mod === libName || mod.endsWith('/' + libSeg)) { libModuleNotFound = true; break; }
762
+ }
763
+ } catch (e) { /* ignore */ }
764
+ if (libModuleNotFound) {
765
+ console.error(`⚠️ [警告] AST 模式下库模块 '${libName}' 解析失败(Cannot find module),类型推断将坍缩为 any,` +
766
+ `匹配结果会被严重低估。请检查库入口文件大小写(Index.ets vs index.ets)与测试代码 import 写法。放弃 AST 结果,降级正则模式。`);
767
+ return null;
768
+ }
769
+
770
+ const fileCoverageMap = {};
771
+ const importOnlyFiles = []; // import 了库但匹配数为 0 的文件(疑似匹配遗漏)
772
+ for (const testFile of testFiles) {
773
+ const virtualTestPath = toPosix(testFile.replace(/\.ets$/, '.ts'));
774
+ const sourceFile = program.getSourceFile(virtualTestPath);
775
+ if (!sourceFile) continue;
776
+ const fileName = path.basename(testFile);
777
+ const importedSymbols = extractImportedSymbolsAST(sourceFile, libName);
778
+ if (importedSymbols.size === 0) {
779
+ // 降级告警:文件存在 import 语句但均未被识别(可能为未兼容的导入风格)
780
+ const importSpecs = [];
781
+ for (const stmt of sourceFile.statements) {
782
+ if (ts.isImportDeclaration(stmt) && stmt.moduleSpecifier && ts.isStringLiteral(stmt.moduleSpecifier)) {
783
+ importSpecs.push(stmt.moduleSpecifier.text);
784
+ }
785
+ }
786
+ if (importSpecs.length > 0) {
787
+ console.warn(`[告警] 测试文件 ${fileName} 存在 import 语句但均未被识别,已跳过(模块说明符: ${importSpecs.join(', ')})`);
788
+ }
789
+ continue;
790
+ }
791
+ const varMap = trackInstanceVariablesAST(sourceFile, checker, importedSymbols);
792
+ // 降级告警:导入符号非空但实例变量追踪为空,提示 checker 类型推断可能失效
793
+ if (Object.keys(varMap).length === 0) {
794
+ console.warn(`[告警] 测试文件 ${fileName} 已识别 ${importedSymbols.size} 个导入符号但实例变量追踪结果为空,类型推断可能失效`);
795
+ }
796
+ const fileCovered = collectCoverageAST(sourceFile, checker, interfaces, importedSymbols, varMap);
797
+ fileCoverageMap[fileName] = { count: fileCovered.size, interfaces: Array.from(fileCovered) };
798
+ if (fileCovered.size === 0) importOnlyFiles.push(fileName);
799
+ }
800
+
801
+ if (importOnlyFiles.length > 0) {
802
+ console.warn(`⚠️ [提示] 以下 ${importOnlyFiles.length} 个测试文件 import 了库但未匹配到任何接口` +
803
+ `(可能为纯类型引用,也可能存在匹配遗漏,建议 grep 抽样核验): ${importOnlyFiles.slice(0, 5).join(', ')}` +
804
+ `${importOnlyFiles.length > 5 ? ' ...' : ''}`);
805
+ }
806
+
807
+ const astResult = aggregateResults(interfaces, fileCoverageMap, {}, libName, testFiles.length);
808
+ astResult.importedFileCount = Object.keys(fileCoverageMap).length;
809
+ astResult.importOnlyFiles = importOnlyFiles;
810
+ return astResult;
811
+ }
812
+
813
+ // ════════════════════════════════════════════════════════════
814
+ // Part C: 结果聚合
815
+ // ════════════════════════════════════════════════════════════
816
+
817
+ function aggregateResults(interfaces, fileCoverageMap, classCoverageMap, libName, demoFileCount) {
818
+ const coveredSet = new Set();
819
+ for (const fc of Object.values(fileCoverageMap)) {
820
+ for (const intf of fc.interfaces) coveredSet.add(intf);
821
+ }
822
+
823
+ const covered = [];
824
+ const uncovered = [];
825
+
826
+ for (const iface of interfaces) {
827
+ const className = iface.className || '';
828
+ if (!classCoverageMap[className]) classCoverageMap[className] = { total: 0, covered: 0 };
829
+ classCoverageMap[className].total++;
830
+ if (coveredSet.has(iface.fullName)) {
831
+ classCoverageMap[className].covered++;
832
+ covered.push({
833
+ ...iface,
834
+ files: Object.entries(fileCoverageMap)
835
+ .filter(([_, fc]) => fc.interfaces.includes(iface.fullName))
836
+ .map(([fn]) => fn)
837
+ });
838
+ } else {
839
+ uncovered.push(iface);
840
+ }
841
+ }
842
+
843
+ const N = interfaces.length;
844
+ const M = coveredSet.size;
845
+ const X = N > 0 ? (M / N * 100).toFixed(2) : '0.00';
846
+
847
+ const byKind = {};
848
+ const kindNames = ['method', 'constructor', 'property', 'field', 'enumValue', 'function', 'variable'];
849
+ for (const kind of kindNames) {
850
+ const kindInterfaces = interfaces.filter(i => i.kind === kind);
851
+ const kindCovered = kindInterfaces.filter(i => coveredSet.has(i.fullName));
852
+ byKind[kind] = {
853
+ total: kindInterfaces.length,
854
+ covered: kindCovered.length,
855
+ rate: kindInterfaces.length > 0 ? (kindCovered.length / kindInterfaces.length * 100).toFixed(2) : '-'
856
+ };
857
+ }
858
+
859
+ const classCoverage = [];
860
+ for (const [className, data] of Object.entries(classCoverageMap)) {
861
+ if (className === '') continue;
862
+ classCoverage.push({
863
+ className, total: data.total, covered: data.covered,
864
+ uncovered: data.total - data.covered,
865
+ rate: data.total > 0 ? (data.covered / data.total * 100).toFixed(2) : '-'
866
+ });
867
+ }
868
+
869
+ const S = Object.keys(classCoverageMap).filter(k => k !== '').length;
870
+
871
+ return {
872
+ libraryName: libName,
873
+ totalInterfacesClassLevel: S,
874
+ totalInterfacesMethodLevel: N,
875
+ coveredMethodCount: M,
876
+ coverageRateMethodLevel: X,
877
+ byKind,
878
+ classCoverage,
879
+ covered,
880
+ uncovered,
881
+ fileCoverage: Object.entries(fileCoverageMap).map(([file, data]) => ({
882
+ file, count: data.count, interfaces: data.interfaces
883
+ })),
884
+ demoFileCount,
885
+ };
886
+ }
887
+
888
+ // ════════════════════════════════════════════════════════════
889
+ // Part D: Markdown 渲染
890
+ // ════════════════════════════════════════════════════════════
891
+
892
+ function renderAnalysisWarnings(result) {
893
+ const warnings = result.analysisWarnings || [];
894
+ if (warnings.length === 0) return '';
895
+ const lines = [];
896
+ lines.push('## ⚠️ 分析可信度警告');
897
+ lines.push('');
898
+ lines.push(warnings.map((w) => `> ⚠️ ${w}`).join('\n>\n'));
899
+ lines.push('');
900
+ return lines.join('\n');
901
+ }
902
+
903
+ function renderHeader(result, title) {
904
+ const lines = [];
905
+ lines.push(`# ${title || result.libraryName + ' 覆盖率分析报告'}`);
906
+ lines.push('');
907
+ lines.push('> 本报告由 `analyze-coverage.cjs` 自动生成。');
908
+ lines.push('');
909
+ lines.push('## 基本信息');
910
+ lines.push('');
911
+ lines.push('| 属性 | 值 |');
912
+ lines.push('|------|----|');
913
+ lines.push(`| 库名 | \`${result.libraryName}\` |`);
914
+ lines.push(`| 源文件数 | ${result.demoFileCount || '-'} |`);
915
+ lines.push(`| 生成时间 | ${new Date(Date.now() + 8 * 3600 * 1000).toISOString().replace('T', ' ').replace('Z', '+08:00')} |`);
916
+ if ((result.filteredLowValueCount || 0) > 0 || (result.filteredTypeCount || 0) > 0) {
917
+ lines.push(`| 过滤低价值常量 | ${result.filteredLowValueCount || 0} |`);
918
+ lines.push(`| 过滤类型声明 | ${result.filteredTypeCount || 0} |`);
919
+ lines.push(`| 实际统计接口 | ${result.totalInterfacesMethodLevel} |`);
920
+ }
921
+ lines.push('');
922
+ return lines.join('\n');
923
+ }
924
+
925
+ function renderOverview(result) {
926
+ const lines = [];
927
+ lines.push('## 覆盖率总览');
928
+ lines.push('');
929
+ const rate = parseFloat(result.coverageRateMethodLevel) || 0;
930
+ lines.push(`### 方法级覆盖率: ${result.coverageRateMethodLevel}% (${coverageBadge(result.coverageRateMethodLevel)})`);
931
+ lines.push('');
932
+ lines.push('| 统计项 | 数量 |');
933
+ lines.push('|--------|------|');
934
+ lines.push(`| 类/接口总数 (S) | ${result.totalInterfacesClassLevel} |`);
935
+ lines.push(`| 方法/属性总数 (N) | ${result.totalInterfacesMethodLevel} |`);
936
+ lines.push(`| 已覆盖 (M) | ${result.coveredMethodCount} |`);
937
+ lines.push(`| 未覆盖 | ${result.totalInterfacesMethodLevel - result.coveredMethodCount} |`);
938
+ lines.push('');
939
+
940
+ const filled = Math.round(rate / 5);
941
+ const empty = 20 - filled;
942
+ const bar = '█'.repeat(filled) + '░'.repeat(empty);
943
+ lines.push('```');
944
+ lines.push(`覆盖率进度: [${bar}] ${rate.toFixed(2)}%`);
945
+ lines.push('```');
946
+ lines.push('');
947
+ return lines.join('\n');
948
+ }
949
+
950
+ function renderByKind(result) {
951
+ const lines = [];
952
+ lines.push('### 按接口类型统计');
953
+ lines.push('');
954
+ lines.push('| 类型 | 总数 | 已覆盖 | 覆盖率 | 状态 |');
955
+ lines.push('|------|------|--------|--------|------|');
956
+ const byKind = result.byKind || {};
957
+ for (const [kind, data] of Object.entries(byKind)) {
958
+ const badge = coverageBadge(data.rate);
959
+ lines.push(`| ${kind} | ${data.total} | ${data.covered} | ${data.rate}% | ${badge} |`);
960
+ }
961
+ lines.push('');
962
+ return lines.join('\n');
963
+ }
964
+
965
+ function renderClassCoverage(result) {
966
+ const lines = [];
967
+ lines.push('## 类级覆盖率');
968
+ lines.push('');
969
+ lines.push('| # | 类名 | 总数 | 已覆盖 | 未覆盖 | 覆盖率 | 状态 |');
970
+ lines.push('|---|------|------|--------|--------|--------|------|');
971
+
972
+ const classCoverage = result.classCoverage || [];
973
+ const sorted = [...classCoverage].sort((a, b) => {
974
+ const ra = parseFloat(a.rate) || 0;
975
+ const rb = parseFloat(b.rate) || 0;
976
+ return ra - rb;
977
+ });
978
+
979
+ sorted.forEach((cls, i) => {
980
+ const badge = coverageBadge(cls.rate);
981
+ lines.push(`| ${i + 1} | \`${cls.className}\` | ${cls.total} | ${cls.covered} | ${cls.uncovered} | ${cls.rate}% | ${badge} |`);
982
+ });
983
+ lines.push('');
984
+ return lines.join('\n');
985
+ }
986
+
987
+ function renderFileCoverage(result) {
988
+ const lines = [];
989
+ lines.push('## 文件覆盖详情');
990
+ lines.push('');
991
+ lines.push('| # | 文件 | 覆盖接口数 | 覆盖的接口 |');
992
+ lines.push('|---|------|-----------|-----------|');
993
+
994
+ const fileCoverage = result.fileCoverage || [];
995
+ const sorted = [...fileCoverage].sort((a, b) => b.count - a.count);
996
+
997
+ sorted.forEach((fc, i) => {
998
+ const ifaces = fc.interfaces.slice(0, 10).map(i => `\`${i}\``).join(', ');
999
+ const more = fc.interfaces.length > 10 ? ` ... (+${fc.interfaces.length - 10})` : '';
1000
+ lines.push(`| ${i + 1} | \`${fc.file}\` | ${fc.count} | ${ifaces}${more} |`);
1001
+ });
1002
+ lines.push('');
1003
+ return lines.join('\n');
1004
+ }
1005
+
1006
+ function renderCoveredDetails(result) {
1007
+ const lines = [];
1008
+ lines.push('## 已覆盖接口详情');
1009
+ lines.push('');
1010
+ const covered = result.covered || [];
1011
+ if (covered.length === 0) {
1012
+ lines.push('*暂无已覆盖接口*');
1013
+ lines.push('');
1014
+ return lines.join('\n');
1015
+ }
1016
+
1017
+ const grouped = {};
1018
+ for (const c of covered) {
1019
+ const cn = c.className || '(顶层)';
1020
+ if (!grouped[cn]) grouped[cn] = [];
1021
+ grouped[cn].push(c);
1022
+ }
1023
+
1024
+ let sectionIdx = 0;
1025
+ for (const [className, members] of Object.entries(grouped)) {
1026
+ sectionIdx++;
1027
+ lines.push(`### ${sectionIdx}. ${className}`);
1028
+ lines.push('');
1029
+ lines.push('| # | 接口 | 类型 | 静态 | 签名 | 覆盖文件 |');
1030
+ lines.push('|---|------|------|------|------|----------|');
1031
+ members.forEach((m, i) => {
1032
+ const files = (m.files || []).slice(0, 3).map(f => `\`${f}\``).join(', ');
1033
+ const more = m.files && m.files.length > 3 ? ` +${m.files.length - 3}` : '';
1034
+ lines.push(`| ${i + 1} | \`${m.memberName}\` | ${m.kind} | ${m.isStatic ? '是' : '否'} | \`${escapeMd(m.signature)}\` | ${files}${more} |`);
1035
+ });
1036
+ lines.push('');
1037
+ }
1038
+ return lines.join('\n');
1039
+ }
1040
+
1041
+ function renderUncoveredDetails(result) {
1042
+ const lines = [];
1043
+ lines.push('## 未覆盖接口详情');
1044
+ lines.push('');
1045
+ const uncovered = result.uncovered || [];
1046
+ if (uncovered.length === 0) {
1047
+ lines.push('*所有接口均已覆盖!*');
1048
+ lines.push('');
1049
+ return lines.join('\n');
1050
+ }
1051
+
1052
+ const grouped = {};
1053
+ for (const u of uncovered) {
1054
+ const cn = u.className || '(顶层)';
1055
+ if (!grouped[cn]) grouped[cn] = [];
1056
+ grouped[cn].push(u);
1057
+ }
1058
+
1059
+ let sectionIdx = 0;
1060
+ for (const [className, members] of Object.entries(grouped)) {
1061
+ sectionIdx++;
1062
+ lines.push(`### ${sectionIdx}. ${className} (${members.length} 个未覆盖)`);
1063
+ lines.push('');
1064
+ lines.push('| # | 接口 | 类型 | 静态 | 参数 | 返回类型 | 说明 |');
1065
+ lines.push('|---|------|------|------|------|----------|------|');
1066
+ members.forEach((m, i) => {
1067
+ lines.push(`| ${i + 1} | \`${m.memberName}\` | ${m.kind} | ${m.isStatic ? '是' : '否'} | ${escapeMd(formatParams(m.params))} | ${escapeMd(m.returnType)} | ${escapeMd(m.description)} |`);
1068
+ });
1069
+ lines.push('');
1070
+ }
1071
+ return lines.join('\n');
1072
+ }
1073
+
1074
+ function renderRecommendations(result) {
1075
+ const lines = [];
1076
+ lines.push('## 覆盖率提升建议');
1077
+ lines.push('');
1078
+
1079
+ const uncovered = result.uncovered || [];
1080
+ const rate = parseFloat(result.coverageRateMethodLevel) || 0;
1081
+
1082
+ if (rate >= 80) {
1083
+ lines.push('覆盖率优秀!以下接口仍可补充测试:');
1084
+ } else if (rate >= 50) {
1085
+ lines.push('覆盖率中等,建议优先补充以下接口的测试:');
1086
+ } else {
1087
+ lines.push('覆盖率较低,强烈建议补充以下接口的测试:');
1088
+ }
1089
+ lines.push('');
1090
+
1091
+ const priority = uncovered.sort((a, b) => {
1092
+ const kindOrder = { 'method': 0, 'property': 1, 'field': 2, 'function': 3, 'variable': 4, 'enumValue': 5 };
1093
+ const ka = kindOrder[a.kind] !== undefined ? kindOrder[a.kind] : 99;
1094
+ const kb = kindOrder[b.kind] !== undefined ? kindOrder[b.kind] : 99;
1095
+ if (ka !== kb) return ka - kb;
1096
+ return (a.className || '').localeCompare(b.className || '');
1097
+ });
1098
+
1099
+ const top = priority.slice(0, 20);
1100
+ top.forEach((u, i) => {
1101
+ const cn = u.className ? `\`${u.className}.\`` : '';
1102
+ lines.push(`${i + 1}. [ ] ${cn}\`${u.memberName}\` — ${u.kind}${u.isStatic ? ' (静态)' : ''}${formatParams(u.params) ? ` (${formatParams(u.params)})` : ''}`);
1103
+ });
1104
+ if (priority.length > 20) {
1105
+ lines.push(`- ... 还有 ${priority.length - 20} 个未覆盖接口,详见上方表格`);
1106
+ }
1107
+ lines.push('');
1108
+ return lines.join('\n');
1109
+ }
1110
+
1111
+ // ════════════════════════════════════════════════════════════
1112
+ // Part E: 入口 — XTS 覆盖率匹配 + 渲染
1113
+ // ════════════════════════════════════════════════════════════
1114
+
1115
+ const args = parseArgs();
1116
+ const specPath = args.spec;
1117
+ const mode = args.mode || 'auto';
1118
+ const title = args.title;
1119
+
1120
+ if (!specPath && !args.libroot && !args.repoDir) {
1121
+ console.error('用法: node analyze-xts-coverage.cjs --spec <interface-spec.json> --libroot <库根目录> [--output <report.md>] [--title <标题>] [--mode auto|ast|regex]');
1122
+ console.error(' 或: node analyze-xts-coverage.cjs --libroot <库根目录> [--output <report.md>] [--mode auto|ast|regex]');
1123
+ process.exit(1);
1124
+ }
1125
+
1126
+ const libRoot = args.libroot || args.repoDir || (specPath ? path.dirname(specPath) : '');
1127
+
1128
+ // 获取接口规格数据
1129
+ let specData;
1130
+ let specBasename;
1131
+ if (specPath) {
1132
+ specData = JSON.parse(fs.readFileSync(specPath, 'utf-8'));
1133
+ specBasename = path.basename(specPath, '-interface-spec.json');
1134
+ console.log(`[XTS 覆盖率分析]`);
1135
+ console.log(`接口规格: ${specPath} (${specData.interfaces ? specData.interfaces.length : 0} 个接口)`);
1136
+ } else {
1137
+ console.log(`[XTS 覆盖率分析]`);
1138
+ console.log(`从源码目录提取接口: ${libRoot}`);
1139
+ specData = generateSpec(libRoot);
1140
+ specBasename = specData.libraryName ? specData.libraryName.replace(/^@[^/]+\//, '') : path.basename(libRoot);
1141
+ console.log(`提取完成: ${specData.interfaces ? specData.interfaces.length : 0} 个接口, 库名: ${specData.libraryName}`);
1142
+ }
1143
+
1144
+ // XTS 源码目录固定为 <libroot>/entry/src/ohosTest
1145
+ const srcDir = args.src || path.join(libRoot, 'entry', 'src', 'ohosTest');
1146
+ const libName = args.lib || '';
1147
+
1148
+ // 输出路径默认取 spec 同目录下的 03-xxx-XTS覆盖率报告.md
1149
+ const outputPath = args.output || path.join(path.dirname(specPath || __dirname), `03-${specBasename}-XTS覆盖率报告.md`);
1150
+
1151
+ let filteredLowValueCount = 0, filteredTypeCount = 0;
1152
+ const interfaces = (specData.interfaces || []).filter(i => {
1153
+ if (i.lowValue === true) { filteredLowValueCount++; return false; }
1154
+ if (i.kind === 'type') { filteredTypeCount++; return false; }
1155
+ return true;
1156
+ });
1157
+ const resolvedLibName = libName || specData.libraryName || specBasename;
1158
+
1159
+ console.log(`库名: ${resolvedLibName}`);
1160
+ console.log(`XTS 源码目录: ${srcDir}`);
1161
+
1162
+ if (!fs.existsSync(srcDir)) {
1163
+ console.error(`XTS 源码目录不存在: ${srcDir}`);
1164
+ process.exit(1);
1165
+ }
1166
+
1167
+ // 扫描 XTS 源文件
1168
+ const testFiles = walkDir(srcDir, EXCLUDE_TOP_DIRS, srcDir);
1169
+ console.log(`扫描到 ${testFiles.length} 个 .ets/.ts 文件`);
1170
+
1171
+ // 执行匹配
1172
+ let result = null;
1173
+
1174
+ if (mode === 'ast' || (mode === 'auto' && ts)) {
1175
+ console.log('使用 AST 模式匹配...');
1176
+ result = matchCoverageAST(interfaces, testFiles, resolvedLibName, libRoot);
1177
+ if (!result && mode === 'auto') {
1178
+ console.warn('AST 模式失败,降级到正则模式...');
1179
+ }
1180
+ }
1181
+
1182
+ if (!result) {
1183
+ console.log('使用正则模式匹配...');
1184
+ result = matchCoverageRegex(interfaces, testFiles, resolvedLibName);
1185
+ }
1186
+
1187
+ result.specSummary = specData.summary;
1188
+ result.filteredLowValueCount = filteredLowValueCount;
1189
+ result.filteredTypeCount = filteredTypeCount;
1190
+
1191
+ // ── 结果合理性校验(静默失败防线)──────────────────────────
1192
+ // 有测试文件 import 了库、但匹配数异常低/为 0 时,大概率是匹配缺陷
1193
+ // 而非测试完全未使用库,在控制台与报告中显著告警,避免误报被直接采信。
1194
+ const analysisWarnings = [];
1195
+ const importedFileCount = result.importedFileCount || 0;
1196
+ const coveredCount = result.coveredMethodCount || 0;
1197
+ const rateNum = parseFloat(result.coverageRateMethodLevel) || 0;
1198
+ if (importedFileCount > 0 && coveredCount === 0) {
1199
+ analysisWarnings.push(
1200
+ `检测到 ${importedFileCount} 个测试文件 import 了库 \`${resolvedLibName}\`,但匹配到的接口数为 0。` +
1201
+ `这通常意味着存在匹配缺陷(如 import 路径写法、库模块解析问题),而非测试完全未使用库,请勿直接采信本报告。`
1202
+ );
1203
+ } else if (importedFileCount > 0 && rateNum < 10) {
1204
+ analysisWarnings.push(
1205
+ `覆盖率异常低(${result.coverageRateMethodLevel}%):有 ${importedFileCount} 个测试文件 import 了库 \`${resolvedLibName}\`。` +
1206
+ `建议对"未覆盖"接口抽样 grep 真实调用数进行交叉验证。`
1207
+ );
1208
+ }
1209
+ if (result.importOnlyFiles && result.importOnlyFiles.length > 0) {
1210
+ analysisWarnings.push(
1211
+ `以下文件 import 了库但未匹配到任何接口(可能为纯类型引用,也可能存在匹配遗漏,建议 grep 抽样核验):` +
1212
+ result.importOnlyFiles.map((f) => `\`${f}\``).join('、')
1213
+ );
1214
+ }
1215
+ result.analysisWarnings = analysisWarnings;
1216
+ for (const w of analysisWarnings) console.error(`⚠️ [分析可信度警告] ${w}`);
1217
+
1218
+ console.log(`XTS 匹配完成: ${result.coveredMethodCount}/${result.totalInterfacesMethodLevel} (${result.coverageRateMethodLevel}%)`);
1219
+
1220
+ // 合理性检查:覆盖率异常低但测试代码量大时提示人工复核(静态匹配可能存在盲区)
1221
+ const totalTestLines = testFiles.reduce((sum, f) => {
1222
+ try { return sum + fs.readFileSync(f, 'utf-8').split('\n').length; } catch (e) { return sum; }
1223
+ }, 0);
1224
+ if ((parseFloat(result.coverageRateMethodLevel) || 0) < 5 && totalTestLines > 500) {
1225
+ console.warn(`[告警] 覆盖率 ${result.coverageRateMethodLevel}% 异常低但测试代码共 ${totalTestLines} 行,匹配结果可能失真,建议人工 grep 盘点实例/静态方法调用并按类对账复核`);
1226
+ }
1227
+
1228
+ // 渲染 Markdown
1229
+ const resolvedTitle = title || `${resolvedLibName} XTS 覆盖率分析报告`;
1230
+ const parts = [];
1231
+ parts.push(renderHeader(result, resolvedTitle));
1232
+ const warnSection = renderAnalysisWarnings(result);
1233
+ if (warnSection) parts.push(warnSection);
1234
+ parts.push(renderOverview(result));
1235
+ parts.push(renderByKind(result));
1236
+ parts.push(renderClassCoverage(result));
1237
+ parts.push(renderUncoveredDetails(result));
1238
+ parts.push(renderCoveredDetails(result));
1239
+ parts.push(renderFileCoverage(result));
1240
+
1241
+ const markdown = parts.join('\n---\n\n');
1242
+
1243
+ fs.mkdirSync(path.dirname(outputPath), { recursive: true });
1244
+ fs.writeFileSync(outputPath, markdown, 'utf-8');
1245
+ console.log(`XTS 报告渲染完成: ${outputPath}`);