From 7be7952bd01613dd82e53f743e7e71482e156343 Mon Sep 17 00:00:00 2001 From: Emerson Knapp Date: Thu, 10 Sep 2026 10:16:33 -0700 Subject: [PATCH] feat: copyright headers for go, javascript, and typescript Extend the `//` comment-style file set of `polymath-copyright` to Go, JavaScript, JSX, TypeScript, and TSX, so those languages get the same header insertion and validation as C and C++. The `--relicense` strip loop now stops at Go directive comments, which keeps a `//go:build` or `// +build` constraint out of the block it deletes. Co-Authored-By: Claude Fable 5.1 Signed-off-by: Emerson Knapp --- .pre-commit-hooks.yaml | 4 +- README.md | 4 +- .../checkers/copyright/__init__.py | 23 ++++-- tests/test_copyright.py | 74 ++++++++++++++++++- 4 files changed, 92 insertions(+), 13 deletions(-) diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml index e969eab..528c548 100644 --- a/.pre-commit-hooks.yaml +++ b/.pre-commit-hooks.yaml @@ -98,10 +98,10 @@ name: Polymath Code Standard [copyright] description: > Insert copyright headers. Handles all supported file types: - Python/CMake/Shell (# style) and C/C++ (// style). + Python/CMake/Shell (# style) and C/C++/Go/JavaScript/JSX/TypeScript/TSX (// style). Requires args: [--license, , --copyright-org, ] entry: polymath_code_standard copyright - types_or: [python, cmake, shell, c, c++] + types_or: [python, cmake, shell, c, c++, go, javascript, jsx, ts, tsx] exclude: (^|/)\.envrc$ - <<: *python-hook diff --git a/README.md b/README.md index 3fc2256..7c2a85b 100644 --- a/README.md +++ b/README.md @@ -123,10 +123,10 @@ No arguments. ### `polymath-copyright` -Inserts and validates copyright headers for Python, CMake, Shell, C, and C++ files. +Inserts and validates copyright headers for Python, CMake, Shell, C, C++, Go, JavaScript, JSX, TypeScript, and TSX files. Also creates or updates the `LICENSE` file (skipped for proprietary licenses). Python, CMake, and Shell files use `#` comment style. -C and C++ files use `//` comment style. +C, C++, Go, JavaScript, JSX, TypeScript, and TSX files use `//` comment style. **Required:** diff --git a/polymath_code_standard/checkers/copyright/__init__.py b/polymath_code_standard/checkers/copyright/__init__.py index 2c1e6df..7f4e721 100644 --- a/polymath_code_standard/checkers/copyright/__init__.py +++ b/polymath_code_standard/checkers/copyright/__init__.py @@ -11,6 +11,8 @@ from polymath_code_standard.checkers.copyright.licenses import PROPRIETARY, get_license_full_text, get_license_header from polymath_code_standard.insert_license import COPYRIGHT_ORG_SENTINEL +_GO_DIRECTIVE_PATTERN = re.compile(r'//\s*(go:|\+build)') + @check_group class CopyrightGroup(CheckerGroup): @@ -64,13 +66,13 @@ def run(self, args: argparse.Namespace) -> list[Result]: header_text = get_license_header( args.license_id, args.copyright_year, insert_org, reuse_style_header=args.reuse_style ) - py_cmake_shell = filter_files(args.files, frozenset({'python', 'cmake', 'shell'})) - cpp = filter_files(args.files, frozenset({'c', 'c++'})) + hash_style = filter_files(args.files, frozenset({'python', 'cmake', 'shell'})) + slash_style = filter_files(args.files, frozenset({'c', 'c++', 'go', 'javascript', 'jsx', 'ts', 'tsx'})) if args.relicense: - for f in py_cmake_shell: + for f in hash_style: self._strip_leading_comment_block(f, '#') - for f in cpp: + for f in slash_style: self._strip_leading_comment_block(f, '//') wildcard_flag = ['--wildcard-copyright-org'] if args.wildcard_copyright_org else [] @@ -90,15 +92,15 @@ def run(self, args: argparse.Namespace) -> list[Result]: '--no-extra-eol', ] + wildcard_flag, - py_cmake_shell, + hash_style, name='copyright (py/cmake/shell)', ), self._check( 'polymath_copyright_header', ['--license-filepath', license_filepath, '--comment-style', '//', '--allow-past-years'] + wildcard_flag, - cpp, - name='copyright (cpp)', + slash_style, + name='copyright (c/cpp/go/js/ts)', ), ] finally: @@ -120,6 +122,7 @@ def _strip_leading_comment_block(filepath: str, comment_prefix: str) -> None: Strips all contiguous comment lines (matching comment_prefix) starting after any shebang or encoding declaration, plus one following blank line. Used so that a subsequent insert_license run can write a fresh header in their place. + A Go directive (`//go:` or `// +build`) ends the block. """ path = Path(filepath) lines = path.read_text(encoding='utf-8', errors='replace').splitlines(keepends=True) @@ -129,7 +132,11 @@ def _strip_leading_comment_block(filepath: str, comment_prefix: str) -> None: if idx < len(lines) and re.match(r'#\s*-\*-\s*coding', lines[idx]): idx += 1 block_start = idx - while idx < len(lines) and lines[idx].rstrip('\r\n').lstrip().startswith(comment_prefix): + while ( + idx < len(lines) + and lines[idx].rstrip('\r\n').lstrip().startswith(comment_prefix) + and not _GO_DIRECTIVE_PATTERN.match(lines[idx].lstrip()) + ): idx += 1 if idx < len(lines) and not lines[idx].strip(): idx += 1 diff --git a/tests/test_copyright.py b/tests/test_copyright.py index 480cd46..0886137 100644 --- a/tests/test_copyright.py +++ b/tests/test_copyright.py @@ -1,10 +1,13 @@ # SPDX-FileCopyrightText: 2026 Polymath Robotics, Inc. # SPDX-License-Identifier: Apache-2.0 -"""Tests for CopyrightGroup._check_license_file and _strip_leading_comment_block.""" +"""Tests for CopyrightGroup: LICENSE file management, leading comment stripping, and header insertion.""" import argparse from unittest.mock import patch +import pytest + +from polymath_code_standard.checker import Result from polymath_code_standard.checkers.copyright import CopyrightGroup _check = CopyrightGroup._check_license_file @@ -153,3 +156,72 @@ def test_does_not_strip_non_matching_comment_style(self, tmp_path): p = self._write(tmp_path, 'f.cpp', content) _strip(str(p), '#') # wrong prefix — should leave file untouched assert p.read_text() == content + + def test_preserves_go_build_constraint(self, tmp_path): + p = self._write(tmp_path, 'f.go', '// Copyright 2024\n\n//go:build linux\n\npackage main\n') + _strip(str(p), '//') + assert p.read_text() == '//go:build linux\n\npackage main\n' + + def test_leading_go_build_constraint_leaves_file_unchanged(self, tmp_path): + content = '//go:build linux\n\npackage main\n' + p = self._write(tmp_path, 'f.go', content) + _strip(str(p), '//') + assert p.read_text() == content + + def test_preserves_legacy_go_build_constraint(self, tmp_path): + p = self._write(tmp_path, 'f.go', '// Copyright 2024\n// +build linux\n\npackage main\n') + _strip(str(p), '//') + assert p.read_text() == '// +build linux\n\npackage main\n' + + +_SLASH_STYLE_SOURCES = [ + ('main.cpp', 'int x;\n'), + ('main.go', 'package main\n'), + ('index.js', 'export const x = 1;\n'), + ('App.jsx', 'export const App = () => null;\n'), + ('index.ts', 'export const x = 1;\n'), + ('App.tsx', 'export const App = () => null;\n'), +] + +_SLASH_STYLE_RESULT_NAME = 'copyright (c/cpp/go/js/ts)' + +_REUSE_HEADER = '// SPDX-FileCopyrightText: 2024 Test Corp\n// SPDX-License-Identifier: Apache-2.0\n\n' + + +def _run_copyright(files): + """Run the group over files, with LICENSE file management stubbed out.""" + args = argparse.Namespace( + license_id='Apache-2.0', + copyright_year='2024', + copyright_org='Test Corp', + wildcard_copyright_org=False, + reuse_style=True, + relicense=False, + files=[str(f) for f in files], + ) + with patch.object(CopyrightGroup, '_check_license_file', return_value=Result(name='LICENSE file', passed=True)): + results = CopyrightGroup().run(args) + return next(r for r in results if r.name == _SLASH_STYLE_RESULT_NAME) + + +@pytest.mark.parametrize(('name', 'body'), _SLASH_STYLE_SOURCES) +class TestSlashCommentStyleFiles: + def test_header_is_inserted(self, tmp_path, name, body): + src = tmp_path / name + src.write_text(body, encoding='utf-8') + result = _run_copyright([src]) + assert not result.passed + assert src.read_text() == _REUSE_HEADER + body + + def test_second_run_passes(self, tmp_path, name, body): + src = tmp_path / name + src.write_text(body, encoding='utf-8') + _run_copyright([src]) + assert _run_copyright([src]).passed + + def test_existing_correct_header_passes(self, tmp_path, name, body): + src = tmp_path / name + src.write_text(_REUSE_HEADER + body, encoding='utf-8') + result = _run_copyright([src]) + assert result.passed + assert src.read_text() == _REUSE_HEADER + body