diff --git a/README.md b/README.md index 381a600..a249de6 100644 --- a/README.md +++ b/README.md @@ -177,6 +177,20 @@ When licence header validation/formatting is enabled, files are filtered using t * Suffixes: `.designer.cs`, `.g.cs`, `.min.js`, `.lock` * Any file containing `.generated.` in its name +### Header Detection + +* The header may appear anywhere in the file's leading preamble (blank lines, comments, + region directives, and directives such as C# `using`/`#nullable`/`#pragma`, `#pragma once` + or `'use strict'`) before the first line of code. +* Matching ignores comment markers, whitespace, line wrapping and marker-only (`//` or `#`) + padding lines, and accepts any copyright year. +* Headers wrapped in regions are supported for C# (`#region`/`#endregion`) and Python + (`#region`/`# region`). Further languages can be added to `REGION_SYNTAX` in + `main/licence_headers.py`. +* In fix mode, damaged headers are repaired in place, duplicate headers are removed, and a + missing header is inserted inside a region labelled `Licence`/`License`/`Copyright`/`Header` + if one exists, otherwise at the top of the file (after any shebang or encoding line). + ## Recommended settings ### To ensure the line endings are correctly converted: 1. On Windows: `git config --global core.autocrlf true` diff --git a/main/licence_headers.py b/main/licence_headers.py index 93cba62..a0bc6c1 100644 --- a/main/licence_headers.py +++ b/main/licence_headers.py @@ -18,7 +18,10 @@ import argparse import codecs +from dataclasses import dataclass from datetime import datetime +from difflib import SequenceMatcher +from functools import lru_cache from io import BytesIO from pathlib import Path, PurePosixPath import re @@ -35,14 +38,87 @@ IGNORED_SUFFIXES = ('.designer.cs', '.g.cs', '.min.js', '.lock') TEMPLATE_DIRECTORY = Path(__file__).resolve().parent / 'headers' PYTHON_ENCODING_PATTERN = re.compile(r'^[ \t\f]*#.*?coding[:=][ \t]*[-\w.]+') +PYTHON_BLANK_OR_COMMENT_PATTERN = re.compile(r'^[ \t\f]*(?:[#\r\n]|$)') LEGACY_HEADER_PATTERN = re.compile( r'^(#|//) Copyright The Cambridge Crystallographic Data Centre ' r'\(CCDC\) \d{4}(?:, \d{4})?\r?\n?$' ) +COMMENT_MARKERS = {'hash': '#', 'slash': '//'} +YEAR_TOKEN = '{{ .Year }}' +YEAR_PATTERN = r'\d{4}(?:, \d{4})?' +IDENTITY_PREFIX = 'This code is' +IDENTITY_OWNER = 'Crystallographic Data Centre (CCDC)' +LICENCE_REGION_LABEL = re.compile(r'licen[cs]e|copyright|header', re.IGNORECASE) +DECODE_ERROR = 'file encoding could not be decoded' + +# Region open/close directives per file extension. To support another +# language, add its extension with (open pattern, close pattern); the open +# pattern may capture a "label" group used to find a licence region. +REGION_SYNTAX = { + '.cs': ( + re.compile(r'^\s*#region\b(?P