Design and algorithms
Pipeline
scan.Project.discover -> scan.load -> header.parse_source -> engine.analyze / fix / strip
(git ls-files / walk) (skip rules, (preamble, header (rules, years, templates)
language) block, copyright lines)
engine works on a Source: the decoded lines (each keeping its own line ending), the BOM flag, the dominant line ending, the end of the preamble and the parsed header. Nothing is re-serialised from a model, so untouched lines are byte-identical after a fix.
1. Locating an existing header
- Skip the preamble (
header.preamble_end). Each language lists the preamble kinds it knows, in order; a kind consumes at most one line, except Ruby magic comments. - Skip blank lines. Take the first comment: a block comment up to its terminator, or a run of consecutive line comments.
- Reject it if it starts with a directive (
//go:build,# vim:,# shellcheck,// swift-tools-version,{-#...) or does not look like a license header: it must containcopyright,SPDX-License-Identifier,(c) 2024,©or one of a few notice phrases (permission is hereby granted,licensed under,free software...). A leading doc comment is therefore not mistaken for a header, andfixinserts a new header above it.
Each comment line records the character range of its text (after //, *, ...). That is what makes in-place edits possible: a year change rewrites only the [start, end) slice of a raw line, so decorations, trailing */ and line endings survive.
2. Identifying the license
- SPDX tag:
SPDX-License-Identifier: <expression>. - Notice: the lower-cased, whitespace-collapsed text is matched against ordered signatures. The first signature whose patterns all match wins, so wording shared between licenses resolves to the most specific one.
| Text contains | Result |
|---|---|
gnu general public license + version 3 + any later version |
GPL-3.0-or-later |
the same without any later version |
GPL-3.0-only |
redistribution and use in source and binary forms + neither the name |
BSD-3-Clause |
redistribution and use in source and binary forms only |
BSD-2-Clause |
permission is hereby granted, free of charge or MIT License |
MIT |
SPDX ids are compared canonically and case-insensitively: GPL-3.0+ equals GPL-3.0-or-later and GPL-2.0 equals GPL-2.0-only.
3. Problems
For a header and the rule for that path, engine.analyze reports:
missing: no header.- License: nothing identifiable gives
malformed; an identifiable license that differs from the rule giveswrong-license. If it matches, the form is checked:spdxstyle needs an SPDX tag (a notice alone ismalformed);noticestyle needs a notice, and its text must match the template with adifflibsimilarity of at least 0.85 after removing copyright lines (so a reflowed notice is fine, a rewritten one is not).customstyle accepts a header whose non-copyright text is at least 0.85 similar to the template. missing-owner: some configured owner has no copyright line (case-insensitive substring match).outdated-year: a copyright line of a configured owner does not cover the required years.
Copyright lines are recognised by Copyright, (c), © or SPDX-FileCopyrightText: followed by an optional year list (2019, 2019-2024, 2019, 2021, 2024). Prose such as "retain the above copyright notice" is not mistaken for one.
Year coverage
Years come from git (years.git_year_ranges) or the fixed config. The required span is [first, last]; a copyright line covers it when start <= first and end >= last. A line claiming earlier years (2015-2024 on a file first committed in 2019, as after an import) is therefore fine. Templates that only use {year} require just the last year.
Worked example (file first committed 2019, last changed 2024):
| Existing line | Outcome |
|---|---|
Copyright (c) 2019-2024 Acme |
ok |
Copyright (c) 2019 Acme |
outdated, fixed to 2019-2024 |
Copyright (c) 2022-2024 Acme |
outdated, fixed to 2019-2024 |
Copyright (c) 2010-2024 Acme |
ok |
Copyright (c) Acme |
outdated, years inserted: 2019-2024 |
Copyright (c) 2010 Other Corp |
untouched (not a configured owner) |
Years are computed in one git log --name-status -M --relative pass for the whole repository. The log is walked newest to oldest; a rename old -> new seen on the way back makes old an alias of new, so history before the rename still counts. Files unknown to git (new, untracked) get the current year. Working-tree edits are deliberately ignored: otherwise a fix would dirty the file and check would then demand the current year, breaking idempotency.
4. Fixing
| Situation | Action |
|---|---|
missing |
Insert a freshly rendered header after the preamble. |
only outdated-year |
In-place edit of the stale copyright lines. Nothing else changes. |
malformed, missing-owner, wrong-license with --relicense |
Rebuild the header block: existing copyright lines are kept (years extended where stale), missing owners are added, license text is regenerated from the template. Other lines inside the old header (author notes, All rights reserved.) are replaced. The block is re-rendered in the language's preferred comment style. |
wrong-license without --relicense |
File skipped and reported. A wrong license may be a deliberate third-party file, so headerkit never overwrites it silently. |
Because the result of every fix passes analyze, running fix twice is a no-op (covered by the golden tests, which assert analyze == [] and fix == (None, []) on every output).
5. Rendering
Header text is a list of plain lines. header.render wraps it in the language's preferred style:
# line: /* block */: <!-- html -->:
# Copyright ... /* <!--
# SPDX-... * Copyright ... Copyright ...
* SPDX-... SPDX-...
*/ -->
Blank template lines become bare comment markers (#, *) without trailing spaces.
6. Conflict detection
report collects the license ids of all files (expressions containing OR are skipped: the user may choose) and flags:
GPL-2.0-onlytogether withApache-2.0, or with any GPL/AGPL-3.0 id.- A GPL/AGPL id in a project whose configured license is permissive (MIT, BSD, ISC, Apache-2.0).
This is a heuristic aid, not legal advice.
Limitations
- Only the first comment of a file is considered; a license header after code is not found.
- Rebuilding a malformed header drops non-license lines inside it.
- Renames detected by git (
-M) only; copies are not followed. - Non-UTF-8 files are skipped.