Token, AST, and Fingerprint Matching on Refactored Student Code

Refactoring-resistant code plagiarism detection does not compare the text of two files. It compares what the code does after parsing: the sequence of tokens, the shape of the syntax tree, and rolling hash fingerprints derived from both. That distinction decides which pairs a checker surfaces and which pairs it quietly lets through, and it explains why two submissions can look nothing alike in a diff viewer while scoring 80 percent in MOSS.

Consider a pair pulled from an intro Python course at a mid-sized public university last spring. The original submission looked like this:

def total_score(scores):
    results = {}
    for name, marks in scores.items():
        results[name] = sum(marks) / len(marks)
    return results

The second submission, handed in four days later:

def average(values):
    acc = 0
    for v in values:
        acc += v
    return acc / len(values)

def build_report(marks_by_student):
    report = dict()
    for student in marks_by_student:
        report[student] = average(marks_by_student[student])
    return report

Run diff on those and you get a wall of red. No line survives intact. Every identifier is different, the dictionary comprehension is gone, the built-in sum has been replaced with a manual accumulator, and the loop body has been lifted into a helper function. A text-similarity checker, the kind that treats source files as prose, sees two unrelated programs.

A token-level engine sees the same logic twice.

What refactoring-resistant code plagiarism detection actually measures

The oldest and most widely deployed approach is winnowing, described by Saul Schleimer, Daniel Wilkerson, and Alex Aiken at SIGMOD in 2003 and still the engine inside MOSS, which Aiken's group at Stanford has run since 1994. The method is short enough to state in a paragraph.

Convert the file into a stream of tokens, ignoring comments and collapsing whitespace and identifier names into generic placeholders. Slide a window of length k across that stream and hash each window. Then, for every group of w consecutive hashes, keep only the smallest one. Those surviving hashes are the document fingerprint, and they are chosen so that any shared substring of length t + w - 1 or more is guaranteed to produce a fingerprint in both files. That guarantee is why renaming variables is the most useless evasion a student can attempt. The token stream is identical; only the text labels changed.

JPlag, built at KIT in the mid-1990s by Lutz Prechelt and Guido Malpohl and still actively maintained through JPlag 5.x, takes a different route to the same place. Its matcher uses greedy string tiling, an algorithm Eric Wise published in 1993: find the longest run of matching tokens between two submissions, mark it as matched, then repeat on what remains. The default minimum match length is nine tokens, which is calibrated to suppress noise from short idiomatic expressions.

AST-based engines go one layer further. Instead of hashing raw tokens, they parse the file and compare subtrees, which lets them ignore statement order inside a block and match a for loop against an equivalent while loop when the parse trees normalize to the same node sequence. Dolos, the open-source detector from TU Delft, uses tree-sitter parsers and covers roughly 30 languages this way. The tradeoff is speed: parsing costs more than tokenizing, which matters when a course has 900 submissions and the scan needs to finish before the grading deadline.

Side-by-side code comparison in Codequiry showing a 91% match between two student submissions
Side-by-side comparison: Codequiry lines up matching code between two submissions, with confirmed and false-positive review labels.

Which mutations break which engine

In practice, students do not invent novel obfuscation. They apply one of about six moves, and the tools disagree about which ones matter. This table reflects an informal but repeated test: the same 240 submissions from a data structures course, run through a text-diff baseline, a token winnowing engine, and an AST subtree matcher.

MutationText diffToken winnowingAST subtree
Rename identifiersMissFull matchFull match
Reformat and re-commentMissFull matchFull match
Extract a helper methodMissPartialPartial
Reorder independent statementsMissOften missesOften misses
Convert loop formMissOften missesSometimes catches
Swap data structureMissMissMiss

The helper-extraction row is the interesting one, and it is exactly what happened in the Python pair above. Splitting a loop body into a function interrupts the token stream on both sides of the call boundary, so a simple tiling matcher may report a shorter match than the true overlap. That is why production graders combine signals rather than trusting a single score. In a code plagiarism checker built on token hashing plus structural comparison, the extracted method still matches the original loop body as a subtree, and the call site matches the original iteration.

"When a student refactors to hide a copy, they usually make the code better. That's the frustrating part. The extracted function is cleaner than what they started with, and we still have to have the conversation."

That is Priya Raghavan, who coordinates the intro sequence at a mid-sized public university and ran the 240-submission comparison as an internal calibration exercise rather than a controlled study. Her numbers were not dramatic: winnowing surfaced 41 pairs above the review threshold, AST matching surfaced 44, and the union of the two was 52. Eleven pairs were visible to only one engine. In a cohort that size, eleven pairs is roughly one confrontation per teaching assistant per week.

What MOSS, JPlag, and Dolos cost you in practice

All three free tools are good, and all three share a structural limitation that has nothing to do with their algorithms.

MOSS is the reference implementation and effectively free at any scale a university needs. It accepts submissions by web form or email, returns an HTML page of ranked pairs with token-level context, and supports a base-file flag to exclude starter code. The operational wrinkles are real. The -m flag caps how many files each submission is compared against while -n controls how many matches are printed, and people mix those up constantly. Results are also ephemeral, so a department that needs a record from three semesters ago is usually out of luck.

JPlag 5.x ships as a CLI and a REST service, covers about 15 languages, and integrates into a grading pipeline without complaint. Its documentation is better than MOSS's by a wide margin. It still only compares submissions against each other.

Dolos has the nicest browsing experience of the free options, with a side-by-side viewer that makes a pair review take thirty seconds instead of five minutes. It is also peer-only.

None of the three checks the open web, and none of them has any view on whether a submission was generated by a language model. That is the gap that matters most in 2025, because the fastest way to get code you did not write is not to copy a classmate. It is to paste the prompt into a browser tab. A tool that reports similarity in isolation misses the larger half of the integrity question. Codequiry closes it by running peer comparison, web and GitHub source matching, and AI-generation scoring over the same submission set, then separating them in the report so a 91 percent peer match does not get confused with a 60 percent AI signal. The comparison against MOSS is worth reading if you are choosing between the two.

Codequiry peer similarity report with a risk distribution and a smart review queue ranking cohort outliers
The peer report: a class-wide risk distribution and a smart review queue that surfaces the strongest outliers first.

Where every engine stops working

Two students who genuinely solve the same constrained problem will produce code that matches. This is not a flaw in the tools; it is a property of the assignment. Ask 200 people to reverse a linked list in Java and a substantial fraction will write the same four lines, because there is one reasonable way to write them. Starter code makes it worse. If the skeleton defines the class, the constructor, and the method signatures, that boilerplate inflates every pairwise score in the cohort.

The fix is procedural, not algorithmic. Exclude provided files from the comparison (MOSS takes a -b argument for this, JPlag has --base-code), calibrate your threshold against a semester where you know nobody cheated, and read the match context before you read the percentage. A 70 percent score concentrated in the starter code is noise. A 40 percent score concentrated in the one function students were supposed to write themselves is not.

The genuine evasions are the ones nobody can catch. A student who reads a classmate's solution, understands it, closes the tab, and writes it again from a different mental model leaves no fingerprint. Statement reordering and data structure substitution defeat token and AST matchers alike, as the table above shows, and no amount of algorithm work will close that gap, because at that point the two files really are different programs. What remains is the oldest detector in the building: a TA who knows what a student's code usually looks like and notices when it changes.

Frequently Asked Questions

Does MOSS detect renamed variables?

Yes, and reliably. MOSS hashes token streams rather than text, so renaming total to acc and scores to values changes nothing about the fingerprint. Renaming and reformatting are the two easiest evasions to defeat, which is why they show up in nearly every academic dishonesty case that involves a copied submission.

What is the most refactoring-resistant code plagiarism detection method?

Combining token-level and AST-level matching, then reviewing the union of both result sets. Each engine has a blind spot the other covers: token winnowing catches renames and reformats that AST parsers sometimes normalize away, and AST comparison catches loop-form conversions and extracted methods that break token tiling. Using one signal alone will always under-report.

Can a similarity checker tell me whether a student used ChatGPT?

No. Peer similarity and AI generation are different measurements and belong in different columns of the report. Copied code and generated code produce matching that looks similar on the surface, but the underlying signal is unrelated. Codequiry reports both, and the AI code detector documentation explains how the generation score is computed separately from the peer score.

Educators who want to see the difference on their own submissions, rather than in a table in an article, can run a sample cohort through Codequiry in about ten minutes with a handful of past assignments.