| #!/usr/bin/env python |
| |
| ##===--- iwyu-check-license-header.py - check license headers -------------===## |
| # |
| # The LLVM Compiler Infrastructure |
| # |
| # This file is distributed under the University of Illinois Open Source |
| # License. See LICENSE.TXT for details. |
| # |
| ##===----------------------------------------------------------------------===## |
| |
| from __future__ import print_function |
| import sys |
| import os |
| import re |
| import argparse |
| |
| |
| # This is used in selected functions to calculate the maximum length of filename |
| # and filler dashes. Not otherwise useful. |
| EMBELLISHMENTS = '//===--- ---===//' |
| |
| HDRFORMAT = """ |
| {c} |
| {c} The LLVM Compiler Infrastructure |
| {c} |
| {c} This file is distributed under the University of Illinois Open Source |
| {c} License. See LICENSE.TXT for details. |
| {c} |
| {t}===----------------------------------------------------------------------==={t} |
| """ |
| |
| |
| def make_hdrformat(one, two): |
| """ Materialize HDRFORMAT based on the one and two comment styles. |
| |
| The one and two args should be '#' and '##' or '//' and '//', respectively. |
| Returns a list of lines. |
| """ |
| r = HDRFORMAT.lstrip().format(c=one, t=two) |
| return r.splitlines() |
| |
| |
| def truncated(filename): |
| """ Truncate the filename with ellipsis if too long """ |
| maxlen = 80 - len(EMBELLISHMENTS) |
| trunclen = maxlen - 3 # ... |
| if len(filename) > maxlen: |
| filename = filename[:trunclen] + '...' |
| return filename |
| |
| |
| def make_license_header(filename, one, two): |
| """ Build a valid license header from filename and comment styles. |
| |
| The one and two args should be '#' and '##' or '//' and '//', respectively. |
| Returns a list of lines. |
| """ |
| assert len(two) == 2 |
| filename = os.path.basename(filename) |
| filename = truncated(filename) |
| |
| def dashes(): |
| c = 80 |
| c -= len(EMBELLISHMENTS) |
| c -= len(filename) |
| return '-' * c |
| |
| firstline = '%s===--- %s %s===%s' % (two, filename, dashes(), two) |
| return [firstline] + make_hdrformat(one, two) |
| |
| |
| def format_file_error(filename, *lines): |
| """ Format an error message from filename and lines """ |
| lines = list(lines) |
| lines[0] = '%s: %s' % (filename, lines[0]) |
| return os.linesep.join(lines) |
| |
| |
| def find_license_header(lines, two): |
| """ Return an index where the license header begins """ |
| if not lines: |
| return -1 |
| |
| for i, line in enumerate(lines): |
| # Allow leading blank lines and hash-bangs. |
| if not line or line.startswith('#!'): |
| continue |
| |
| # Besides those, the first line should be a license header. |
| if line.startswith(two + '==='): |
| break |
| |
| # If not, this fails the test entirely. |
| return -1 |
| |
| return i |
| |
| |
| class File(object): |
| """ Base class for a source file with a license header |
| |
| Do not use directly, instead use File.parse to instantiate a more derived |
| class. |
| |
| Derived classes must have three class variables: |
| |
| * one - One comment char ('#' for Python, '//' for C++) |
| * two - Two comment chars ('##' for Python, '//' for C++) |
| * pattern - a regex matching the first line in a license header |
| """ |
| def __init__(self, filename): |
| with open(filename, 'r') as fd: |
| content = fd.read() |
| |
| self.lines = list(content.splitlines()) |
| self.hdrindex = find_license_header(self.lines, self.two) |
| self.filename = filename |
| self.errors = [] |
| |
| @classmethod |
| def parse(_, filename): |
| """ Return an object derived from File to analyze license headers """ |
| _, ext = os.path.splitext(filename) |
| if ext == '.py': |
| klass = PythonFile |
| elif ext in ('.h', '.c', '.cc'): |
| klass = CxxFile |
| else: |
| return None |
| |
| return klass(filename) |
| |
| def has_license_header(self): |
| """ Return True if a license header has been found """ |
| return self.hdrindex != -1 |
| |
| def add_license_header(self): |
| """ Add license header to a file that doesn't have one. """ |
| assert not self.has_license_header() |
| |
| # Find insertion point |
| for p, line in enumerate(self.lines): |
| # Skip past leading blank lines and hash-bangs. |
| if line and not line.startswith('#!'): |
| break |
| |
| # Split the lines around the insertion point |
| if self.lines: |
| before, after = self.lines[:p], self.lines[p:] |
| else: |
| before, after = [], [] |
| |
| # Rebuild the contents with the license header in the middle |
| lines = before |
| if before and before[-1] != '': |
| lines += [''] |
| lines += make_license_header(self.filename, self.one, self.two) |
| if after and after[0] != '': |
| lines += [''] |
| lines += after |
| |
| # Write back out |
| with open(self.filename, 'wb') as fd: |
| fd.write('\n'.join(lines)) |
| fd.write('\n') |
| |
| def check_license_header(self): |
| """ Check that the header lines follow convention. |
| |
| Returns True if everything is OK, otherwise returns False and populates |
| self.errors with all found errors. |
| """ |
| if not self.has_license_header(): |
| self.file_error('No license header found') |
| return False |
| |
| hdrlines = self.lines[self.hdrindex:self.hdrindex+8] |
| |
| # First line has the most structure |
| line = hdrlines[0] |
| if len(line) != 80: |
| self.line_error( |
| 1, 'Bad header line length (expected: 80, was: %d)' % len(line), |
| " Header line: '%s'" % line) |
| |
| m = self.pattern.match(line) |
| if not m: |
| self.line_error(1, 'Bad header line', |
| " Expected: '%s'" % self.pattern.pattern, |
| " Actual: '%s'" % line) |
| else: |
| hfilename = truncated(m.group(1)) |
| xfilename = truncated(os.path.basename(self.filename)) |
| if hfilename != xfilename: |
| self.line_error(1, 'Bad header filename', |
| " Expected: '%s'" % xfilename, |
| " Actual: '%s'" % hfilename) |
| |
| # The following seven lines always follow the layout of HDRFORMAT. |
| hdrformat = make_hdrformat(self.one, self.two) |
| hdrlines = hdrlines[1:] |
| for lineno, (expected, actual) in enumerate(zip(hdrformat, hdrlines)): |
| if expected != actual: |
| self.line_error(lineno + 2, 'Bad header line', |
| " Expected: '%s'" % expected, |
| " Actual: '%s'" % actual) |
| |
| return not self.errors |
| |
| def file_error(self, *lines): |
| """ Log an error for the file """ |
| self.errors.append(format_file_error(self.filename, *lines)) |
| |
| def line_error(self, lineno, *lines): |
| """ Log an error for a specific line in the file """ |
| lines = list(lines) |
| lines[0] = '%s:%d: %s' % (self.filename, lineno, lines[0]) |
| self.errors.append(os.linesep.join(lines)) |
| |
| |
| class PythonFile(File): |
| """ Python file with license header """ |
| one = '#' |
| two = '##' |
| pattern = re.compile( |
| r'##===--- ([a-z0-9_.-]+) -[A-Za-z0-9_.,/# -{}]+===##') |
| |
| def __init__(self, filename): |
| super(PythonFile, self).__init__(filename) |
| |
| |
| class CxxFile(File): |
| """ C++ file with license header """ |
| one = '//' |
| two = '//' |
| pattern = re.compile( |
| r'//===--- ([a-z0-9_.-]+) -[A-Za-z0-9_.,/# -{}]+ [-* C+]+===//') |
| |
| def __init__(self, filename): |
| super(CxxFile, self).__init__(filename) |
| |
| |
| def main(filenames, add_if_missing): |
| """ Entry point. |
| |
| Checks license header of all filenames provided. |
| Returns zero if all license headers are OK, non-zero otherwise. |
| """ |
| errors = [] |
| for filename in filenames: |
| if os.path.isdir(filename): |
| continue |
| |
| checker = File.parse(filename) |
| if not checker: |
| # TODO: Consider printing a warning here in verbose mode. |
| continue |
| |
| if not checker.check_license_header(): |
| errors.extend(checker.errors) |
| |
| if add_if_missing and not checker.has_license_header(): |
| checker.add_license_header() |
| |
| for err in errors: |
| print(err) |
| |
| return len(errors) |
| |
| |
| if __name__ == '__main__': |
| parser = argparse.ArgumentParser('IWYU license header checker') |
| parser.add_argument('filename', nargs='+') |
| parser.add_argument('--add', action='store_true') |
| args = parser.parse_args() |
| sys.exit(main(args.filename, args.add)) |