Skip to content
Closed
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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ jobs:
runs-on: ubuntu-latest
steps:

- name: Set up Go 1.21
- name: Set up Go 1.24
uses: actions/setup-go@v1
with:
go-version: 1.21
go-version: 1.24

- name: Check out code into the Go module directory
uses: actions/checkout@v2
Expand All @@ -32,4 +32,4 @@ jobs:
run: go install ./...

- name: Self-check
run: go-header $(git ls-files | grep -E '.*\.go$')
run: go-header ./...
9 changes: 4 additions & 5 deletions .go-header.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
values:
regexp:
copyright-holder: Copyright \(c\) {{mod-year-range}} Denis Tingaikin
template: |
vars:
copyright-holder: Copyright \(c\) {{mod-year-range}} Denis Tingaikin
template: |-
{{copyright-holder}}

SPDX-License-Identifier: Apache-2.0
Expand All @@ -16,4 +15,4 @@ template: |
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.
limitations under the License.
53 changes: 45 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
# go-header
[![ci](https://github.com/denis-tingaikin/go-header/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/denis-tingaikin/go-header/actions/workflows/ci.yml)

Go source code linter providing checks for license headers.
Go source code linter providing checks for license headers.

## Features

| Feature | Status | Details |
|-----------------------------|--------|------------------------------------------|
| ✅ **Copyright Headers** | ✔️ | Supports all standard formats |
| ✅ **Parallel Processing** | ✔️ | Processes files concurrently |
| ✅ **Comment Support** | ✔️ | `//`, `/* */`, `/* * */`|
| ✅ **Go/Analysis** | ✔️ | Native Go tooling integration |
| ✅ **Regex Customization** | ✔️ | User-defined pattern matching |
| ✅ **Automatic Year Checks** | ✔️ | Validates & updates copyright years |
| ✅ **Auto-Fix Files** | ✔️ | In-place header corrections |
| ⏳ **Go/Template Support** | ❌ | *In development* |
| ⏳ **Multi-License Support** | ❌ | *Planned* |



## Installation

Expand All @@ -10,11 +26,32 @@ For installation you can simply use `go install`.
```bash
go install github.com/denis-tingaikin/go-header/cmd/go-header@latest
```

## Configuration

To configuring `.go-header.yml` linter you simply need to fill the next fields:


Inline template:
```yaml
---
template: # expects header template string.
vars: # expects valid key value paris where key is string, value is regexp.
key1: value1 # const value just checks equality. Note `key1` should be used in template string as {{ .key1 }} or {{ .KEY1 }}.
key2: value2(.*) # regexp value just checks regex match. The value should be a valid regexp pattern. Note `key2` should be used in template string as {{ .key2 }} or {{ .KEY2 }}.
```
Filebased template:
```yaml
---
template-path: # expects header template path string.
vars: # expects `const` or `regexp` node with values where values is a map string to string.
key1: value1 # const value just checks equality. Note `key1` should be used in template string as {{ key1 }} or {{ KEY1 }}.
key2: value2(.*) # regexp value just checks regex match. The value should be a valid regexp pattern. Note `key2` should be used in template string as {{ key2 }} or {{ KEY2 }}.
```

## Configuration (DEPRECATED)

To configuring `.go-header.yml` linter you simply need to fill the next fields:

```yaml
---
template: # expects header template string.
Expand Down Expand Up @@ -48,7 +85,7 @@ values:
`go-header` linter expects file paths on input. If you want to run `go-header` only on diff files, then you can use this command:

```bash
go-header $(git diff --name-only | grep -E '.*\.go')
go-header ./...
```

## Setup example
Expand All @@ -59,11 +96,11 @@ Create configuration file `.go-header.yml` in the root of project.

```yaml
---
values:
const:
MY COMPANY: mycompany.com
vars:
DOMAIN: sales|product
MY_COMPANY: {{ .DOMAIN }}.mycompany.com
template: |
{{ MY COMPANY }}
{{ .MY_COMPANY }}
SPDX-License-Identifier: Apache-2.0

Licensed under the Apache License, Version 2.0 (the "License");
Expand All @@ -80,4 +117,4 @@ template: |
```

### Step 2
You are ready! Execute `go-header ${PATH_TO_FILES}` from the root of the project.
Run `go-header ./...`
161 changes: 161 additions & 0 deletions analysis.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
// Copyright (c) 2025 Denis Tingaikin
//
// SPDX-License-Identifier: Apache-2.0
//
// 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.

package goheader

import (
"go/ast"
"go/token"
"runtime"
"strings"
"sync"

"golang.org/x/tools/go/analysis"
)

// NewAnalyzer creates new analyzer based on template and goheader values
func NewAnalyzer(templ string, vars map[string]Value) *analysis.Analyzer {
var goheader = New(WithTemplate(templ), WithValues(vars))
return &analysis.Analyzer{
Doc: "the_only_doc",
URL: "https://github.com/denis-tingaikin/go-header",
Name: "goheader",
Run: func(p *analysis.Pass) (any, error) {
var wg sync.WaitGroup

var jobCh = make(chan *ast.File, len(p.Files))

for _, file := range p.Files {
jobCh <- file
}
close(jobCh)

for range runtime.NumCPU() {
wg.Add(1)
go func() {
defer wg.Done()

for file := range jobCh {

filename := p.Fset.Position(file.Pos()).Filename
if !strings.HasSuffix(filename, ".go") {
continue
}

issue := goheader.Analyze(&Target{
File: file,
Path: filename,
})

if issue == nil {
continue
}
f := p.Fset.File(file.Pos())

commentLine := 1
var offset int

// Inspired by https://github.com/denis-tingaikin/go-header/blob/4c75a6a2332f025705325d6c71fff4616aedf48f/analyzer.go#L85-L92
if len(file.Comments) > 0 && file.Comments[0].Pos() < file.Package {
if !strings.HasPrefix(file.Comments[0].List[0].Text, "/*") {
// When the comment is "//" there is a one character offset.
offset = 1
}
commentLine = p.Fset.PositionFor(file.Comments[0].Pos(), true).Line
}

// Skip issues related to build directives.
// https://github.com/denis-tingaikin/go-header/issues/18
if issue.Location().Position-offset < 0 {
continue
}

diag := analysis.Diagnostic{
Pos: f.LineStart(issue.Location().Line+1) + token.Pos(issue.Location().Position-offset), // The position of the first divergence.
Message: issue.Message(),
}

if fix := issue.Fix(); fix != nil {
current := len(fix.Actual)
for _, s := range fix.Actual {
current += len(s)
}

start := f.LineStart(commentLine)

end := start + token.Pos(current)

header := strings.Join(fix.Expected, "\n") + "\n"

// Adds an extra line between the package and the header.
if end == file.Package {
header += "\n"
}

diag.SuggestedFixes = []analysis.SuggestedFix{{
TextEdits: []analysis.TextEdit{{
Pos: start,
End: end,
NewText: []byte(header),
}},
}}
}

p.Report(diag)
}
}()
}

wg.Wait()
return nil, nil
},
}
}

// NewAnalyzerFromConfig creates a new analysis.Analyzer from goheader config file
func NewAnalyzerFromConfig(config *string) *analysis.Analyzer {
var goheaderOncer sync.Once
var goheader *analysis.Analyzer

return &analysis.Analyzer{
Doc: "the_only_doc",
URL: "https://github.com/denis-tingaikin/go-header",
Name: "goheader",
Run: func(p *analysis.Pass) (any, error) {
var err error
goheaderOncer.Do(func() {
var cfg Config
if err = cfg.Parse(*config); err != nil {
return
}
templ, err := cfg.GetTemplate()
if err != nil {
return
}
vars, err := cfg.GetValues()
if err != nil {
return
}
goheader = NewAnalyzer(templ, vars)
})

if err != nil {
return nil, err
}
return goheader.Run(p)
},
}
}
Loading