
开发工具代码质量静态分析【免费下载链接】jscpdCopy/paste detector for source code. 220 languages, Rust engine, SARIF/HTML/badge reporters, GitHub Action, MCP server for AI agents.项目地址https://gitcode.com/gh_mirrors/js/jscpd点击查看免费下载本篇技术指南以 jscpd 仓库中fixtures/embedded-stats-demo演示目录为核心讲解 jscpdCopy/paste detector220 语言Rust 引擎如何扫描嵌入在 Markdown、Vue、Svelte、Astro 文件中的另一种语言代码块并解答一个关键问题统计报告中的行数、重复行与百分比如何做到既准确又不被宿主文件中的散文或模板文字污染。读完本文你将掌握 jscpd 嵌入子格式sub-format的识别机制、报告聚合函数buildReport/mergeReports的语义、行统计修复前后的行为差异以及如何在本地复现验证。一、认识 embedded-stats-demo一个为 issue #1090 而生的演示目录fixtures/embedded-stats-demo是 jscpd 仓库中的一个自包含演示夹具目录结构如下fixtures/embedded-stats-demo/ ├── README.md # 统计行为说明与运行输出对照 ├── markdown/ # 两个 Markdown 指南各含两个 ts 代码块 │ ├── exporting.md │ └── reporting.md └── component/ # 两个 Vue 组件各含一个 script langts ├── ExportCard.vue └── ReportCard.vue该目录对应回归问题issue #1090Markdown、Vue、Svelte 和 Astro 文件里包含用另一种语言书写的代码jscpd 需要以嵌入语言自身的格式扫描这些代码块但旧版本README 中以 5.3.1 为对照的统计逻辑存在缺陷——代码块保留宿主文件的行号旧版本统计却把块所在的行区间当成整个文件从头到尾都是这段嵌入代码导致块周围的文本Markdown 散文、组件模板被错误计入了嵌入语言的代码行总数和重复行数。README 对照表 给出了修复前后的直观对比均为默认阈值下从仓库根运行目录嵌入内容5.3.1 的 typescript 行统计现在的 typescript 行统计markdown/每个指南两个ts块块间有散文114行34 (29.82%)26行13 (50.00%)component/长模板下有一个script langts96行13 (13.54%)26行13 (50.00%)注意两列百分比测量的是同一份重复代码因此修复后的数值应当接近当行百分比29.82%与 token 百分比50.00%差距悬殊时就意味着行统计把不含代码的文本也算进去了。二、Reporting Guide 中的报告聚合逻辑buildReport 与 mergeReportsfixtures/embedded-stats-demo/markdown/reporting.md是一份 Reporting guide其正文包含两个 TypeScript 代码块两块之间的 Aggregation note 段落是刻意放置、用于在夹具中制造距离的填充文本这正是嵌入式统计演示的关键素材。第一块定义报告构建函数export function buildReport(rows: Row[], limit: number): Report { const sorted rows.slice().sort((a, b) b.total - a.total); const top sorted.slice(0, limit); const rest sorted.slice(limit); const total sorted.reduce((sum, row) sum row.total, 0); return { top, others: rest.length, total, generated: Date.now() }; }这段代码描述了报告聚合的第一阶段——从原始行数据构建单份报告rows.slice().sort(...)先复制再按total降序排序避免原地修改调用方数据top取排序后的前limit行即报告要展开详列的头部条目othersrest.length即头部之外的所有条目数量报告通常只展示它们的合计而非明细total对所有行的total求和作为整个数据集的汇总值generated: Date.now()记录报告生成时间戳供报告消费方HTML 页面、Dashboard展示报告生成于何时。第二块定义报告合并函数export function mergeReports(left: Report, right: Report): Report { const top left.top.concat(right.top).slice(0, left.top.length); const others left.others right.others; const total left.total right.total; return { top, others, total, generated: Date.now() }; }mergeReports描述报告聚合的第二阶段——把两份报告合并成一份头部列表拼接后截断到左侧top的长度保证合并后头部条目数不变始终等于limitothers与total直接相加时间戳更新为合并时刻。buildReport → mergeReports这一组合刻画了典型的分片扫描、集中汇总报告管线每个目录先各自buildReport再逐级mergeReports得到全局报告。这两段代码在 demo 中并非孤立文本它们原样出现在fixtures/embedded-stats-demo/component/ReportCard.vue与ExportCard.vue的script langts块内是组件的数据层逻辑。也就是说markdown/与component/两个目录共享完全相同的 TypeScript 代码README 明确写道 The two guides share their code and nothing else: 13 lines of TypeScript each只是宿主形态不同——这正是衡量嵌入式统计是否准确的理想对照。三、嵌入式代码块如何被识别MULTI_FORMAT_EXTS 与 path:format 合成源报告聚合逻辑能工作前提是 jscpd 能先把嵌在别种语言文件里的代码块当作独立格式的代码源提取出来。这一识别在扫描编排层完成见 orchestrate.rsconst MULTI_FORMAT_EXTS: [str] [md, markdown, mkd, vue, svelte, astro];命中这些扩展名时走多格式multi-format路径orchestrate.rs#L500-L573对宿主文件整体按宿主格式做一次扁平 tokenize得到父级SourceFile对内容做多格式 tokenize得到每个子格式sub-format的 token 映射——Markdown 的围栏代码块、Vue/Svelte 的script、Astro 的 frontmatter 都会各自成为一个子格式每个子格式生成一个合成源synthetic SourceFile其 id 形如path:format例如reporting.md:typescriptbytes置 0字节数由父文件统一计数见 models.rs#L358-L361只有当子格式与宿主格式不同时embedded标志才为真宿主自身从头到尾覆盖整份文件的 map 不算嵌入见 orchestrate.rs#L562-L568。合成源的嵌入身份由SourceFile::is_embedded()判定models.rs#L364-L374id 以:格式名结尾且前缀路径非空即为嵌入块否则是普通文件。这正是markdown行与typescript行能同时出现在报告中的原因——同一份reporting.md在报告中既贡献 markdown 行宿主又贡献 typescript 行嵌入子格式。四、行数统计的修复covered_lines 与 line_count识别出嵌入块之后关键问题落在一行代码行到底怎么数上。旧版本5.3.1 之前把嵌入块的 token 行号直接当作连续区间计算——两个块之间隔着 22 行散文就被误算成这段 TypeScript 有 35 行散文既污染了分母总行数也污染了分子重复行数。修复后的计数逻辑在 SourceFile::line_count()pub fn line_count(self) - u64 { if self.is_embedded() { covered_lines(self.tokens.iter().map(|t| (t.start.line, t.end.line))) as u64 } else { self.tokens.iter().map(|t| t.start.line).max().unwrap_or(0) as u64 } }普通文件取最后一个持有 token 的行的行号即 jscpd 一直以来的算法接近文件长度嵌入块改用covered_lines只统计该格式 token 实际覆盖的、互不重复的行。covered_lines的实现models.rs#L395-L399对按源顺序排列的 token 行跨度做一次线性扫描遇到新行才递增计数pub fn covered_lines(spans: impl IntoIteratorItem (u32, u32)) - u32 { let mut covered 0; // Lines are 1-based, so 0 reads as nothing counted yet. let mut last 0; for (start, end) in spans { // ... 只对 last start 且未计入的行累加 } }由此得到两条边界明确的规则embedded-stats-demo/README.md嵌入源只统计承载其代码的行。块内的空行不计所以同一份代码嵌在 Markdown 里会比作为独立.ts文件少计一两行普通文件行为不变仍计到最后一个持有 token 的行。五、重复行报告的精确性unmatched_lines 与 fragment_lines跨块克隆是报告准确性的另一个考验markdown/目录中两个指南各有一个片段横跨两个代码块克隆片段的起止行覆盖了两块之间的全部散文README 指出片段从第 23 行延伸到第 57 行但真正重复的只有两个块里的 13 行代码。如果直接按片段跨度上报行数散文就会混进重复行。解决方式是把片段跨度与实际重复解耦。Fragment携带unmatched_linesmodels.rs#L223-L230记录片段跨度内不属于重复代码的行来源有两类--max-gap-lines合并后未匹配的间隙行以及嵌入块场景下宿主语言夹在两个块之间的行即 issue #1090 的修复点。统计上报时用fragment_lines()扣除pub fn fragment_lines(self, index: usize) - u64 { let fragment ...; let span fragment.end.line.saturating_sub(fragment.start.line) 1; span.saturating_sub(self.unmatched_lines[index]) as u64 }见 models.rs#L268-L290。克隆对统计的贡献则由matched_lines()取主片段fragment A的重复行数给出。于是报告里能同时呈现控制台打印的是片段实际重复的 13 行而不是片段跨度的 35 行夹在中间的 22 行散文则归属 markdown 行——因为它们在宿主语言里本就存在。六、回归测试如何验证统计行为嵌入式统计的修复由回归测试锁定见 rust/crates/cpd-finder/tests/embedded_statistics_integration.rs。测试使用另一组夹具guide-a.md 与guide-b.md每份含两个 10 行ts块、块间及周边为散文用min_tokens: 20、min_lines: 2、Mode::Mild配置运行覆盖四个断言a_formats_line_total_counts_only_its_own_blockstypescript 格式的总行数等于每个指南两个块的行数之和20 × 2 40而不是指南文件全长合成源数量为 2每个指南一个prose_between_two_blocks_is_not_duplicated_code克隆片段跨度跨块达 36 行但matched_lines()只上报 20 行块内代码——散文不是重复代码the_host_format_keeps_counting_whole_filesmarkdown 宿主格式不受影响仍按整文件计数66 × 2 132 行修复只作用于嵌入源filtering_to_the_embedded_format_gives_the_same_line_total用-f typescript过滤后由于夹具中没有普通.ts文件过滤结果中该格式为空——修复前全量运行与过滤运行对 TypeScript 行数的结论会严重冲突现在不再冲突。这四个测试分别对应报告聚合的四个关键数字总行数、重复行数、宿主行数、按格式过滤后的行数恰好与buildReport/mergeReports维护的total、top、others语义一一呼应。七、本地复现运行输出与读数方法README 明确说明所有命令从仓库根目录、在默认阈值下运行。复现 markdown 场景jscpd fixtures/embedded-stats-demo/markdown # Found 1 clones. # # │ markdown │ 2 │ 130 │ 2584 │ 0 │ 0 (0.00%) │ 0 (0.00%) │ # │ typescript │ 2 │ 26 │ 378 │ 1 │ 13 (50.00%) │ 189 (50.00%) │复现 Vue 组件场景jscpd fixtures/embedded-stats-demo/component # Found 1 clones. # # │ html │ 2 │ 60 │ 1134 │ 0 │ 0 (0.00%) │ 0 (0.00%) │ # │ typescript │ 2 │ 26 │ 378 │ 1 │ 13 (50.00%) │ 189 (50.00%) │ # │ vue │ 2 │ 98 │ 1512 │ 0 │ 0 (0.00%) │ 0 (0.00%) │输出表格的列依次为格式、源数量、行数、token 数、克隆数、克隆行数行百分比、克隆 token 数token 百分比。两种场景中 typescript 行均显示26行、13 (50.00%)与 README 对照表的 now 列一致——同样的重复代码在行与 token 两种度量下百分比吻合。component/场景解释了旧版本为什么会把 50% 算成 13.54%脚本最后一个 token 位于第 48 行旧逻辑据此把源长度误判为 48 行而真实代码只有 13 行——分子正确、分母被放大约 4 倍。这也是 README 给出的最实用校验法比较报告中的行百分比与 token 百分比两者测量同一份重复应当接近若差距悬殊就说明行统计混入了不含代码的文本。八、小结与延伸阅读回到reporting.md的标题 Reporting guide这份指南的代码块buildReportmergeReports加上其姊妹篇exporting.md共同演示了 jscpd 对嵌入代码的完整报告管线——识别子格式、按嵌入语言统计行数、聚合成报告并在每一步把宿主文本与嵌入代码严格分开。相关材料可继续深入阅读演示说明与运行输出fixtures/embedded-stats-demo/README.md报告聚合代码的组件形态ReportCard.vue 与 ExportCard.vue多格式扫描与合成源构建rust/crates/cpd-finder/src/orchestrate.rs#L425-L573嵌入判定与行计数实现rust/crates/cpd-core/src/models.rs#L358-L399回归测试与测试夹具embedded_statistics_integration.rs、guide-a.md通用使用指南README.md、docs/api.md如果要在自己的仓库里排查行百分比异常偏低/偏高优先检查项目是否包含 Markdown/Vue/Svelte/Astro 混合文件并用本文第七节的百分比对照法快速定位。赞分享开发工具代码质量静态分析【免费下载链接】jscpdCopy/paste detector for source code. 220 languages, Rust engine, SARIF/HTML/badge reporters, GitHub Action, MCP server for AI agents.项目地址https://gitcode.com/gh_mirrors/js/jscpd点击查看免费下载相关推荐jscpd 嵌入式代码块统计修复实战Markdown 与 SFC 组件中的重复代码行数如何正确计算jscpd 嵌入式代码块统计修复实战Markdown 与 SFC 组件中的重复代码行数如何正确计算 本文围绕 jscpd 修复 issue 1090 的完整过开发工具代码质量静态分析Milvus 嵌入式分组聚合Search Embedded Aggregation设计解读从 Elasticsearch 语义到向量检索的 GroupBy 指标 层级聚合Milvus 嵌入式分组聚合Search Embedded Aggregation设计解读从 Elasticsearch 语义到向量检索的 GroupBy数据库向量数据库分布式数据库后端网易云音乐升级API核心功能解析登录、签到与刷播放量全攻略网易云音乐升级API核心功能解析登录、签到与刷播放量全攻略 网易云音乐升级API是一个基于PHP语言开发的项目能够通过调用官方接口提供网易云音乐每日听满30后端上一篇64K上下文新标杆LongAlign-13B-64k如何重塑长文本处理范式下一篇轻量级HTTP服务器快速搭建指南零配置静态文件服务实战创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考