From 95d3df73fe975a54a4cd38a0aff02f1a6fb37732 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Wed, 15 Jul 2026 11:50:07 +0200 Subject: [PATCH 1/2] Feat: Add cr_checker --- .bazelrc | 41 ++ .bazelversion | 1 + .github/workflows/pre-commit.yml | 27 + .github/workflows/tests.yml | 39 ++ .gitignore | 11 + .pre-commit-config.yaml | 22 + .pre-commit-hooks.yaml | 20 + LICENSE | 177 +++++ LICENSES/Apache-2.0.txt | 73 ++ LICENSES/EPL-2.0.txt | 80 +++ MODULE.bazel | 22 + NOTICE | 38 ++ README.md | 62 +- REUSE.toml | 49 ++ cr_checker/BUILD | 0 cr_checker/LICENSE | 13 + cr_checker/README.md | 199 ++++++ cr_checker/cr_checker.bzl | 135 ++++ cr_checker/deps.bzl | 37 + cr_checker/resources/BUILD | 36 + cr_checker/resources/config.json | 3 + cr_checker/resources/exclusion.txt | 1 + cr_checker/resources/templates.ini | 91 +++ cr_checker/tests/.bazelrc | 6 + cr_checker/tests/.bazelversion | 1 + cr_checker/tests/BUILD | 24 + cr_checker/tests/MODULE.bazel | 48 ++ cr_checker/tests/requirements_lock.txt | 1 + cr_checker/tests/test_cr_checker.py | 461 +++++++++++++ cr_checker/tool/BUILD | 21 + cr_checker/tool/cr_checker.py | 904 +++++++++++++++++++++++++ cr_checker/tool/pre-commit_wrapper | 40 ++ 32 files changed, 2681 insertions(+), 2 deletions(-) create mode 100644 .bazelrc create mode 100644 .bazelversion create mode 100644 .github/workflows/pre-commit.yml create mode 100644 .github/workflows/tests.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 .pre-commit-hooks.yaml create mode 100644 LICENSE create mode 100644 LICENSES/Apache-2.0.txt create mode 100644 LICENSES/EPL-2.0.txt create mode 100644 MODULE.bazel create mode 100644 NOTICE create mode 100644 REUSE.toml create mode 100644 cr_checker/BUILD create mode 100644 cr_checker/LICENSE create mode 100644 cr_checker/README.md create mode 100644 cr_checker/cr_checker.bzl create mode 100644 cr_checker/deps.bzl create mode 100644 cr_checker/resources/BUILD create mode 100644 cr_checker/resources/config.json create mode 100644 cr_checker/resources/exclusion.txt create mode 100644 cr_checker/resources/templates.ini create mode 100644 cr_checker/tests/.bazelrc create mode 120000 cr_checker/tests/.bazelversion create mode 100644 cr_checker/tests/BUILD create mode 100644 cr_checker/tests/MODULE.bazel create mode 100644 cr_checker/tests/requirements_lock.txt create mode 100644 cr_checker/tests/test_cr_checker.py create mode 100644 cr_checker/tool/BUILD create mode 100755 cr_checker/tool/cr_checker.py create mode 100755 cr_checker/tool/pre-commit_wrapper diff --git a/.bazelrc b/.bazelrc new file mode 100644 index 0000000..2d44ee2 --- /dev/null +++ b/.bazelrc @@ -0,0 +1,41 @@ +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# SPDX-License-Identifier: Apache-2.0 + +common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ +common --registry=https://bcr.bazel.build + +#common --extra_toolchains=@score_gcc_x86_64_toolchain//:x86_64-linux + +# libclang toolchain for the C/C++ parser used by the score rules. Registered +# only for score_tooling's own build (its LLVM is a dev dependency); integrating +# repositories register their own libclang toolchain instead. +#common --extra_toolchains=//cpp/libclang:score_tooling_libclang_toolchain + +build --java_language_version=17 +build --tool_java_language_version=17 +build --java_runtime_version=remotejdk_17 +build --tool_java_runtime_version=remotejdk_17 + +# Use GNU ld (bfd) as the Rust linker; avoids the deprecation warning that +# newer Rust emits when it detects the system default linker is gold. +#build --@rules_rust//rust/settings:extra_rustc_flag=-Clink-arg=-fuse-ld=bfd + +# Rust clippy linter +#build:clippy --aspects=@rules_rust//rust:defs.bzl%rust_clippy_aspect +#build:clippy --output_groups=+clippy_checks + +# Log level configuration for rules_score build output +# normal build: only errors and warnings (default – no flag needed) +# info build: additionally show info messages from all tools +build:info --//bazel/rules/rules_score:verbosity=info +# debug build: complete output including debug/trace from all tools +build:debug --//bazel/rules/rules_score:verbosity=debug + +# Standard combined coverage (Rust + Python, no Ferrocene required) +# Usage: bazel coverage --config=coverage +# Then run: bazel run //coverage:combined_report +coverage:coverage --combined_report=lcov +#coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Clink-dead-code +#coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Ccodegen-units=1 + diff --git a/.bazelversion b/.bazelversion new file mode 100644 index 0000000..acd405b --- /dev/null +++ b/.bazelversion @@ -0,0 +1 @@ +8.6.0 diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..1b398af --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,27 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +name: pre-commit +on: + pull_request: + types: [opened, reopened, synchronize] +jobs: + self_test: + name: 🔬 Self Test + runs-on: ubuntu-latest + steps: + - name: 📥 Check out + uses: actions/checkout@v7.0.0 + - name: ⚙️ Setup uv + uses: astral-sh/setup-uv@v7 + - name: 🛠️ Run pre-commit + run: uvx pre-commit run --all-files diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..44232e6 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,39 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + integration_tests: + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: write + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.0 + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + - uses: castler/setup-bazel@cache-optimized + with: + bazelisk-cache: true + disk-cache: integration_tests + repository-cache: true + cache-optimized: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + # - name: Run python_basics integration tests + # run: | + # cd python_basics/integration_tests + # bazel test //... + - name: Run cr_checker unit tests + run: | + cd cr_checker/tests + bazel test //... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9c1191b --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# SPDX-License-Identifier: Apache-2.0 + +bazel-* +MODULE.bazel.lock +external +.vscode/ + +__pycache__ +.ruff_cache/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..d2a5d82 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,22 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + exclude: requirements_lock.txt|\.svg$ + - id: trailing-whitespace + - id: check-shebang-scripts-are-executable + - id: check-executables-have-shebangs + - id: check-added-large-files + args: [--maxkb=100, --enforce-all] # increase or add git lfs if too strict + exclude: org.eclipse.dash.licenses-1.1.0.jar|blanket_index.html + - repo: https://github.com/google/yamlfmt + rev: 21ca5323a9c87ee37a434e0ca908efc0a89daa07 # v0.21.0 + hooks: + - id: yamlfmt + - repo: https://codeberg.org/fsfe/reuse-tool + rev: a1bb792acda6fd0724936b4ebbdbc8eceb9c0459 # v6.2.0 + hooks: + - id: reuse-lint-file + exclude: \.(svg|tpl|txt|conf|lock|json|in|png)$ diff --git a/.pre-commit-hooks.yaml b/.pre-commit-hooks.yaml new file mode 100644 index 0000000..3607a34 --- /dev/null +++ b/.pre-commit-hooks.yaml @@ -0,0 +1,20 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +- id: copyright + name: Check and fix copyright headers with cr_checker + entry: cr_checker/tool/pre-commit_wrapper --extensions h hpp c cpp rs rst py sh bzl ini yml yaml trlc rsl puml svg BUILD bazel --fix + # this would be the better language implementation, but struggle with python tooling + # language: python + language: script + minimum_pre_commit_version: 3.2.0 + types_or: [python, yaml, ini, rst, sh, shell, bash, bazel, c, c++, rust, plantuml, svg] diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..f433b1a --- /dev/null +++ b/LICENSE @@ -0,0 +1,177 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt new file mode 100644 index 0000000..137069b --- /dev/null +++ b/LICENSES/Apache-2.0.txt @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright [yyyy] [name of copyright owner] + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/LICENSES/EPL-2.0.txt b/LICENSES/EPL-2.0.txt new file mode 100644 index 0000000..78756b5 --- /dev/null +++ b/LICENSES/EPL-2.0.txt @@ -0,0 +1,80 @@ +Eclipse Public License - v 2.0 +THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC LICENSE (“AGREEMENT”). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. + +1. DEFINITIONS +“Contribution” means: + +a) in the case of the initial Contributor, the initial content Distributed under this Agreement, and +b) in the case of each subsequent Contributor: +i) changes to the Program, and +ii) additions to the Program; +where such changes and/or additions to the Program originate from and are Distributed by that particular Contributor. A Contribution “originates” from a Contributor if it was added to the Program by such Contributor itself or anyone acting on such Contributor's behalf. Contributions do not include changes or additions to the Program that are not Modified Works. +“Contributor” means any person or entity that Distributes the Program. + +“Licensed Patents” mean patent claims licensable by a Contributor which are necessarily infringed by the use or sale of its Contribution alone or when combined with the Program. + +“Program” means the Contributions Distributed in accordance with this Agreement. + +“Recipient” means anyone who receives the Program under this Agreement or any Secondary License (as applicable), including Contributors. + +“Derivative Works” shall mean any work, whether in Source Code or other form, that is based on (or derived from) the Program and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. + +“Modified Works” shall mean any work in Source Code or other form that results from an addition to, deletion from, or modification of the contents of the Program, including, for purposes of clarity any new file in Source Code form that contains any contents of the Program. Modified Works shall not include works that contain only declarations, interfaces, types, classes, structures, or files of the Program solely in each case in order to link to, bind by name, or subclass the Program or Modified Works thereof. + +“Distribute” means the acts of a) distributing or b) making available in any manner that enables the transfer of a copy. + +“Source Code” means the form of a Program preferred for making modifications, including but not limited to software source code, documentation source, and configuration files. + +“Secondary License” means either the GNU General Public License, Version 2.0, or any later versions of that license, including any exceptions or additional permissions as identified by the initial Contributor. + +2. GRANT OF RIGHTS +a) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, Distribute and sublicense the Contribution of such Contributor, if any, and such Derivative Works. +b) Subject to the terms of this Agreement, each Contributor hereby grants Recipient a non-exclusive, worldwide, royalty-free patent license under Licensed Patents to make, use, sell, offer to sell, import and otherwise transfer the Contribution of such Contributor, if any, in Source Code or other form. This patent license shall apply to the combination of the Contribution and the Program if, at the time the Contribution is added by the Contributor, such addition of the Contribution causes such combination to be covered by the Licensed Patents. The patent license shall not apply to any other combinations which include the Contribution. No hardware per se is licensed hereunder. +c) Recipient understands that although each Contributor grants the licenses to its Contributions set forth herein, no assurances are provided by any Contributor that the Program does not infringe the patent or other intellectual property rights of any other entity. Each Contributor disclaims any liability to Recipient for claims brought by any other entity based on infringement of intellectual property rights or otherwise. As a condition to exercising the rights and licenses granted hereunder, each Recipient hereby assumes sole responsibility to secure any other intellectual property rights needed, if any. For example, if a third party patent license is required to allow Recipient to Distribute the Program, it is Recipient's responsibility to acquire that license before distributing the Program. +d) Each Contributor represents that to its knowledge it has sufficient copyright rights in its Contribution, if any, to grant the copyright license set forth in this Agreement. +e) Notwithstanding the terms of any Secondary License, no Contributor makes additional grants to any Recipient (other than those set forth in this Agreement) as a result of such Recipient's receipt of the Program under the terms of a Secondary License (if permitted under the terms of Section 3). +3. REQUIREMENTS +3.1 If a Contributor Distributes the Program in any form, then: + +a) the Program must also be made available as Source Code, in accordance with section 3.2, and the Contributor must accompany the Program with a statement that the Source Code for the Program is available under this Agreement, and informs Recipients how to obtain it in a reasonable manner on or through a medium customarily used for software exchange; and +b) the Contributor may Distribute the Program under a license different than this Agreement, provided that such license: +i) effectively disclaims on behalf of all other Contributors all warranties and conditions, express and implied, including warranties or conditions of title and non-infringement, and implied warranties or conditions of merchantability and fitness for a particular purpose; +ii) effectively excludes on behalf of all other Contributors all liability for damages, including direct, indirect, special, incidental and consequential damages, such as lost profits; +iii) does not attempt to limit or alter the recipients' rights in the Source Code under section 3.2; and +iv) requires any subsequent distribution of the Program by any party to be under a license that satisfies the requirements of this section 3. +3.2 When the Program is Distributed as Source Code: + +a) it must be made available under this Agreement, or if the Program (i) is combined with other material in a separate file or files made available under a Secondary License, and (ii) the initial Contributor attached to the Source Code the notice described in Exhibit A of this Agreement, then the Program may be made available under the terms of such Secondary Licenses, and +b) a copy of this Agreement must be included with each copy of the Program. +3.3 Contributors may not remove or alter any copyright, patent, trademark, attribution notices, disclaimers of warranty, or limitations of liability (‘notices’) contained within the Program from any copy of the Program which they Distribute, provided that Contributors may add their own appropriate notices. + +4. COMMERCIAL DISTRIBUTION +Commercial distributors of software may accept certain responsibilities with respect to end users, business partners and the like. While this license is intended to facilitate the commercial use of the Program, the Contributor who includes the Program in a commercial product offering should do so in a manner which does not create potential liability for other Contributors. Therefore, if a Contributor includes the Program in a commercial product offering, such Contributor (“Commercial Contributor”) hereby agrees to defend and indemnify every other Contributor (“Indemnified Contributor”) against any losses, damages and costs (collectively “Losses”) arising from claims, lawsuits and other legal actions brought by a third party against the Indemnified Contributor to the extent caused by the acts or omissions of such Commercial Contributor in connection with its distribution of the Program in a commercial product offering. The obligations in this section do not apply to any claims or Losses relating to any actual or alleged intellectual property infringement. In order to qualify, an Indemnified Contributor must: a) promptly notify the Commercial Contributor in writing of such claim, and b) allow the Commercial Contributor to control, and cooperate with the Commercial Contributor in, the defense and any related settlement negotiations. The Indemnified Contributor may participate in any such claim at its own expense. + +For example, a Contributor might include the Program in a commercial product offering, Product X. That Contributor is then a Commercial Contributor. If that Commercial Contributor then makes performance claims, or offers warranties related to Product X, those performance claims and warranties are such Commercial Contributor's responsibility alone. Under this section, the Commercial Contributor would have to defend claims against the other Contributors related to those performance claims and warranties, and if a court requires any other Contributor to pay any damages as a result, the Commercial Contributor must pay those damages. + +5. NO WARRANTY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN “AS IS” BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the appropriateness of using and distributing the Program and assumes all risks associated with its exercise of rights under this Agreement, including but not limited to the risks and costs of program errors, compliance with applicable laws, damage to or loss of data, programs or equipment, and unavailability or interruption of operations. + +6. DISCLAIMER OF LIABILITY +EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. + +7. GENERAL +If any provision of this Agreement is invalid or unenforceable under applicable law, it shall not affect the validity or enforceability of the remainder of the terms of this Agreement, and without further action by the parties hereto, such provision shall be reformed to the minimum extent necessary to make such provision valid and enforceable. + +If Recipient institutes patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Program itself (excluding combinations of the Program with other software or hardware) infringes such Recipient's patent(s), then such Recipient's rights granted under Section 2(b) shall terminate as of the date such litigation is filed. + +All Recipient's rights under this Agreement shall terminate if it fails to comply with any of the material terms or conditions of this Agreement and does not cure such failure in a reasonable period of time after becoming aware of such noncompliance. If all Recipient's rights under this Agreement terminate, Recipient agrees to cease use and distribution of the Program as soon as reasonably practicable. However, Recipient's obligations under this Agreement and any licenses granted by Recipient relating to the Program shall continue and survive. + +Everyone is permitted to copy and distribute copies of this Agreement, but in order to avoid inconsistency the Agreement is copyrighted and may only be modified in the following manner. The Agreement Steward reserves the right to publish new versions (including revisions) of this Agreement from time to time. No one other than the Agreement Steward has the right to modify this Agreement. The Eclipse Foundation is the initial Agreement Steward. The Eclipse Foundation may assign the responsibility to serve as the Agreement Steward to a suitable separate entity. Each new version of the Agreement will be given a distinguishing version number. The Program (including Contributions) may always be Distributed subject to the version of the Agreement under which it was received. In addition, after a new version of the Agreement is published, Contributor may elect to Distribute the Program (including its Contributions) under the new version. + +Except as expressly stated in Sections 2(a) and 2(b) above, Recipient receives no rights or licenses to the intellectual property of any Contributor under this Agreement, whether expressly, by implication, estoppel or otherwise. All rights in the Program not expressly granted under this Agreement are reserved. Nothing in this Agreement is intended to be enforceable by any entity that is not a Contributor or Recipient. No third-party beneficiary rights are created under this Agreement. + +Exhibit A – Form of Secondary Licenses Notice +“This Source Code may also be made available under the following Secondary Licenses when the conditions for such availability set forth in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), version(s), and exceptions or additional permissions here}.” + +Simply including a copy of this Agreement, including this Exhibit A is not sufficient to license the Source Code under Secondary Licenses. + +If it is not possible or desirable to put the notice in a particular file, then You may include the notice in a location (such as a LICENSE file in a relevant directory) where a recipient would be likely to look for such a notice. + +You may add additional accurate notices of copyright ownership. diff --git a/MODULE.bazel b/MODULE.bazel new file mode 100644 index 0000000..5c512d5 --- /dev/null +++ b/MODULE.bazel @@ -0,0 +1,22 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +module( + name = "score_utils" +) +# CR_CHECKER +bazel_dep(name = "aspect_rules_py", version = "1.4.0") +bazel_dep(name = "score_tooling", version = "1.2.0") +# bazel_dep(name = "rules_python", version = "1.4.1") +# bazel_dep(name = "rules_rust", version = "0.68.1-score") +# bazel_dep(name = "rules_shell", version = "0.5.0") diff --git a/NOTICE b/NOTICE new file mode 100644 index 0000000..9c933ea --- /dev/null +++ b/NOTICE @@ -0,0 +1,38 @@ + + +# Notices for Eclipse Safe Open Vehicle Core + +This content is produced and maintained by the Eclipse Safe Open Vehicle Core project. + + * Project home: https://projects.eclipse.org/projects/automotive.score + +## Trademarks + +Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. + +## Copyright + +All content is the property of the respective authors or their employers. +For more information regarding authorship of content, please consult the +listed source code repository logs. + +## Declared Project Licenses + +This program and the accompanying materials are made available under the terms +of the Apache License Version 2.0 which is available at +https://www.apache.org/licenses/LICENSE-2.0. + +SPDX-License-Identifier: Apache-2.0 + +## Cryptography + +Content may contain encryption software. The country in which you are currently +may have restrictions on the import, possession, and use, and/or re-export to +another country, of encryption software. BEFORE using any encryption software, +please check the country's laws, regulations and policies concerning the import, +possession, or use, and re-export of encryption software, to see if this is +permitted. diff --git a/README.md b/README.md index fbff2dd..5a59127 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,60 @@ -# tools -Home of score-tools, the new pypi based tools approach + + +# Tools + +## Why this repository exists + +The utilities/tools in this repository used to live inside [tooling](https://github.com/eclipse-score/tooling). That +repo works well for its main purpose, but it also pulls in a large and +growing dependency graph because it bundles many unrelated concerns together. + +That created a real cost for consumers: if you only needed one small, +self-contained utility, building or depending on it meant pulling in the +entire dependency graph of `tooling`, even though 99% of it was +irrelevant to what you actually wanted. + +This repository exists to fix that. It pulls a set of small, self-contained +utilities out of `tooling` into their own home, so that depending on +a utility costs you only what that utility actually needs — not the +dependency footprint of an unrelated, much larger codebase. + +## Scope + +This repo is intentionally narrow. To stay useful, it needs to *stay* +narrow — the whole point of splitting it out was to avoid recreating the +same problem in a new location. + +**In scope:** +- Small, self-contained, general-purpose utilities with minimal external + dependencies. +- Code with no coupling to `tooling`. +- Utilities that are (or are likely to be) reused across multiple, + otherwise-unrelated projects. + +**Out of scope:** +- Anything tied to a specific product, service, or domain — that belongs in + the repo that owns that domain. +- Utilities that only make sense in the context of `tooling`. +- Anything that would pull in a large or heavyweight dependency for the + benefit of a single utility. If a proposed addition needs a big + dependency, prefer keeping it in whichever repo already depends on that + library, or give it its own repo instead of adding it here. +- Grab-bag/"misc" code with no clear justification for living here. When in + doubt, ask whether this utility would want to be depended on by something + that doesn't want any of the other dependencies this repo would then + bring in. If the answer isn't clearly yes, it doesn't belong here. + +## Adding something new + +Before adding a new utility here, check: +1. Is it genuinely general-purpose, not tied to one product's domain logic? +2. Does it avoid pulling in dependencies that most other things in this + repo don't already need? +3. Would splitting it into its own small repo/target actually serve + consumers better than adding it here? + +If any of these gives you pause, raise it for discussion before merging. diff --git a/REUSE.toml b/REUSE.toml new file mode 100644 index 0000000..6c94d54 --- /dev/null +++ b/REUSE.toml @@ -0,0 +1,49 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +version = 1 + +[[annotations]] +path = [".devcontainer/*.json"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = [".vscode/*.json"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ["**/*.lock.json"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + +[[annotations]] +path = ["**/requirements_lock.txt"] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" + + +[[annotations]] +path = ["cr_checker/resources/config.json", + "**/.bazelversion", + "coverage/tests/fixtures/symbol_report.json", + "python_basics/.bazelversion", + "python_basics/integration_tests/venv-with-extra-requirements/requirements.in", + "python_basics/requirements.in", + "python_basics/requirements.txt", + "bazel/rules/rules_score/test/requirements.in", + "bazel/rules/rules_score/test/requirements.txt" + ] +SPDX-FileCopyrightText = "Copyright (c) 2026 Contributors to the Eclipse Foundation" +SPDX-License-Identifier = "Apache-2.0" diff --git a/cr_checker/BUILD b/cr_checker/BUILD new file mode 100644 index 0000000..e69de29 diff --git a/cr_checker/LICENSE b/cr_checker/LICENSE new file mode 100644 index 0000000..8c69a8b --- /dev/null +++ b/cr_checker/LICENSE @@ -0,0 +1,13 @@ +Copyright 2025 Contributors to the Eclipse Foundation + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/cr_checker/README.md b/cr_checker/README.md new file mode 100644 index 0000000..e9d9833 --- /dev/null +++ b/cr_checker/README.md @@ -0,0 +1,199 @@ + + +# CopyRight Checker + +`cr_checker.py` is a tool designed to check if files contain a specified copyright header. It provides configurable logging, color-coded console output, and can handle large file sets efficiently. The script supports reading configuration files for custom copyright templates and can utilize memory-mapped file reading for better performance with large files. Tool itself can also append copyright header at the beginning of file if flag `--fix` is used. + +## Features + +- Checks files for specified copyright headers based on file extensions. +- Configurable logging, including color-coded output for easy visibility of log levels. +- Supports parameter files for flexible input handling. +- Can use memory mapping for large file handling. +- Customizable file encoding and offset adjustments for header text positioning. +- Can append copyright headers. +- Can remove provided number of characters from beginning of the file. + +## Requirements + +- Python 3.6+ +- `argparse`, `logging`, `os`, `sys`, `mmap`, `tempfile`, and `pathlib` (standard library modules) + +## Installation + +To use `cr_checker.py`, simply clone this repository. + +## Usage + +The script can be run from the command line with various options to customize its behavior: + +```bash +python cr_checker.py -t [options] +``` + +### Arguments + +- **-t**, **--template-file**: (Required) Path to the template file that defines the copyright text for each file extension. +- **-v**, **--verbose**: Enable debug-level logging. +- **-l**, **--log-file**: Path to a log file where logs will be saved. If not provided, logs will print to the console. +- **-e**, **--extensions**: List of file extensions to filter, e.g., -e .py .cpp. +- **--use_memory_map**: Use memory-mapped file reading for large files. +- **--encoding**: File encoding (default is utf-8). +- **--offset**: Additional offset for the header length to account for lines like a shebang. +- **--fix**: Setting script into fix mode where copyright header will be added to the files if it's missing from same. +- **--remove-offset**: Number of characters to remove before appending proper copyright header (works only with `--fix` option). +- **inputs**: (Required) Directories or files to parse, or a parameter file prefixed with @ that lists files or directories. + +> NOTE: Option `--remove-offset` can have severe consequences if the offset is miscalculated. Use with **extreme caution**. + +> NOTE: Setting directory as `.` will cause that tool removes your complete workspace! This is connected with how Bazel includes python into build. **DO NOT USE THIS OPTION UNLESS YOU'RE 100% SURE IN WHAT YOU'RE DOING**. + +### Examples + +```sh +python cr_checker.py -t templates.ini -e py cpp -v -l logs.txt my_random_file.cpp my_random_file.py + +python cr_checker.py -t templates.ini -e py cpp --offset 24 --use_memory_map @files_to_check.txt + +python cr_checker.py -t templates.ini -e py cpp --fix --offset 24 --use_memory_map @files_to_check.txt + +``` + +#### A bit more about `--offset` + +This mode is special that will enable tool to do advance search & replace copyright headers. For example, assuming that we have following python implementation: + +```python +#!/usr/bin/env python3 + +import os +``` + +and we use following command: + +```sh +python cr_checker.py -t templates.ini -e py cpp --fix --use_memory_map @files_to_check.txt +``` + +The result will be following: + +```python +################## +# COPYRIGHT HEADER +################## +#!/usr/bin/env python3 + +import os +``` + +This is of course not what we want, and for that we use `--offset=24` where 24 is number of char + 1 (for new line char). +if we apply this arguments now on same file, the outcome is different. + +```python +#!/usr/bin/env python3 + +################## +# COPYRIGHT HEADER +################## + +import os +``` + +On another hand, `--offset` has also another role. Assuming that a header text in file is soo big that copyright header is in the middle of the file, with regular command, the tool will not detect copyright headed. With `--offset=` we can tell the tool from where to start considering the search for +copyright header. + +### Template File Format + +The template file should be in INI format, with each section representing a file extension and a section specifying the copyright text. +The copyright text can use format expressions to match the year and the author. + +Example templates.ini: + +```ini +[py,sh] +# Copyright (c) {year} {author} + +[cpp,c,hpp, h] +// Copyright (c) {year} {author} +``` + +## Exit Codes + +- 0: All files contain the required copyright text. +- 1: Some files are missing the required copyright text. +- Other: Error encountered during file processing. + +### Logging and Color-Coded Output + +By default, logs are printed to the console in color-coded format to indicate log levels. You can redirect logs to a file using the -l option. + +#### Log Colors + +- DEBUG: Blue +- INFO: Green +- WARNING: Yellow +- ERROR: Red + +## Bazel integration + +### Copyright Checker Bazel Macro + +To integrate copyright verification into your Bazel-based project, you can use the `copyright_checker` macro. This macro allows you to check source files for compliance with a specified copyright template and configuration. Additionally, it can automatically apply fixes when necessary. + +#### Usage + +```python +load("@score_cr_checker//cr_checker:def.bzl", "copyright_checker") +copyright_checker( + name = "copyright_check", + srcs = glob(["src/**/*.cpp", "src/**/*.h"]), + config = "@score_cr_checker//tools/cr_checker/resources:config", + template = "@score_cr_checker//tools/cr_checker/resources:templates", + visibility = ["//visibility:public"], +) +``` + +#### Parameters + +- **name**: Unique identifier for the rule. +- **srcs**: List of source files to check. +- **visibility**: Defines which targets can access this rule. +- **template**: Path to the copyright header template. +- **config**: Path to the project-specific configuration. +- **extensions** (optional): List of file extensions to filter files. Defaults to all files. +- **offset** (optional): Line offset for checking/modifying files. +- **remove_offset** (optional): Number of characters to remove from the beginning of the file. +- **debug** (optional): Enables verbose logging for debugging. +- **use_memory_map** (optional): Uses memory-mapped files for performance optimization. +- **fix** (optional): Automatically applies fixes instead of just reporting issues. + +### Integrate `cr_checker` using Bazel module + +The CopyRight check tool is integrated in other bazel repository using Bazel modules mechanism. The current tool is not registered within BCR so private Bazel registry needs to be select. To select custom Bazel registry add following lines into .bazelrc: + +```python +common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ +common --registry=https://bcr.bazel.build +``` + +This will allow Bazel to look into project Bazel registry. After that all what is needed is to add following lines in MODULE.bazel: + +```python +############################################################################### +# +# CopyRight checker dependencies +# +############################################################################### +bazel_dep(name = "score_cr_checker", version = "0.2.2") +``` diff --git a/cr_checker/cr_checker.bzl b/cr_checker/cr_checker.bzl new file mode 100644 index 0000000..c567554 --- /dev/null +++ b/cr_checker/cr_checker.bzl @@ -0,0 +1,135 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +"""Defines Bazel rules for running copyright checks and fixes.""" + +load("@aspect_rules_py//py:defs.bzl", "py_binary") + +def copyright_checker( + name, + srcs, + visibility, + template, + config, + exclusion = None, + extensions = [], + offset = 0, + remove_offset = 0, + debug = False, + use_memory_map = False, + fix = False, + target_compatible_with = None): + """ + Defines a custom build rule for checking and optionally fixing files for compliance + with specific requirements, such as copyright headers. + + Args: + name (str): The name of the rule, used as an identifier in the build system. + srcs (list): A list of source file paths to check. + visibility (list): A list defining the visibility of the rule, specifying which + targets can use this rule. + template (str, optional): Path to the template resource used for validation. + Defaults to "//tools/cr_checker/resources:templates". + config (str, optional): Path to the config resource used for project variables. + Defaults to "//tools/cr_checker/resources:config". + exclusion (str, optional): Path to a text file listing files to be excluded from the copyright check. + File format: one path per line, relative to the repository root. + extensions (list, optional): A list of file extensions to filter the source files. + Defaults to an empty list, meaning all files are checked. + offset (int, optional): The line offset for applying checks or modifications. + Defaults to 0. + remove_offset (int, optional): The line offset for removing chars from beginning of file. + Defaults to 0. + debug (bool, optional): Whether to enable debug mode, providing additional logs. + Defaults to False. + use_memory_map (bool, optional): Whether to use memory mapping for large files to + improve performance. Defaults to False. + fix (bool, optional): Whether to apply fixes to files instead of just reporting issues. + Defaults to False. + + Returns: + None: This function defines a rule for a build system and does not return a value. + """ + t_names = [ + "{}.check".format(name), + "{}.fix".format(name), + ] + + args = [ + "-t $(location {})".format(template), + "-c $(location {})".format(config), + ] + if len(extensions): + args.append("-e {exts}".format( + exts = " ".join([exts for exts in extensions]), + )) + + if exclusion: + args.append("--exclusion-file $(location {})".format(exclusion)) + + if offset: + args.append("--offset {}".format(offset)) + + if debug: + args.append("-v") + + if use_memory_map: + args.append("--use_memory_map") + + for src in srcs: + args.append("$(locations {})".format(src)) + + for t_name in t_names: + if t_name == "{}.fix".format(name): + args.insert(0, "--fix") + if remove_offset: + args.append("--remove_offset {}".format(remove_offset)) + + data = srcs[:] + data.append(template) + data.append(config) + if exclusion: + data.append(exclusion) + + py_binary( + name = t_name, + main = "cr_checker.py", + srcs = [ + "@score_tooling//cr_checker/tool:cr_checker_lib", + ], + args = args, + data = data, + visibility = visibility, + target_compatible_with = target_compatible_with, + ) + + native.alias( + name = "copyright-check", + actual = ":" + name + ".check", + visibility = visibility, + target_compatible_with = target_compatible_with, + tags = [ + "cli_help=Check for license headers:\n" + + "bazel run //:copyright-check", + ], + ) + + native.alias( + name = "copyright-fix", + actual = ":" + name + ".fix", + visibility = visibility, + tags = [ + "cli_help=Fix license headers:\n" + + "bazel run //:copyright-fix", + ], + ) diff --git a/cr_checker/deps.bzl b/cr_checker/deps.bzl new file mode 100644 index 0000000..bf84784 --- /dev/null +++ b/cr_checker/deps.bzl @@ -0,0 +1,37 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") + +def score_cr_checker_deps(): + if not native.existing_rule("rules_cc"): + http_archive( + name = "rules_cc", + urls = ["https://github.com/bazelbuild/rules_cc/releases/download/0.1.1/rules_cc-0.1.1.tar.gz"], + sha256 = "712d77868b3152dd618c4d64faaddefcc5965f90f5de6e6dd1d5ddcd0be82d42", + strip_prefix = "rules_cc-0.1.1", + ) + if not native.existing_rule("aspect_rules_py"): + http_archive( + name = "rules_python", + sha256 = "9f9f3b300a9264e4c77999312ce663be5dee9a56e361a1f6fe7ec60e1beef9a3", + strip_prefix = "rules_python-1.4.1", + url = "https://github.com/bazel-contrib/rules_python/releases/download/1.4.1/rules_python-1.4.1.tar.gz", + ) + if not native.existing_rule("aspect_rules_py"): + http_archive( + name = "aspect_rules_py", + sha256 = "8e9a1f00e4ba5696f9e93a770a6c1de863544cce489df91809fc3a4027ccfddc", + strip_prefix = "rules_py-1.4.0", + url = "https://github.com/aspect-build/rules_py/releases/download/v1.4.0/rules_py-v1.4.0.tar.gz", + ) diff --git a/cr_checker/resources/BUILD b/cr_checker/resources/BUILD new file mode 100644 index 0000000..307734e --- /dev/null +++ b/cr_checker/resources/BUILD @@ -0,0 +1,36 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +filegroup( + name = "templates", + srcs = [ + "templates.ini", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "config", + srcs = [ + "config.json", + ], + visibility = ["//visibility:public"], +) + +filegroup( + name = "exclusion", + srcs = [ + "exclusion.txt", + ], + visibility = ["//visibility:public"], +) diff --git a/cr_checker/resources/config.json b/cr_checker/resources/config.json new file mode 100644 index 0000000..285c132 --- /dev/null +++ b/cr_checker/resources/config.json @@ -0,0 +1,3 @@ +{ + "author": "Contributors to the Eclipse Foundation" +} diff --git a/cr_checker/resources/exclusion.txt b/cr_checker/resources/exclusion.txt new file mode 100644 index 0000000..73d6ffc --- /dev/null +++ b/cr_checker/resources/exclusion.txt @@ -0,0 +1 @@ +cr_checker/resources/templates.ini diff --git a/cr_checker/resources/templates.ini b/cr_checker/resources/templates.ini new file mode 100644 index 0000000..d9f1220 --- /dev/null +++ b/cr_checker/resources/templates.ini @@ -0,0 +1,91 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +[cpp,c,h,hpp,trlc,rsl] +/******************************************************************************** + * Copyright (c) {year} {author} + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License Version 2.0 which is available at + * https://www.apache.org/licenses/LICENSE-2.0 + * + * SPDX-License-Identifier: Apache-2.0 + ********************************************************************************/ +[py,sh,bzl,ini,yml,yaml,BUILD,bazel] +# ******************************************************************************* +# Copyright (c) {year} {author} +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +[rst] +.. + # ******************************************************************************* + # Copyright (c) {year} {author} + # + # See the NOTICE file(s) distributed with this work for additional + # information regarding copyright ownership. + # + # This program and the accompanying materials are made available under the + # terms of the Apache License Version 2.0 which is available at + # https://www.apache.org/licenses/LICENSE-2.0 + # + # SPDX-License-Identifier: Apache-2.0 + # ******************************************************************************* +[rs] +// ******************************************************************************* +// Copyright (c) {year} {author} +// +// See the NOTICE file(s) distributed with this work for additional +// information regarding copyright ownership. +// +// This program and the accompanying materials are made available under the +// terms of the Apache License Version 2.0 which is available at +// +// +// SPDX-License-Identifier: Apache-2.0 +// ******************************************************************************* +[puml] +' ******************************************************************************* +' Copyright (c) {year} Contributors to the Eclipse Foundation +' +' See the NOTICE file(s) distributed with this work for additional +' information regarding copyright ownership. +' +' This program and the accompanying materials are made available under the +' terms of the Apache License Version 2.0 which is available at +' https://www.apache.org/licenses/LICENSE-2.0 +' +' SPDX-License-Identifier: Apache-2.0 +' ******************************************************************************* +[md] + diff --git a/cr_checker/tests/.bazelrc b/cr_checker/tests/.bazelrc new file mode 100644 index 0000000..b425658 --- /dev/null +++ b/cr_checker/tests/.bazelrc @@ -0,0 +1,6 @@ +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# SPDX-License-Identifier: Apache-2.0 + +common --registry=https://raw.githubusercontent.com/eclipse-score/bazel_registry/main/ +common --registry=https://bcr.bazel.build diff --git a/cr_checker/tests/.bazelversion b/cr_checker/tests/.bazelversion new file mode 120000 index 0000000..96cf949 --- /dev/null +++ b/cr_checker/tests/.bazelversion @@ -0,0 +1 @@ +../../.bazelversion \ No newline at end of file diff --git a/cr_checker/tests/BUILD b/cr_checker/tests/BUILD new file mode 100644 index 0000000..3bc90fc --- /dev/null +++ b/cr_checker/tests/BUILD @@ -0,0 +1,24 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +load("@score_tooling//python_basics:defs.bzl", "score_py_pytest") + +score_py_pytest( + name = "unit_tests", + srcs = [ + "test_cr_checker.py", + ], + deps = [ + "@score_tooling//cr_checker/tool:cr_checker_lib", + ], +) diff --git a/cr_checker/tests/MODULE.bazel b/cr_checker/tests/MODULE.bazel new file mode 100644 index 0000000..57b285e --- /dev/null +++ b/cr_checker/tests/MODULE.bazel @@ -0,0 +1,48 @@ +# ******************************************************************************* +# Copyright (c) 2025 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +module( + name = "score_cr_checker_tests", + version = "0.1.0", +) + +# PYTHON +bazel_dep(name = "rules_python", version = "1.4.1") + +PYTHON_VERSION = "3.12" + +python = use_extension("@rules_python//python/extensions:python.bzl", "python") +python.toolchain( + python_version = PYTHON_VERSION, +) +use_repo(python) + +# PIP +pip = use_extension("@rules_python//python/extensions:pip.bzl", "pip") +pip.parse( + hub_name = "pip_deps_test", + python_version = PYTHON_VERSION, + requirements_lock = "//:requirements_lock.txt", +) +use_repo(pip, "pip_deps_test") + +bazel_dep(name = "score_utils", version = "0.0.0") +local_path_override( + module_name = "score_utils", + path = "../../", +) +bazel_dep(name = "score_tooling", version = "1.2.0") + +bazel_dep(name = "rules_rust", version = "0.68.1-score") +bazel_dep(name = "rules_shell", version = "0.5.0") + +# end Tests diff --git a/cr_checker/tests/requirements_lock.txt b/cr_checker/tests/requirements_lock.txt new file mode 100644 index 0000000..e504a61 --- /dev/null +++ b/cr_checker/tests/requirements_lock.txt @@ -0,0 +1 @@ +bazel-runfiles==1.3.0 \ No newline at end of file diff --git a/cr_checker/tests/test_cr_checker.py b/cr_checker/tests/test_cr_checker.py new file mode 100644 index 0000000..48bc842 --- /dev/null +++ b/cr_checker/tests/test_cr_checker.py @@ -0,0 +1,461 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* + +# unit tests for the shebang handling in the cr_checker module +from __future__ import annotations + +import importlib.util +import json +import pytest +from datetime import datetime +from pathlib import Path + + +# load the cr_checker module +def load_cr_checker_module(): + module_path = Path(__file__).resolve().parents[1] / "tool" / "cr_checker.py" + spec = importlib.util.spec_from_file_location("cr_checker_module", module_path) + if spec is None or spec.loader is None: + raise RuntimeError(f"Failed to load cr_checker module from {module_path}") + + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +# load the license template +def load_template(extension: str) -> str: + cr_checker = load_cr_checker_module() + template_file = Path(__file__).resolve().parents[1] / "resources" / "templates.ini" + templates = cr_checker.load_templates(template_file) + return templates[extension] + + +# write the config file here so that the year is always up to date with the year +# written in the test file +def write_config(path: Path, author: str) -> Path: + config_path = path / "config.json" + config_path.write_text(json.dumps({"author": author}), encoding="utf-8") + return config_path + + +# test that offset matches the length of the shebang line including trailing newlines +def test_detect_shebang_offset_counts_trailing_newlines(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + script.write_text( + "#!/usr/bin/env python3\n\nprint('hi')\n", + encoding="utf-8", + ) + + offset = cr_checker.detect_shebang_offset(script, "utf-8") + + assert offset == len("#!/usr/bin/env python3\n\n".encode("utf-8")) + + +@pytest.fixture( + params=[ + "cpp", + "c", + "h", + "hpp", + "py", + "sh", + "bzl", + "ini", + "yml", + "yaml", + "BUILD", + "bazel", + "rs", + "rst", + ] +) +def prepare_test_with_header(request: SubRequest, tmp_path: PosixPath) -> tuple: + extension = request.param + test_file = tmp_path / ("file." + extension) + header_template = load_template(extension) + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text( + header + "some content\n", + encoding="utf-8", + ) + return test_file, extension, header_template + + +@pytest.fixture( + params=[ + "cpp", + "c", + "h", + "hpp", + "py", + "sh", + "bzl", + "ini", + "yml", + "yaml", + "BUILD", + "bazel", + "rs", + "rst", + ] +) +def prepare_test_no_header(request: SubRequest, tmp_path: PosixPath) -> tuple: + extension = request.param + test_file = tmp_path / ("file." + extension) + header_template = load_template(extension) + current_year = datetime.now().year + test_file.write_text( + "some content\n", + encoding="utf-8", + ) + return test_file, extension, header_template, tmp_path + + +def test_process_files_detects_header(prepare_test_with_header): + cr_checker = load_cr_checker_module() + test_file, extension, header_template = prepare_test_with_header + + results = cr_checker.process_files( + [test_file], + {extension: header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +def test_process_files_detects_missing_header(prepare_test_no_header): + cr_checker = load_cr_checker_module() + test_file, extension, header_template, tmp_path = prepare_test_no_header + + results = cr_checker.process_files( + [test_file], + {extension: header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 1 + + +def test_process_files_inserts_missing_header(prepare_test_no_header): + cr_checker = load_cr_checker_module() + test_file, extension, header_template, tmp_path = prepare_test_no_header + author = "Author" + config = write_config(tmp_path, author) + + results = cr_checker.process_files( + [test_file], + {extension: header_template}, + True, + config=config, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 1 + assert results["fixed"] == 1 + expected_header = header_template.format(year=datetime.now().year, author="Author") + assert test_file.read_text(encoding="utf-8").startswith(expected_header) + + +def test_process_files_skips_exclusion_with_missing_header(prepare_test_no_header): + cr_checker = load_cr_checker_module() + test_file, extension, header_template, tmp_path = prepare_test_no_header + + results = cr_checker.process_files( + [test_file], + {extension: header_template}, + False, + [str(test_file)], + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that process_files function validates a license header after the shebang line +def test_process_files_accepts_header_after_shebang(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + script.write_text( + "#!/usr/bin/env python3\n" + header + "print('hi')\n", + encoding="utf-8", + ) + + results = cr_checker.process_files( + [script], + {"py": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that process_files function fixes a missing license header after the shebang line +def test_process_files_fix_inserts_header_after_shebang(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + script.write_text( + "#!/usr/bin/env python3\nprint('hi')\n", + encoding="utf-8", + ) + header_template = load_template("py") + current_year = datetime.now().year + author = "Author" + config = write_config(tmp_path, author) + + results = cr_checker.process_files( + [script], + {"py": header_template}, + True, + config=config, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["fixed"] == 1 + assert results["no_copyright"] == 1 + expected_header = header_template.format(year=current_year, author=author) + assert script.read_text(encoding="utf-8") == ( + "#!/usr/bin/env python3\n" + expected_header + "\n" + "print('hi')\n" + ) + + +# test that process_files function validates a license header without the shebang line +def test_process_files_accepts_header_without_shebang(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + script.write_text(header + "print('hi')\n", encoding="utf-8") + + results = cr_checker.process_files( + [script], + {"py": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that process_files function fixes a missing license header without the shebang +def test_process_files_fix_inserts_header_without_shebang(tmp_path): + cr_checker = load_cr_checker_module() + script = tmp_path / "script.py" + script.write_text("print('hi')\n", encoding="utf-8") + header_template = load_template("py") + current_year = datetime.now().year + author = "Author" + config = write_config(tmp_path, author) + + results = cr_checker.process_files( + [script], + {"py": header_template}, + True, + config=config, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["fixed"] == 1 + assert results["no_copyright"] == 1 + expected_header = header_template.format(year=current_year, author=author) + assert ( + script.read_text(encoding="utf-8") == expected_header + "\n" + "print('hi')\n" + ) + + +# test that border lines with different fill characters are accepted (flexible matching) +def test_process_files_accepts_flexible_border(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.cpp" + current_year = datetime.now().year + # Use '/' fill chars instead of '*' for border lines + header = ( + "/////////////////////////////////////////////////////////////////////////////////////\n" + f" * Copyright (c) {current_year} Author\n" + " *\n" + " * See the NOTICE file(s) distributed with this work for additional\n" + " * information regarding copyright ownership.\n" + " *\n" + " * This program and the accompanying materials are made available under the\n" + " * terms of the Apache License Version 2.0 which is available at\n" + " * https://www.apache.org/licenses/LICENSE-2.0\n" + " *\n" + " * SPDX-License-Identifier: Apache-2.0\n" + " /////////////////////////////////////////////////////////////////////////////////////\n" + ) + test_file.write_text(header + "int main() {}\n", encoding="utf-8") + header_template = load_template("cpp") + + results = cr_checker.process_files( + [test_file], + {"cpp": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that a blank line after the header does not cause a check failure +def test_process_files_accepts_header_with_trailing_blank_line(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text(header + "\nsome content\n", encoding="utf-8") + + results = cr_checker.process_files( + [test_file], + {"py": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["no_copyright"] == 0 + + +# test that fix_copyright inserts a blank line after the header +def test_process_files_fix_inserts_trailing_blank_line(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + test_file.write_text("some content\n", encoding="utf-8") + header_template = load_template("py") + current_year = datetime.now().year + author = "Author" + config = write_config(tmp_path, author) + + cr_checker.process_files( + [test_file], + {"py": header_template}, + True, + config=config, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + expected_header = header_template.format(year=current_year, author=author) + assert test_file.read_text(encoding="utf-8").startswith(expected_header + "\n") + + +# test that has_duplicate_copyright detects a header that appears twice +def test_has_duplicate_copyright_detects_duplicate(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text(header + header + "some content\n", encoding="utf-8") + + result = cr_checker.has_duplicate_copyright( + test_file, header_template, False, "utf-8", 0 + ) + + assert result is True + + +# test that has_duplicate_copyright returns False for a single header +def test_has_duplicate_copyright_single_header(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text(header + "some content\n", encoding="utf-8") + + result = cr_checker.has_duplicate_copyright( + test_file, header_template, False, "utf-8", 0 + ) + + assert result is False + + +# test that process_files counts duplicate headers separately from missing headers +def test_process_files_detects_duplicate_header(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + current_year = datetime.now().year + header = header_template.format(year=current_year, author="Author") + test_file.write_text(header + header + "some content\n", encoding="utf-8") + + results = cr_checker.process_files( + [test_file], + {"py": header_template}, + False, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, + ) + + assert results["duplicate_copyright"] == 1 + assert results["no_copyright"] == 0 + + +# test that has_duplicate_copyright detects two headers with different year ranges +def test_has_duplicate_copyright_detects_different_year_ranges(tmp_path): + cr_checker = load_cr_checker_module() + test_file = tmp_path / "file.py" + header_template = load_template("py") + header1 = header_template.format(year="2026", author="Author") + header2 = header_template.format(year="2024-2026", author="Author") + test_file.write_text(header1 + header2 + "some content\n", encoding="utf-8") + + result = cr_checker.has_duplicate_copyright( + test_file, header_template, False, "utf-8", 0 + ) + + assert result is True diff --git a/cr_checker/tool/BUILD b/cr_checker/tool/BUILD new file mode 100644 index 0000000..8ef719b --- /dev/null +++ b/cr_checker/tool/BUILD @@ -0,0 +1,21 @@ +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +load("@aspect_rules_py//py:defs.bzl", "py_library") + +py_library( + name = "cr_checker_lib", + srcs = [ + "cr_checker.py", + ], + visibility = ["//visibility:public"], +) diff --git a/cr_checker/tool/cr_checker.py b/cr_checker/tool/cr_checker.py new file mode 100755 index 0000000..1cfe050 --- /dev/null +++ b/cr_checker/tool/cr_checker.py @@ -0,0 +1,904 @@ +#!/usr/bin/env python3 + +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""The tool for checking if artifacts have proper copyright.""" + +import argparse +import json +import logging +import mmap +import os +import re +import shutil +import sys +import tempfile +from datetime import datetime +from pathlib import Path + +BYTES_TO_READ = 4 * 1024 +DEFAULT_AUTHOR = "Contributors to the Eclipse Foundation" + +BORDER_FILL_PATTERN = re.compile(r"([/*#'\-=+])\1{4,}") +FILL_CHARS_REGEX = r"[/*#'\-=+]+" + +LOGGER = logging.getLogger() + +COLORS = { + "BLUE": "\033[34m", + "GREEN": "\033[32m", + "YELLOW": "\033[33m", + "RED": "\033[31m", + "DARK_RED": "\033[35;1m", + "ENDC": "\033[0m", +} + +LOGGER_COLORS = { + "DEBUG": COLORS["BLUE"], + "INFO": COLORS["GREEN"], + "WARNING": COLORS["YELLOW"], + "ERROR": COLORS["RED"], + "CRITICAL": COLORS["DARK_RED"], +} + + +class ColoredFormatter(logging.Formatter): + """ + A custom logging formatter to add color to log level names based on the logging level. + + The `ColoredFormatter` class extends `logging.Formatter` and overrides the `format` + method to add color codes to the log level name (e.g., `INFO`, `WARNING`, `ERROR`) + based on a predefined color mapping in `LOGGER_COLORS`. This color coding helps in + visually distinguishing log messages by severity. + + Attributes: + LOGGER_COLORS (dict): A dictionary mapping log level names (e.g., "INFO", "ERROR") + to their respective color codes. + COLORS (dict): A dictionary of terminal color codes, including an "ENDC" key to reset + colors after the level name. + + Methods: + format(record): Adds color to the `levelname` attribute of the log record and then + formats the record as per the superclass `Formatter`. + """ + + def format(self, record): + log_color = LOGGER_COLORS.get(record.levelname, "") + record.levelname = f"{log_color}{record.levelname}:{COLORS['ENDC']}" + return super().format(record) + + +class ParamFileAction(argparse.Action): # pylint: disable=too-few-public-methods + """ + A custom argparse action to support exclusive parameter files for command-line arguments. + + The `ParamFileAction` class allows users to specify a parameter file (prefixed with '@') + containing file paths or other inputs, which will override any additional inputs provided + in the command line. If a parameter file is found, its contents are used exclusively, + and all other inputs are ignored. If no parameter file is provided, standard inputs are used. + + Attributes: + parser (argparse.ArgumentParser): The argument parser instance. + namespace (argparse.Namespace): The namespace where arguments are stored. + values (list): The list of argument values passed from the command line. + option_string (str, optional): The option string that triggered this action, if any. + + Methods: + __call__(parser, namespace, values, option_string=None): Processes the arguments. + - If any value starts with '@', it reads the parameter file and sets `file_paths` + in `namespace`. + - If no parameter file is detected, it directly assigns `values` to `namespace`. + """ + + def __call__(self, parser, namespace, values, option_string=None): + paramfile = next((v[1:] for v in values if v.startswith("@")), None) + if paramfile: + with open(paramfile, "r", encoding="utf-8") as handle: + file_paths = [line.strip() for line in handle if line.strip()] + setattr(namespace, self.dest, file_paths) + else: + setattr(namespace, self.dest, values) + + +def get_author_from_config(config_path: Path = None) -> str: + """ + Reads the author from a JSON configuration file. + + Args: + config_path (Path): Path to the configuration JSON file. + + Returns: + str: Author from the configuration file. + """ + if not config_path: + return DEFAULT_AUTHOR + with config_path.open("r") as file: + config = json.load(file) + return config.get("author", DEFAULT_AUTHOR) + + +def convert_bre_to_regex(template: str) -> str: + """ + Convert BRE-style template (literal by default) to standard regex. + In the template: * is literal, \\* is a metacharacter. + """ + # First, escape all regex metacharacters to make them literal + escaped = re.escape(template) + # Now, find escaped backslashes followed by escaped metacharacters + # and convert them back to actual regex metacharacters + metacharacters = r"\\.*+-?[]{}()^$|" + for char in metacharacters: + escaped = escaped.replace(re.escape("\\" + char), char) + return escaped + + +def line_to_flexible_regex(line: str) -> str: + """ + Convert a border line to a regex that accepts any fill characters. + + Runs of 5+ identical fill characters (e.g. ``****``) are replaced with + ``[/*#'\\-=+]+`` so that alternative styles (e.g. ``////``) are also + accepted. + """ + stripped = line.rstrip("\n") + has_newline = line.endswith("\n") + result = [] + last_end = 0 + for m in BORDER_FILL_PATTERN.finditer(stripped): + result.append(re.escape(stripped[last_end : m.start()])) + result.append(FILL_CHARS_REGEX) + last_end = m.end() + result.append(re.escape(stripped[last_end:])) + if has_newline: + result.append("\n") + return "".join(result) + + +def load_templates(path): + """ + Loads the copyright templates from a configuration file. + + Args: + path (str): Path to the template file. + + Returns: + dict: A dictionary where each key is a file extension (e.g., ".cpp") + and the value is the template string from the config. + """ + + def add_template_for_extensions(templates: dict, extensions: list, template: str): + # Remove trailing lines from template and ensure line end + template = template.rstrip() + "\n" + for extension in extensions: + templates[extension] = template + + templates = {} + current_extensions = [] + + with open(path, "r", encoding="utf-8") as file: + lines = file.readlines() + template_for_extensions = "" + + for line in lines: + stripped_line = line.strip() + + if stripped_line.startswith("[") and stripped_line.endswith("]"): + add_template_for_extensions( + templates, current_extensions, template_for_extensions + ) + + template_for_extensions = "" + + extensions = stripped_line[1:-1].split(",") + current_extensions = [ext.strip() for ext in extensions] + LOGGER.debug(current_extensions) + else: + template_for_extensions += line + + add_template_for_extensions( + templates, current_extensions, template_for_extensions + ) + + LOGGER.debug(templates) + return templates + + +def load_exclusion(path): + """ + Loads the list of files being excluded from the copyright check. + + Args: + path (str): Path to the exclusion file. + + Returns: + tuple(list, bool): a list of files that are excluded from the copyright check and a boolean indicating whether + all paths listed in the exclusion file exist and are files. + """ + + exclusion = [] + valid = True + with open(path, "r", encoding="utf-8") as file: + exclusion = file.read().splitlines() + + for item in exclusion: + path = Path(item) + if not path.exists(): + LOGGER.error("Excluded file %s does not exist.", item) + exclusion.remove(item) + valid = False + continue + if not path.is_file(): + exclusion.remove(item) + LOGGER.error("Excluded file %s is not a file.", item) + valid = False + continue + + LOGGER.debug(exclusion) + return exclusion, valid + + +def configure_logging(log_file_path=None, verbose=False): + """ + Configures logging to write messages to the specified log file. + + Args: + log_file_path (str, optional): Path to the log file. + verbose (bool, optional): If True, sets log level to DEBUG. Otherwise, sets it to INFO. + """ + log_level = logging.DEBUG if verbose else logging.INFO + LOGGER.setLevel(log_level) + LOGGER.handlers.clear() + + if log_file_path is not None: + handler = logging.FileHandler(log_file_path) + formatter = logging.Formatter("%(levelname)s: %(message)s") + else: + handler = logging.StreamHandler() + formatter = ColoredFormatter("%(levelname)s %(message)s") + + handler.setLevel(log_level) + handler.setFormatter(formatter) + LOGGER.addHandler(handler) + + +def detect_shebang_offset(path, encoding): + """ + Detects if a file starts with a shebang (#!) and returns the byte offset + to skip it (length of the first line including newline). + + Args: + path (Path): A `pathlib.Path` object pointing to the file. + encoding (str): Encoding type to use when reading the file. + + Returns: + int: The byte length of the shebang line (including newline) if present, + otherwise 0. + """ + try: + with open(path, "r", encoding=encoding) as handle: + first_line = handle.readline() + if first_line.startswith("#!"): + # Calculate byte length of the first line + byte_length = len(first_line.encode(encoding)) + while True: + next_char = handle.read(1) + if not next_char or next_char not in ("\n", "\r"): + break + byte_length += len(next_char.encode(encoding)) + LOGGER.debug( + "Detected shebang in %s with offset %d bytes", path, byte_length + ) + return byte_length + except (IOError, OSError) as err: + LOGGER.debug("Could not detect shebang in %s: %s", path, err) + return 0 + + +def load_text_from_file(path, header_length, encoding, offset): + """ + Reads the first portion of a file, up to `header_length` characters + plus an additional offset if provided. + + Args: + path (Path): A `pathlib.Path` object pointing to the file. + header_length (int): Number of characters to read for the header. + encoding (str): Encoding type to use when reading the file. + offset (int): Additional number of characters to read beyond + `header_length`, typically used to account for extra + lines (such as a shebang) before the header. + + Returns: + str: The portion of the file read, which should contain the header if present, + including any extra characters specified by `offset`. + """ + total_length = header_length + offset + LOGGER.debug( + "Reading first %d characters from file: %s [%s]", total_length, path, encoding + ) + with open(path, "r", encoding=encoding) as handle: + content = handle.read(total_length) + return content[offset:] if offset else content + + +def load_text_from_file_with_mmap(path, header_length, encoding, offset): + """ + Maps the file and reads only the first `header_length` bytes plus + an additional offset if provided. + + Args: + path (Path): A `pathlib.Path` object pointing to the file. + header_length (int): Length of the header text to check. + encoding (str): String for setting decoding type. + offset (int): Additional number of characters to read beyond + `header_length`, typically used to account for extra + lines (such as a shebang) before the header. + + Returns: + str: The portion of the file read, which should contain the header if present. + """ + + file_size = os.path.getsize(path) + total_length = header_length + offset + length = min(total_length, file_size) + + if not length: + LOGGER.warning( + "File %s is empty [length: %d]. Return empty string.", path, length + ) + return "" + + LOGGER.debug("Memory mapping first %d bytes from file: %s", total_length, path) + with open(path, "r", encoding=encoding) as handle: + with mmap.mmap(handle.fileno(), length=length, access=mmap.ACCESS_READ) as fmap: + return fmap[:length].decode(encoding)[offset:] + + +def has_copyright(path, template, use_mmap, encoding, offset, config=None): + """ + Checks if the specified copyright text is present in the beginning of a file. + + Args: + path (Path): A `pathlib.Path` object pointing to the file to check. + template (str): The copyright text to search for at the beginning + of the file. + use_mmap (bool): If True, uses memory-mapped file reading for efficient + large file handling. + encoding (str): Encoding type to use when reading the file. + offset (int): Additional number of characters to read beyond the length + of `copyright_text`, used to account for extra content + (such as a shebang) before the copyright text. + config (Path): Path to the config JSON file where configuration + variables are stored (e.g. years for copyright headers). + + Returns: + bool: True if the file contains the copyright text, False if it is missing. + + Raises: + IOError: If there is an error opening or reading the file. + """ + + load_text = load_text_from_file_with_mmap if use_mmap else load_text_from_file + + lines = template.splitlines(keepends=True) + regex_parts = [] + for line in lines: + stripped_line = line.rstrip("\n") + if BORDER_FILL_PATTERN.search(stripped_line): + regex_parts.append(line_to_flexible_regex(line)) + else: + formatted = line.format(year=r"\\d\{4\}\(-\\d\{4\}\)\?", author=r"\.\*") + regex_parts.append(convert_bre_to_regex(formatted)) + template_regex = "".join(regex_parts) + "\n?" + + if re.match(template_regex, load_text(path, BYTES_TO_READ, encoding, offset)): + LOGGER.debug("File %s has copyright.", path) + return True + + LOGGER.debug("File %s doesn't have copyright.", path) + return False + + +def has_any_copyright(path, use_mmap, encoding, offset): + """ + Checks if any copyright notice is present in the file header, regardless of format. + + Args: + path (Path): A `pathlib.Path` object pointing to the file to check. + use_mmap (bool): If True, uses memory-mapped file reading. + encoding (str): Encoding type to use when reading the file. + offset (int): Byte offset to skip (e.g. shebang line). + + Returns: + bool: True if any copyright notice is found, False otherwise. + """ + load_text = load_text_from_file_with_mmap if use_mmap else load_text_from_file + content = load_text(path, BYTES_TO_READ, encoding, offset) + return bool( + re.search( + r"Copyright.*SPDX-License-Identifier", content, re.IGNORECASE | re.DOTALL + ) + ) + + +def has_duplicate_copyright(path, template, use_mmap, encoding, offset): + """ + Checks if more than one copyright notice is present in the file header. + + The check is format-agnostic: it counts occurrences of ``SPDX-License-Identifier`` + within a window of twice the template length, so that headers written by different + tools (e.g. REUSE vs. cr_checker) are both counted while string literals that + embed copyright text further into the file are ignored. + + Args: + path (Path): A `pathlib.Path` object pointing to the file to check. + template (str): The copyright template; its length defines the search window. + use_mmap (bool): If True, uses memory-mapped file reading. + encoding (str): Encoding type to use when reading the file. + offset (int): Byte offset to skip (e.g. shebang line). + + Returns: + bool: True if more than one copyright notice is found, False otherwise. + """ + load_text = load_text_from_file_with_mmap if use_mmap else load_text_from_file + content = load_text(path, 2 * len(template), encoding, offset) + matches = list(re.finditer(r"SPDX-License-Identifier", content, re.IGNORECASE)) + if len(matches) > 1: + LOGGER.debug("File %s has %d copyright headers.", path, len(matches)) + return True + return False + + +def get_files_from_dir(directory, exts=None): + """ + Finds files in the specified directories. Filters by extensions if provided. + + Args: + dirs (list of str): List of directories to search for files. + exts (list of str, optional): List of extensions to filter files. + If None, all files are returned. + + Returns: + list of str: List of file paths found in the directories. + """ + collected_files = [] + LOGGER.debug("Getting files from directory: %s", directory) + for path in directory.rglob("*"): + if path.is_file() and path.stat().st_size != 0: + if ( + exts is None + or path.suffix[1:] in exts + or (path.name == "BUILD" and "BUILD" in exts) + ): + collected_files.append(path) + return collected_files + + +def collect_inputs(inputs, exts=None): + """ + Collects files from a list of input paths, optionally filtering by file extensions. + + Args: + inputs (list): A list of paths to files or directories. + If a directory is provided, all files within it are added to the output. + exts (list, optional): A list of file extensions to filter by (e.g., ['.py', '.txt']). + Only files with these extensions will be included if specified. + + Returns: + list: A list of file paths collected from the input paths, filtered by the given extensions. + If an input is neither a file nor a directory, it is skipped with a warning. + + Logs: + Logs messages at the DEBUG level, detailing processing of directories and files, + and warns if an invalid input path is encountered. + """ + all_files = [] + LOGGER.debug("Extensions: %s", exts) + for i in inputs: + item = Path(i) + if item.is_dir(): + LOGGER.debug("Processing directory: %s", item) + all_files.extend(get_files_from_dir(item, exts)) + elif item.is_file() and ( + exts is None + or item.suffix[1:] in exts + or (item.name == "BUILD" and "BUILD" in exts) + ): + LOGGER.debug("Processing file: %s", item) + all_files.append(item) + elif item.is_file(): + LOGGER.debug("Skipped (no configuration for file extension): %s", item) + else: + LOGGER.warning("Skipped (input is not a valid file or directory): %s", item) + return all_files + + +def create_temp_file(path, encoding): + """ + Creates a temporary file with the provided content. + + Args: + path (str): The path of file to write the content to the temporary file. + encoding (str, optional): Encoding type to use when writing the file. + + Returns: + str: The path to the temporary file created. + """ + with tempfile.NamedTemporaryFile(mode="w", encoding=encoding, delete=False) as temp: + with open(path, "r", encoding=encoding) as handle: + for chunk in iter(lambda: handle.read(4096), ""): + temp.write(chunk) + return temp.name + + +def remove_old_header(file_path, encoding, num_of_chars): + """ + Removes the first `num_of_chars` characters from a file and updates it in-place. + + Args: + file_path (str): Path to the file to be modified. + encoding (str): Encoding used to read and write the file. + num_of_chars (int): Number of characters to remove from the beginning of the file. + + Raises: + IOError: If there is an issue reading or writing the file. + ValueError: If `num_of_chars` is negative. + """ + with open(file_path, "r", encoding=encoding) as file: + file.seek(num_of_chars) + with tempfile.NamedTemporaryFile( + "w", delete=False, encoding=encoding + ) as temp_file: + shutil.copyfileobj(file, temp_file) + shutil.move(temp_file.name, file_path) + + +def fix_copyright(path, copyright_text, encoding, offset, config=None) -> bool: + """ + Inserts a copyright header into the specified file, ensuring that existing + content is preserved according to the provided offset. + + Args: + path (str): The path to the file that needs the copyright header. + copyright_text (str): The copyright text to be added. + encoding (str): The character encoding used to read and write the file. + offset (int): The number of bytes to preserve at the top of the file. + If 0, the first line is overwritten unless it's empty. + For non-zero offsets, ensures the correct number of bytes + are preserved. + config (Path): Path to the config JSON file where configuration + variables are stored (e.g. years for copyright headers). + Returns: + bool: True if the copyright header was successfully added, False if there was an error + """ + + temporary_file = create_temp_file(path, encoding) + + with open(temporary_file, "r", encoding=encoding) as temp: + first_line = temp.readline() + byte_array = len(first_line.encode(encoding)) + + if offset > 0 and offset != byte_array: + LOGGER.error( + "%s: Invalid offset value: %d, expected: %d", path, offset, byte_array + ) + return False + + with open(path, "w", encoding=encoding) as handle: + temp.seek(0) + if offset > 0: + handle.write(first_line) + temp.seek(offset) + handle.write( + copyright_text.format( + year=datetime.now().year, author=get_author_from_config(config) + ) + + "\n" + ) + for chunk in iter(lambda: temp.read(4096), ""): + handle.write(chunk) + LOGGER.info("Fixed missing header in: %s", path) + return True + + +def process_files( + files, + templates, + fix, + exclusion=[], + config=None, + use_mmap=False, + encoding="utf-8", + offset=0, + remove_offset=0, +): # pylint: disable=too-many-arguments + """ + Processes a list of files to check for the presence of copyright text. + + Args: + files (list): A list of file paths to check. + templates (dict): A dictionary where keys are file extensions + (e.g., '.py', '.txt') and values are strings or patterns + representing the required copyright text. + exclusion (list): A list of paths to files to be excluded from the copyright + check. + config (Path): Path to the config JSON file where configuration + variables are stored (e.g. years for copyright headers). + use_mmap (bool): Flag for using mmap function for reading files + (instead of standard option). + encoding (str): Encoding type to use when reading the file. + offset (int): Additional number of characters to read beyond the length + of `copyright_text`, used to account for extra content + (such as a shebang) before the copyright text. + remove_offset(int): Flag for removing old header from source files + (before applying the new one) in number of chars. + + Returns: + int: The number of files that do not contain the required copyright text. + """ + results = {"no_copyright": 0, "fixed": 0, "duplicate_copyright": 0} + for item in files: + name = Path(item).name + key = name if name == "BUILD" else Path(item).suffix[1:] + if key not in templates.keys(): + logging.debug( + "Skipped (no configuration for selected file extension): %s", item + ) + continue + + if str(item) in exclusion: + logging.debug("Skipped due to exclusion: %s", item) + continue + + if os.path.getsize(item) == 0: + # No need to add copyright headers to empty files + continue + + # Automatically detect shebang and use its offset if no manual offset provided + shebang_offset = detect_shebang_offset(item, encoding) + effective_offset = offset + shebang_offset if offset == 0 else offset + + if has_duplicate_copyright( + item, templates[key], use_mmap, encoding, effective_offset + ): + LOGGER.error("Duplicate copyright header in: %s", item) + results["duplicate_copyright"] += 1 + elif not has_copyright( + item, templates[key], use_mmap, encoding, effective_offset, config + ): + if has_any_copyright(item, use_mmap, encoding, effective_offset): + LOGGER.warning( + "Wrong copyright format in: %s, expected format from template", item + ) + elif fix: + if remove_offset: + remove_old_header(item, encoding, remove_offset) + fix_result = fix_copyright( + item, templates[key], encoding, effective_offset, config + ) + results["no_copyright"] += 1 + if fix_result: + results["fixed"] += 1 + else: + LOGGER.error( + "Missing copyright header in: %s, use --fix to introduce it", item + ) + results["no_copyright"] += 1 + return results + + +def parse_arguments(argv): + """ + Parses command-line arguments. + + Args: + argv (list of str): List of command-line arguments. + + Returns: + argparse.Namespace: Parsed arguments containing files, directories, + copyright_file, extensions and log_file. + """ + parser = argparse.ArgumentParser( + description="A script to check for copyright in files with specific extensions." + ) + + parser.add_argument( + "-t", + "--template-file", + type=Path, + required=True, + help="Path to the template file", + ) + + parser.add_argument( + "--exclusion-file", + type=Path, + required=False, + help="Path to the file listing file paths excluded from the copyright check.", + ) + + parser.add_argument( + "-c", + "--config-file", + type=Path, + default=None, + help="Path to the config file", + ) + + parser.add_argument( + "-v", "--verbose", action="store_true", help="Enable debug logging level" + ) + + parser.add_argument( + "-l", + "--log-file", + type=Path, + default=None, + help="Redirect logs from STDOUT to this file", + ) + + parser.add_argument( + "-e", + "--extensions", + type=str, + nargs="+", + default=None, + help="List of extensions to filter when searching for files, e.g., '.h .cpp'", + ) + + parser.add_argument( + "--use_memory_map", + action="store_true", + help="Use memory map for reading content of files \ + (should be used reading gigabyte ranged files).", + ) + + parser.add_argument( + "-f", + "--fix", + action="store_true", + help="Fix missing copyright headers by inserting them", + ) + + parser.add_argument( + "--encoding", default="utf-8", help="File encoding (default: utf-8)." + ) + + parser.add_argument( + "--offset", + dest="offset", + type=int, + default=0, + help="Additional length offset to account for characters like a shebang (default is 0)", + ) + + parser.add_argument( + "--remove-offset", + dest="remove_offset", + type=int, + default=0, + help="Offset to remove old header from beginning of the file \ + (supported only with --fix mode)", + ) + + parser.add_argument( + "inputs", + nargs="+", + action=ParamFileAction, + help="Directories and/or files to parse.", + ) + + return parser.parse_args(argv) + + +def main(argv=None): + """ + Entry point for processing files to check for the presence of required copyright text. + + This function parses command-line arguments, configures logging, loads copyright templates, + collects input files based on provided criteria, and checks each file for the required + copyright text. + + Args: + argv (list, optional): List of command-line arguments. + If `None`, defaults to `sys.argv[1:]`. + + Returns: + int: Error code if an IOError occurs during loading templates or collecting input files; + otherwise, returns 0 as success. + """ + args = parse_arguments(argv if argv is not None else sys.argv[1:]) + configure_logging(args.log_file, args.verbose) + + try: + templates = load_templates(args.template_file) + except IOError as err: + LOGGER.error("Failed to load copyright text: %s", err) + return err.errno + + exclusion = [] + exclusion_valid = True + if args.exclusion_file: + try: + exclusion, exclusion_valid = load_exclusion(args.exclusion_file) + except IOError as err: + LOGGER.error("Failed to load exclusion list: %s", err) + return err.errno + + try: + files = collect_inputs(args.inputs, args.extensions) + except IOError as err: + LOGGER.error("Failed to process file %s with error", err.filename) + return err.errno + + LOGGER.debug("Running check on files: %s", files) + + if args.fix and args.remove_offset: + LOGGER.info("%s!------DANGER ZONE------!%s", COLORS["RED"], COLORS["ENDC"]) + LOGGER.info("Remove offset set! This can REMOVE parts of source files!") + LOGGER.info( + "Use ONLY if invalid copyright header is present that needs to be removed!" + ) + LOGGER.info("%s!-----------------------!%s", COLORS["RED"], COLORS["ENDC"]) + + results = process_files( + files, + templates, + args.fix, + exclusion, + args.config_file, + args.use_memory_map, + args.encoding, + args.offset, + args.remove_offset, + ) + total_no = results["no_copyright"] + total_fixes = results["fixed"] + total_duplicates = results["duplicate_copyright"] + + LOGGER.info("=" * 64) + LOGGER.info("Process completed.") + LOGGER.info( + "Total files without copyright: %s%d%s", + COLORS["RED"] if total_no > 0 else COLORS["GREEN"], + total_no, + COLORS["ENDC"], + ) + LOGGER.info( + "Total files with duplicate copyright: %s%d%s", + COLORS["RED"] if total_duplicates > 0 else COLORS["GREEN"], + total_duplicates, + COLORS["ENDC"], + ) + if not exclusion_valid: + LOGGER.info("The exclusion file contains paths that do not exist.") + if args.fix: + total_not_fixed = total_no - total_fixes + LOGGER.info( + "Total files that were fixed: %s%d%s", + COLORS["GREEN"], + total_fixes, + COLORS["ENDC"], + ) + LOGGER.info( + "Total files that were NOT fixed: %s%d%s", + COLORS["RED"] if total_not_fixed > 0 else COLORS["GREEN"], + total_not_fixed, + COLORS["ENDC"], + ) + LOGGER.info("=" * 64) + + return 0 if (total_no == 0 and total_duplicates == 0 and exclusion_valid) else 1 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1:])) diff --git a/cr_checker/tool/pre-commit_wrapper b/cr_checker/tool/pre-commit_wrapper new file mode 100755 index 0000000..90f1f9d --- /dev/null +++ b/cr_checker/tool/pre-commit_wrapper @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 + +# ******************************************************************************* +# Copyright (c) 2024 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* +"""The tool for checking if artifacts have proper copyright.""" + +import cr_checker +import os +import sys + +if __name__ == "__main__": + # --template-file and --config-file cannot be set via .pre-commit-config.yaml + script_dir = os.path.dirname(os.path.abspath(__file__)) + sys.argv.insert(1, '--template-file') + sys.argv.insert( + 2, + os.path.normpath(os.path.join(script_dir, '..', 'resources', 'templates.ini')), + ) + sys.argv.insert(3, '--config-file') + sys.argv.insert( + 4, + os.path.normpath(os.path.join(script_dir, '..', 'resources', 'config.json')), + ) + sys.argv.insert(5, '--exclusion-file') + sys.argv.insert( + 6, + os.path.normpath(os.path.join(script_dir, '..', 'resources', 'exclusion.txt')), + ) + + sys.exit(cr_checker.main(sys.argv[1:])) From d44a44753ab0ab6225eab757c630e07424cd10d0 Mon Sep 17 00:00:00 2001 From: MaximilianSoerenPollak Date: Wed, 15 Jul 2026 12:09:32 +0200 Subject: [PATCH 2/2] Chore: fixing various things (linting & formatting) Fix copyright Fix reuse copyright findingds Fix linting --- .bazelrc | 1 - .github/workflows/tests.yml | 54 ++++++++++++++--------------- .pre-commit-config.yaml | 12 +++++++ cr_checker/tests/test_cr_checker.py | 2 +- 4 files changed, 40 insertions(+), 29 deletions(-) diff --git a/.bazelrc b/.bazelrc index 2d44ee2..1b8cd6c 100644 --- a/.bazelrc +++ b/.bazelrc @@ -38,4 +38,3 @@ build:debug --//bazel/rules/rules_score:verbosity=debug coverage:coverage --combined_report=lcov #coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Clink-dead-code #coverage:coverage --@rules_rust//rust/settings:extra_rustc_flag=-Ccodegen-units=1 - diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 44232e6..5a43bd0 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -10,30 +10,30 @@ # # SPDX-License-Identifier: Apache-2.0 # ******************************************************************************* - integration_tests: - runs-on: ubuntu-24.04 - permissions: - contents: read - actions: write - steps: - - name: Checkout repository - uses: actions/checkout@v7.0.0 - - name: Free Disk Space (Ubuntu) - uses: eclipse-score/more-disk-space@v1 - with: - level: 4 - - uses: castler/setup-bazel@cache-optimized - with: - bazelisk-cache: true - disk-cache: integration_tests - repository-cache: true - cache-optimized: true - cache-save: ${{ github.ref == 'refs/heads/main' }} - # - name: Run python_basics integration tests - # run: | - # cd python_basics/integration_tests - # bazel test //... - - name: Run cr_checker unit tests - run: | - cd cr_checker/tests - bazel test //... +integration_tests: + runs-on: ubuntu-24.04 + permissions: + contents: read + actions: write + steps: + - name: Checkout repository + uses: actions/checkout@v7.0.0 + - name: Free Disk Space (Ubuntu) + uses: eclipse-score/more-disk-space@v1 + with: + level: 4 + - uses: castler/setup-bazel@cache-optimized + with: + bazelisk-cache: true + disk-cache: integration_tests + repository-cache: true + cache-optimized: true + cache-save: ${{ github.ref == 'refs/heads/main' }} + # - name: Run python_basics integration tests + # run: | + # cd python_basics/integration_tests + # bazel test //... + - name: Run cr_checker unit tests + run: | + cd cr_checker/tests + bazel test //... diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d2a5d82..e6c8436 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,15 @@ +# ******************************************************************************* +# Copyright (c) 2026 Contributors to the Eclipse Foundation +# +# See the NOTICE file(s) distributed with this work for additional +# information regarding copyright ownership. +# +# This program and the accompanying materials are made available under the +# terms of the Apache License Version 2.0 which is available at +# https://www.apache.org/licenses/LICENSE-2.0 +# +# SPDX-License-Identifier: Apache-2.0 +# ******************************************************************************* repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: 3e8a8703264a2f4a69428a0aa4dcb512790b2c8c # v6.0.0 diff --git a/cr_checker/tests/test_cr_checker.py b/cr_checker/tests/test_cr_checker.py index 48bc842..1d9a9ee 100644 --- a/cr_checker/tests/test_cr_checker.py +++ b/cr_checker/tests/test_cr_checker.py @@ -324,7 +324,7 @@ def test_process_files_accepts_flexible_border(tmp_path): " * terms of the Apache License Version 2.0 which is available at\n" " * https://www.apache.org/licenses/LICENSE-2.0\n" " *\n" - " * SPDX-License-Identifier: Apache-2.0\n" + " * SPDX-" "License-Identifier: Apache-2.0\n" " /////////////////////////////////////////////////////////////////////////////////////\n" ) test_file.write_text(header + "int main() {}\n", encoding="utf-8")