Skip to content

Commit e1f7ba6

Browse files
authored
chore(lint): stamp license headers with the current year (#246)
## Summary ### Why? The license-header linter pinned the copyright year to a hardcoded `2025` constant and required an exact match, so any file authored in a later year would need a manual constant bump — and bumping it would retroactively fail every existing file. ### What? The header is now validated by pattern: the copyright line matches `// Copyright (c) <YYYY> Uber Technologies, Inc.` for any 4-digit year, while the rest of the block must match exactly. The `-fix` mode stamps `time.Now().Year()`, so new files get the current year automatically with no future edits. Existing files (all `2025`) keep passing. Added test cases covering an older year, a wrong company, and a non-numeric year. ## Test Plan ✅ `bazel test //tool/linter/licenseheader:licenseheader_test` ✅ `bazel run //tool/linter/licenseheader -- -check` (whole repo still clean)
1 parent c4fa997 commit e1f7ba6

2 files changed

Lines changed: 61 additions & 13 deletions

File tree

tool/linter/licenseheader/main.go

Lines changed: 38 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,16 @@ import (
1919
"fmt"
2020
"os"
2121
"path/filepath"
22+
"regexp"
2223
"strings"
24+
"time"
2325
)
2426

25-
const header = `// Copyright (c) 2025 Uber Technologies, Inc.
26-
//
27+
// licenseBody is everything after the copyright line. The copyright line itself
28+
// carries a year, which is validated by pattern (any 4-digit year) rather than
29+
// pinned to a single value — so files authored in earlier years keep passing
30+
// while new files are stamped with the current year.
31+
const licenseBody = `//
2732
// Licensed under the Apache License, Version 2.0 (the "License");
2833
// you may not use this file except in compliance with the License.
2934
// You may obtain a copy of the License at
@@ -36,6 +41,20 @@ const header = `// Copyright (c) 2025 Uber Technologies, Inc.
3641
// See the License for the specific language governing permissions and
3742
// limitations under the License.`
3843

44+
// copyrightLineRe matches the first line of the header with any 4-digit year.
45+
var copyrightLineRe = regexp.MustCompile(`^// Copyright \(c\) \d{4} Uber Technologies, Inc\.$`)
46+
47+
// copyrightLine returns the copyright line for a given year.
48+
func copyrightLine(year int) string {
49+
return fmt.Sprintf("// Copyright (c) %d Uber Technologies, Inc.", year)
50+
}
51+
52+
// header returns the full license header stamped with the given year, used when
53+
// adding a header in -fix mode.
54+
func header(year int) string {
55+
return copyrightLine(year) + "\n" + licenseBody
56+
}
57+
3958
func main() {
4059
fix := flag.Bool("fix", false, "add missing license headers in-place")
4160
check := flag.Bool("check", false, "check for missing license headers (default mode)")
@@ -156,6 +175,7 @@ func isGeneratedFile(path string) bool {
156175
}
157176

158177
// hasLicenseHeader checks if a file starts with the expected license header.
178+
// The copyright year may be any 4-digit year; the rest must match exactly.
159179
func hasLicenseHeader(path string) (bool, error) {
160180
data, err := os.ReadFile(path)
161181
if err != nil {
@@ -171,30 +191,40 @@ func hasLicenseHeader(path string) (bool, error) {
171191
}
172192
}
173193

174-
return strings.HasPrefix(content, header), nil
194+
nl := strings.Index(content, "\n")
195+
if nl < 0 {
196+
return false, nil
197+
}
198+
if !copyrightLineRe.MatchString(content[:nl]) {
199+
return false, nil
200+
}
201+
return strings.HasPrefix(content[nl+1:], licenseBody), nil
175202
}
176203

177-
// addLicenseHeader prepends the license header to a file.
178-
// If the file starts with a //go:build directive, the header is placed after it.
204+
// addLicenseHeader prepends the license header (stamped with the current year)
205+
// to a file. If the file starts with a //go:build directive, the header is
206+
// placed after it.
179207
func addLicenseHeader(path string) error {
180208
data, err := os.ReadFile(path)
181209
if err != nil {
182210
return err
183211
}
184212
content := string(data)
185213

214+
hdr := header(time.Now().Year())
215+
186216
var result string
187217
if strings.HasPrefix(content, "//go:build ") {
188218
idx := strings.Index(content, "\n")
189219
if idx >= 0 {
190220
buildLine := content[:idx+1]
191221
rest := content[idx+1:]
192-
result = buildLine + "\n" + header + "\n\n" + strings.TrimLeft(rest, "\n")
222+
result = buildLine + "\n" + hdr + "\n\n" + strings.TrimLeft(rest, "\n")
193223
} else {
194-
result = content + "\n\n" + header + "\n"
224+
result = content + "\n\n" + hdr + "\n"
195225
}
196226
} else {
197-
result = header + "\n\n" + content
227+
result = hdr + "\n\n" + content
198228
}
199229

200230
return os.WriteFile(path, []byte(result), 0644)

tool/linter/licenseheader/main_test.go

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"path/filepath"
2020
"strings"
2121
"testing"
22+
"time"
2223

2324
"github.com/stretchr/testify/assert"
2425
"github.com/stretchr/testify/require"
@@ -46,14 +47,21 @@ func TestIsGeneratedFile(t *testing.T) {
4647
func TestHasLicenseHeader(t *testing.T) {
4748
dir := t.TempDir()
4849

50+
curr := header(time.Now().Year())
51+
4952
tests := []struct {
5053
name string
5154
content string
5255
want bool
5356
}{
5457
{
5558
name: "has header",
56-
content: header + "\n\npackage foo\n",
59+
content: curr + "\n\npackage foo\n",
60+
want: true,
61+
},
62+
{
63+
name: "older year still valid",
64+
content: header(2025) + "\n\npackage foo\n",
5765
want: true,
5866
},
5967
{
@@ -63,14 +71,24 @@ func TestHasLicenseHeader(t *testing.T) {
6371
},
6472
{
6573
name: "go:build then header",
66-
content: "//go:build linux\n\n" + header + "\n\npackage foo\n",
74+
content: "//go:build linux\n\n" + curr + "\n\npackage foo\n",
6775
want: true,
6876
},
6977
{
7078
name: "go:build without header",
7179
content: "//go:build linux\n\npackage foo\n",
7280
want: false,
7381
},
82+
{
83+
name: "wrong company fails",
84+
content: "// Copyright (c) 2025 Someone Else, Inc.\n" + licenseBody + "\n\npackage foo\n",
85+
want: false,
86+
},
87+
{
88+
name: "non-numeric year fails",
89+
content: "// Copyright (c) YYYY Uber Technologies, Inc.\n" + licenseBody + "\n\npackage foo\n",
90+
want: false,
91+
},
7492
}
7593
for _, tt := range tests {
7694
t.Run(tt.name, func(t *testing.T) {
@@ -96,7 +114,7 @@ func TestAddLicenseHeader(t *testing.T) {
96114
require.NoError(t, err)
97115
content := string(data)
98116

99-
assert.True(t, strings.HasPrefix(content, header))
117+
assert.True(t, strings.HasPrefix(content, header(time.Now().Year())))
100118
assert.Contains(t, content, "package foo")
101119
})
102120

@@ -112,7 +130,7 @@ func TestAddLicenseHeader(t *testing.T) {
112130
content := string(data)
113131

114132
assert.True(t, strings.HasPrefix(content, "//go:build linux\n"))
115-
assert.Contains(t, content, header)
133+
assert.Contains(t, content, header(time.Now().Year()))
116134
assert.Contains(t, content, "package foo")
117135

118136
// Verify order: build directive, then header, then package
@@ -158,7 +176,7 @@ func TestAddLicenseHeader(t *testing.T) {
158176
require.NoError(t, err)
159177
content := string(data)
160178

161-
assert.True(t, strings.HasPrefix(content, header))
179+
assert.True(t, strings.HasPrefix(content, header(time.Now().Year())))
162180
assert.Contains(t, content, "syntax = \"proto3\"")
163181
})
164182
}

0 commit comments

Comments
 (0)