Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .pre-commit-hooks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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, <SPDX_ID or 'proprietary'>, --copyright-org, <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
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:**

Expand Down
23 changes: 15 additions & 8 deletions polymath_code_standard/checkers/copyright/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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 []
Expand All @@ -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:
Expand All @@ -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)
Expand All @@ -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
Expand Down
74 changes: 73 additions & 1 deletion tests/test_copyright.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Loading