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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 38 additions & 8 deletions tool/linter/licenseheader/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,16 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
"time"
)

const header = `// Copyright (c) 2025 Uber Technologies, Inc.
//
// licenseBody is everything after the copyright line. The copyright line itself
// carries a year, which is validated by pattern (any 4-digit year) rather than
// pinned to a single value — so files authored in earlier years keep passing
// while new files are stamped with the current year.
const licenseBody = `//
// 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
Expand All @@ -36,6 +41,20 @@ const header = `// Copyright (c) 2025 Uber Technologies, Inc.
// See the License for the specific language governing permissions and
// limitations under the License.`

// copyrightLineRe matches the first line of the header with any 4-digit year.
var copyrightLineRe = regexp.MustCompile(`^// Copyright \(c\) \d{4} Uber Technologies, Inc\.$`)

// copyrightLine returns the copyright line for a given year.
func copyrightLine(year int) string {
return fmt.Sprintf("// Copyright (c) %d Uber Technologies, Inc.", year)
}

// header returns the full license header stamped with the given year, used when
// adding a header in -fix mode.
func header(year int) string {
return copyrightLine(year) + "\n" + licenseBody
}

func main() {
fix := flag.Bool("fix", false, "add missing license headers in-place")
check := flag.Bool("check", false, "check for missing license headers (default mode)")
Expand Down Expand Up @@ -156,6 +175,7 @@ func isGeneratedFile(path string) bool {
}

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

return strings.HasPrefix(content, header), nil
nl := strings.Index(content, "\n")
if nl < 0 {
return false, nil
}
if !copyrightLineRe.MatchString(content[:nl]) {
return false, nil
}
return strings.HasPrefix(content[nl+1:], licenseBody), nil
}

// addLicenseHeader prepends the license header to a file.
// If the file starts with a //go:build directive, the header is placed after it.
// addLicenseHeader prepends the license header (stamped with the current year)
// to a file. If the file starts with a //go:build directive, the header is
// placed after it.
func addLicenseHeader(path string) error {
data, err := os.ReadFile(path)
if err != nil {
return err
}
content := string(data)

hdr := header(time.Now().Year())

var result string
if strings.HasPrefix(content, "//go:build ") {
idx := strings.Index(content, "\n")
if idx >= 0 {
buildLine := content[:idx+1]
rest := content[idx+1:]
result = buildLine + "\n" + header + "\n\n" + strings.TrimLeft(rest, "\n")
result = buildLine + "\n" + hdr + "\n\n" + strings.TrimLeft(rest, "\n")
} else {
result = content + "\n\n" + header + "\n"
result = content + "\n\n" + hdr + "\n"
}
} else {
result = header + "\n\n" + content
result = hdr + "\n\n" + content
}

return os.WriteFile(path, []byte(result), 0644)
Expand Down
28 changes: 23 additions & 5 deletions tool/linter/licenseheader/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"path/filepath"
"strings"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -46,14 +47,21 @@ func TestIsGeneratedFile(t *testing.T) {
func TestHasLicenseHeader(t *testing.T) {
dir := t.TempDir()

curr := header(time.Now().Year())

tests := []struct {
name string
content string
want bool
}{
{
name: "has header",
content: header + "\n\npackage foo\n",
content: curr + "\n\npackage foo\n",
want: true,
},
{
name: "older year still valid",
content: header(2025) + "\n\npackage foo\n",
want: true,
},
{
Expand All @@ -63,14 +71,24 @@ func TestHasLicenseHeader(t *testing.T) {
},
{
name: "go:build then header",
content: "//go:build linux\n\n" + header + "\n\npackage foo\n",
content: "//go:build linux\n\n" + curr + "\n\npackage foo\n",
want: true,
},
{
name: "go:build without header",
content: "//go:build linux\n\npackage foo\n",
want: false,
},
{
name: "wrong company fails",
content: "// Copyright (c) 2025 Someone Else, Inc.\n" + licenseBody + "\n\npackage foo\n",
want: false,
},
{
name: "non-numeric year fails",
content: "// Copyright (c) YYYY Uber Technologies, Inc.\n" + licenseBody + "\n\npackage foo\n",
want: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand All @@ -96,7 +114,7 @@ func TestAddLicenseHeader(t *testing.T) {
require.NoError(t, err)
content := string(data)

assert.True(t, strings.HasPrefix(content, header))
assert.True(t, strings.HasPrefix(content, header(time.Now().Year())))
assert.Contains(t, content, "package foo")
})

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

assert.True(t, strings.HasPrefix(content, "//go:build linux\n"))
assert.Contains(t, content, header)
assert.Contains(t, content, header(time.Now().Year()))
assert.Contains(t, content, "package foo")

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

assert.True(t, strings.HasPrefix(content, header))
assert.True(t, strings.HasPrefix(content, header(time.Now().Year())))
assert.Contains(t, content, "syntax = \"proto3\"")
})
}
Expand Down
Loading