- 代码质量
- 教育
- 后端
【免费下载链接】JPlag
State-of-the-Art Source Code Plagiarism & Collusion Detection. Check for plagiarism in a set of programs.
JPlag 是一款本地运行的源代码抄袭与串通检测工具,其多语言能力通过一组可插拔的语言模块实现。本文基于 JPlag 仓库中的 支持语言文档 展开,结合languages/目录下的 20 余个语言模块源码、Language 接口 与 LanguageLoader 的加载实现,系统梳理:JPlag 当前支持哪些语言及其版本、各模块成熟度标签(mature/beta/alpha/legacy/unknown)的确切含义、语言模块必须遵守的接口契约(文件后缀、最小匹配 token 数、语义信息、归一化等),以及 CLI 子命令如何动态发现并调用语言模块。读完后你可以准确为不同语言的提交集选择合适的检测模块,并理解新增语言模块的接入方式。
一、当前支持的语言清单
根据 README 的 Supported Languages 表格,JPlag 支持的完整语言清单、对应语言版本、CLI 参数名、成熟度标签(state)与所用解析器如下(本表继承自 README,CLI 参数名已按源码中的实际 identifier 校准):
| Language | Version | CLI 参数名 | state | parser |
|---|---|---|---|---|
| Java | 25 | java | mature | JavaC |
| C | 11 | c | legacy | JavaCC |
| C++ | 14 | cpp | mature | ANTLR 4 |
| C# | 6 | csharp | mature | ANTLR 4 |
| Python | 3.6 | python3 | mature | ANTLR 4 |
| JavaScript | ES6 | javascript | beta | ANTLR 4 |
| TypeScript | ~5 | typescript | beta | ANTLR 4 |
| Go | 1.17 | go | beta | ANTLR 4 |
| Kotlin | 1.3 | kotlin | mature | ANTLR 4 |
| R | 3.5.0 | rlang | mature | ANTLR 4 |
| Rust | 1.60.0 | rust | mature | ANTLR 4 |
| Swift | 5.4 | swift | beta | ANTLR 4 |
| Scala | 2.13.8 | scala | mature | Scalameta |
| LLVM IR | 15 | llvmir | beta | ANTLR 4 |
| Scheme | ? | scheme | legacy | JavaCC |
| EMF Metamodel | 2.25.0 | emf | beta | EMF |
| EMF Model | 2.25.0 | emf-model | alpha | EMF |
| SCXML | 1.0 | scxml | alpha | XML |
| Text(naive, use with caution) | - | text | legacy | CoreNLP |
| Multi-Language | - | multi | alpha | - |
两点源码层面的补充说明:
- 从源码看,Go 模块 GoLanguage 的
getIdentifier()返回go,README 的 CLI 帮助列表(Languages:小节)也以go为准;README 表格中golang的写法与当前源码不一致,使用时以go为准。 languages/目录中还包含一个额外的 Maven 子模块 emf-metamodel-dynamic,其 DynamicEmfLanguage 提供基于动态 Ecore 模型的 EMF 检测能力(identifier 为emf-dynamic)。从源码结构看,该模块未标注@AutoService,因此不会出现在 CLI 的Languages:子命令列表中,主要面向 API 集成场景。
语言模块的“版本”一列指模块所依据的语法/语言版本,并非承诺对更新版本的全量支持;运行 JPlag 本身需要 Java SE 25(见 README)。
二、语言模块的成熟度标签(state)
支持语言文档 指出,由于各语言模块的年龄与被使用频率不同,其成熟度存在差异,因此每个前端(frontend)都被赋予一个 state 标签。这五类标签的定义是选用模块时的第一参考依据:
mature:经过充分验证,并且(大致)与当前语言版本保持同步;beta:相对较新且(大致)保持同步,但测试覆盖不如 mature 充分,欢迎反馈(Feedback welcome);alpha:非常新、尚未完成,请谨慎使用(Use with caution);legacy:来自 JPlag legacy(v3.0.0 之前),可能只支持过时的语言版本,等待更新;unknown:该模块处于什么状态尚不明确。
对照上表可以看到:Java、C++、C#、Python、Kotlin、R、Rust、Scala 为 mature;JavaScript、TypeScript、Go、Swift、LLVM IR、EMF Metamodel 为 beta;EMF Model、SCXML、Multi-Language 为 alpha;C、Scheme、Text 为 legacy。例如 C 模块的 README 还特别提示:该模块“可能”可以解析 C++ 提交,但应使用 cpp 模块处理 C++ 代码——这正是 legacy 模块能力边界的典型说明。
三、语言模块的源码组织
所有语言模块集中在 languages/ 目录,由 languages/pom.xml 聚合为 21 个 Maven 子模块:c、cpp、csharp、emf-metamodel、emf-metamodel-dynamic、emf-model、golang、java、kotlin、python-3、rlang、rust、scala、scheme、scxml、swift、text、typescript、javascript、llvmir、multi-language。
每个模块内部遵循统一结构(详见 Adding New Languages 指南):
| 组件 | 父类型 | 职责 |
|---|---|---|
| Language 类 | de.jplag.Language | 语言模块的访问入口 |
pom.xml | - | Maven 子模块描述符 |
README.md | - | 模块级文档 |
| TokenType 类 | de.jplag.TokenType | 语言专属 token 类型 |
| Lexer/Parser | - | 源码到 AST 的转换 |
| ParserAdapter 类 | de.jplag.AbstractAntlrParser(ANTLR 场景) | 装配 Parser 并调用遍历器 |
| Traverser/Listener 类 | - | 遍历 AST 并生成 token |
值得注意的核心抽象是:JPlag 中的Token 不是词法单元(如标识符、关键字),而是语法实体(如语句、控制结构)的表示。例如class MyClass { private String name; }会被表示为CLASS_DECLARATION、CLASS_BODY_BEGIN、FIELD_DECLARATION、CLASS_BODY_END这样的 token 序列——标识符、字面量等语义信息被有意省略,从而让比较算法聚焦于代码结构。各模块的完整测试文件(如 Complete.go、Complete.R)覆盖了目标语言的主要语法元素,可用于直观了解各模块的 token 化粒度。
四、语言模块的接口契约:Language接口
每个语言模块必须实现 language-api 中的Language接口。该接口定义了 CLI 与核心比较算法依赖的全部契约,关键方法包括:
| 方法 | 说明 |
|---|---|
fileExtensions() | 该语言程序文件的合法扩展名(不区分大小写;空列表表示接受所有扩展名) |
getName() | 描述性名称,如Java |
getIdentifier() | 用于 CLI 选项与动态加载的短标识符,建议符合[a-z_-]+ |
minimumTokenMatch() | 该语言下构成一次匹配所需的最少 token 数(默认比较灵敏度基线) |
parse(Set<File>, boolean normalize) | 解析一个提交的全部文件,返回 token 列表 |
tokensHaveSemantics() | 返回的 token 是否携带语义信息 |
supportsNormalization()/requiresCoreNormalization() | 是否支持 token 序列归一化,以及是否使用 core 模块的归一化 |
useViewFiles()/viewFileExtension() | 报告展示是否使用“视图文件”(对输入文件的另一种表示) |
expectsSubmissionOrder()/customizeSubmissionOrder() | 提交顺序是否影响解析结果 |
supportsMultiLanguage()/hasPriority() | 能否被 multi 模块复用;多模块支持同一扩展名时是否优先 |
getOptions() | 返回语言专属选项对象(供 CLI 子命令展开) |
getCommentExtractorSettings() | 供注释提取器使用的分隔符配置 |
4.1 各模块的最小匹配 token 数(minimumTokenMatch)
minimumTokenMatch()是每个模块自行声明的“一次匹配至少需要多少 token”。从各语言模块源码可直接读出当前取值:
| 模块 | 最小匹配 token 数 | 模块 | 最小匹配 token 数 |
|---|---|---|---|
C (c) | 12 | Kotlin (kotlin) | 8 |
C++ (cpp) | 12 | LLVM IR (llvmir) | 70 |
C# (csharp) | 8 | Python 3 (python3) | 12 |
EMF (emf) | 6 | R (rlang) | 8 |
Go (go) | 8 | Rust (rust) | 8 |
Java (java) | 9 | Scala (scala) | 8 |
SCXML (scxml) | 6 | Swift (swift) | 8 |
Scheme (scheme) | 13 | Text (text) | 5 |
TypeScript (typescript) | 12 | JavaScript (javascript) | 继承 TypeScript(12) |
这些默认值可被 CLI 的-t, --min-tokens参数覆盖以调节灵敏度(取值过小会增加误报,见 README CLI 说明)。其中 LLVM IR 高达 70,反映了 IR 文本 token 密度高、需要更长片段才算有效匹配的设计取向;Text 模块仅 5,对应其“naive、谨慎使用”的定位。
4.2 文件扩展名与模块间关系
各模块声明的扩展名同样决定了 CLI 按什么文件后缀收集提交。几个具有代表性的例子:
- CLanguage 声明
.cpp、.cxx、.c++、.c、.cc、.h、.hpp、.hh、.hxx——因此 C 模块也能吃掉 C++ 文件; - CPPLanguage 声明完全相同的扩展名集合,并通过
hasPriority()返回true声明“对 C 语言模块拥有优先级”(源码注释:Priority over the C language module),保证 multi 场景下 C++ 文件优先交给更成熟的 cpp 模块; - JavaScriptLanguage 直接继承 TypeScriptLanguage,仅改写 identifier(
javascript)、名称与扩展名(.js),注释明确说明“JavaScript 作为 TypeScript 的一个变体,将全部责任委托给 TypeScript 语言模块”; - NaturalLanguage(text 模块)接受
.txt、.asc、.tex、.md、.rtf、.csv、.wiki、.json、.yaml、.yml、.xml等大量文本类后缀。
此外,Java 模块还通过tokensHaveSemantics()与supportsNormalization()返回true,表明它返回带语义的 token 并支持 CLI--normalize选项(README 说明该选项当前支持 Java 与 C++);JavaLanguage 还通过getCommentExtractorSettings()提供了字符串分隔符("""、"、')、行注释(//)、块注释(/* */)与转义符(\)配置,用于注释提取场景。
五、语言模块的加载机制与 CLI 集成
5.1 ServiceLoader 动态发现
语言模块的注册与发现全部基于 Java SPI:每个语言类通过@AutoService(Language.class)注解(编译期生成服务注册文件)声明自己是Language实现,例如 JavaLanguage 类头的@AutoService(Language.class)。
LanguageLoader 是发现逻辑的核心:
for (Language language : ServiceLoader.load(Language.class)) { String languageIdentifier = language.getIdentifier(); if (languages.containsKey(languageIdentifier)) { logger.error("Multiple implementations for a language '{}' are present in the classpath! Skipping ..", languageIdentifier); languages.remove(languageIdentifier); continue; } ... languages.put(languageIdentifier, language); }要点:
- 通过
ServiceLoader.load(Language.class)扫描 classpath 上所有语言实现,按getIdentifier()建立映射并缓存(getAllAvailableLanguages()); - 若同一 identifier 出现多个实现,会记录错误日志并跳过该 identifier,避免歧义;
getLanguage(String identifier)按短名加载单个模块,未命中时输出 WARN 日志。这意味着 classpath 中放入了某个语言 jar,它就能被识别——CLI 帮助中列出的 20 个Languages:子命令正是这 20 个@AutoService模块的投影(见 cli/pom.xml 对languages/各模块的依赖聚合)。
5.2 CLI:子命令优先于-l选项
README 的 CLI 说明 规定:语言可以用-l <language>参数设置,也可以作为子命令传入(jplag [jplag options] -l <language name> [language options]),且子命令优先于-l。源码中 CliInputHandler 直接以子命令名查询语言模块:
Language language = LanguageLoader.getLanguage(subcommand.commandSpec().name()) .orElseThrow(() -> new CliException(IMPOSSIBLE_EXCEPTION));子命令形式的主要好处是支持语言专属选项:每个模块可通过getOptions()返回一个 LanguageOptions 子类,其中的选项会自动展开为对应子命令的参数(例如jplag java -h可查看 Java 子命令的全部选项)。MultiLanguage 即是一个自带选项的示例:它允许指定参与多语言解析的具体语言集合,其minimumTokenMatch()取所选语言中的最小值。
5.3 Multi-Language 模块的边界
multi子命令由 MultiLanguage 实现,是一个“多语言门面”:
fileExtensions()聚合 classpath 上所有其他语言模块的扩展名;parse()将每个源文件按其扩展名委托给对应的具体语言模块(MultiLanguageParser);- 解析时会输出警告:
"This module only allows parsing of multiple languages. No comparisons will be made between languages"——即它只解决“混合语言提交集都能被解析”的问题,相似度比较仍只在同语言 token 列表之间进行; - 其
supportsMultiLanguage()返回false,防止被再次嵌套进 multi 门面。
六、按场景选择语言模块的实践建议
结合上述事实,选型时可按以下规则判断:
- C 与 C++ 混用:优先使用
cpp(mature、ANTLR 4、对 C 扩展名有优先级);c(legacy)仅在需要旧版 C 行为时考虑,其 README 也明确建议 C++ 代码使用 cpp 模块; - JavaScript/TypeScript 提交集:两者共用 TypeScript 语法实现,
javascript模块只是扩展名与标识符不同的变体,可任选其一处理各自文件,多文件混合时使用multi; - EMF 场景:
.ecore元模型文件用emf(beta);EMF 实例模型用emf-model(alpha);API 集成且元模型动态变化时可研究emf-metamodel-dynamic子模块; - 纯文本:
text模块基于 CoreNLP 的朴素分词,标签为 legacy 且 README 标注“naive, use with caution”,其结果解释需格外谨慎; - 对结果稳健性要求高:优先选择 mature 模块(Java、C++、C#、Python、Kotlin、R、Rust、Scala),beta/alpha 模块的结果建议作为参考并结合人工复核;
- 灵敏度调节:在选定模块后,用
-t/--min-tokens在该模块minimumTokenMatch默认值附近调节,并用--similarity-threshold过滤低相似度比较(均见 README CLI 参数表)。
七、验证与扩展:TokenPrinter、language-testutils 与新增模块流程
理解各语言模块“实际产生了什么”,最直观的工具是 TokenPrinter:它逐行打印源代码,并在每行下方标注该行产生的 token,例如:
10 public class Example { |CLASS |CLASS{ 11 private int number; |FIELD各语言模块的单元测试普遍采用 language-testutils 提供的基础设施:测试类继承LanguageModuleTest,通过TestDataCollector声明测试源文件、覆盖率检查与addTokenPositionTests位置断言(测试文件内用>标记源码行、$行标注期望 token 的列号、类型与长度)。以 languages 聚合 POM 为例,language-testutils已作为test-jar依赖注入所有语言模块的测试作用域。
若要新增语言模块,docs/4.-Adding-New-Languages.md 给出了完整流程:创建 Language 类与 TokenType 实现(ANTLR 场景可复用language-antlr-utils中的AbstractAntlrLanguage/AbstractAntlrParser框架)、在 languages/pom.xml 注册子模块、为 cli 与 coverage-report 模块添加依赖、更新 README 与docs文档。由于加载完全依赖 ServiceLoader,只要新模块被加入 classpath 且带@AutoService(Language.class),CLI 子命令与-l选项即可自动识别该语言。
小结
JPlag 的语言支持体系可以概括为三层:其一,languages/下 21 个 Maven 子模块按统一契约(Language接口)封装各语言的解析与 token 化,并各自标注 mature/beta/alpha/legacy/unknown 成熟度标签;其二,LanguageLoader 通过 ServiceLoader 以 identifier 为键动态发现模块,使 CLI 子命令、-l选项与 Java API(JPlagOptions直接持有Language实例)三条使用路径共享同一套语言注册表;其三,multi门面与hasPriority()/supportsMultiLanguage()等机制处理了 C/C++、JavaScript/TypeScript 等扩展名重叠与混合提交集的边界情形。选择检测方案时,先按语言在上表中的 state 标签评估可信度,再用模块默认值(扩展名、minimumTokenMatch)与-t等 CLI 参数校准灵敏度,即可得到可复现的检测配置。
- 代码质量
- 教育
- 后端
【免费下载链接】JPlag
State-of-the-Art Source Code Plagiarism & Collusion Detection. Check for plagiarism in a set of programs.
相关推荐
A380X WASM 系统架构解析:Rust 与 C++ 双语言机载系统模块全景
A380X WASM 系统架构解析:Rust 与 C++ 双语言机载系统模块全景 导读 :本文以仓库中 fbw a380x/src/wasm/README.md
游戏开发Tuist模块文档翻译:多语言支持
Tuist模块文档翻译:多语言支持 在全球化软件开发中,多语言支持是提升用户体验的关键环节。Tuist作为一款专注于简化Apple平台项目管理的工具,通过系统化
开发工具CLI后端云原生react-native-app-tour事件回调实战:如何用3个DeviceEventEmitter事件追踪用户引导完成情况
react native app tour事件回调实战:如何用3个DeviceEventEmitter事件追踪用户引导完成情况 react native app
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考