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

  1. 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.
  2. Skip blank lines. Take the first comment: a block comment up to its terminator, or a run of consecutive line comments.
  3. 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 contain copyright, 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, and fix inserts 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

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:

  1. missing: no header.
  2. License: nothing identifiable gives malformed; an identifiable license that differs from the rule gives wrong-license. If it matches, the form is checked: spdx style needs an SPDX tag (a notice alone is malformed); notice style needs a notice, and its text must match the template with a difflib similarity of at least 0.85 after removing copyright lines (so a reflowed notice is fine, a rewritten one is not). custom style accepts a header whose non-copyright text is at least 0.85 similar to the template.
  3. missing-owner: some configured owner has no copyright line (case-insensitive substring match).
  4. 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:

This is a heuristic aid, not legal advice.

Limitations