Skip to content
Closed
Show file tree
Hide file tree
Changes from 6 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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,4 @@ jobs:
run: go install ./...

- name: Self-check
run: go-header $(git ls-files | grep -E '.*\.go$')
run: go-header $(git ls-files | grep -E '.*\.go$' | grep -v 'testdata')
22 changes: 17 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,24 @@ 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:


```yaml
---
template: # expects header template string.
template-path: # expects path to file with license header 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 @@ -59,11 +72,10 @@ Create configuration file `.go-header.yml` in the root of project.

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

Licensed under the Apache License, Version 2.0 (the "License");
Expand Down
81 changes: 79 additions & 2 deletions analyzer.go
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2020-2024 Denis Tingaikin
// Copyright (c) 2020-2025 Denis Tingaikin
//
// SPDX-License-Identifier: Apache-2.0
//
Expand All @@ -23,6 +23,7 @@ import (
"os/exec"
"strings"
"time"
"unicode"
)

type Target struct {
Expand Down Expand Up @@ -74,17 +75,29 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) {
}

if err := a.processPerTargetValues(target); err != nil {
return &issue{msg: err.Error()}
return NewIssue(err.Error())
}

file := target.File
var header string
var offset = Location{
Position: 1,
}

if isNewLineRequired(file.Comments) {
return NewIssueWithLocation(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally this would include a fix so golangci-lint can automatically apply it. IIUC the Line value will point to the line after the comment as being the error location, so I think Fix{Actual: []string{}, Expected: []string{""}}, would be sufficient. Unfortunately my attempt to test it failed - isNewLineRequired is returning false.

Copy link
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ldez Thoughts?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

golangci-lint can already apply fixes.

I think the current implementation based on io.reader is hard to maintain and has some limitations.

"Missing a newline after the header. Consider adding a newline separator right after the copyright header.",
Location{
Line: countLines(file.Comments[0].Text()),
},
)
}

if len(file.Comments) > 0 && file.Comments[0].Pos() < file.Package {
if strings.HasPrefix(file.Comments[0].List[0].Text, "/*") {
header = (&ast.CommentGroup{List: []*ast.Comment{file.Comments[0].List[0]}}).Text()

header = handleStarBlock(header)
} else {
header = file.Comments[0].Text()
offset.Position += 3
Expand All @@ -101,6 +114,7 @@ func (a *Analyzer) Analyze(target *Target) (i Issue) {
i = NewIssueWithFix(i.Message(), i.Location(), fix)
}()
header = strings.TrimSpace(header)

if header == "" {
return NewIssue("Missed header for check")
}
Expand Down Expand Up @@ -147,6 +161,12 @@ func (a *Analyzer) readField(reader *Reader) string {
_ = reader.Next()
_ = reader.Next()

_ = reader.ReadWhile(unicode.IsSpace)

if reader.Peek() == '.' {
_ = reader.Next()
}

r := reader.ReadWhile(func(r rune) bool {
return r != '}'
})
Expand Down Expand Up @@ -254,3 +274,60 @@ func (a *Analyzer) generateFix(i Issue, file *ast.File, header string) (Fix, boo
fix.Actual = append(fix.Actual, strings.Split(actual, "\n")...)
return fix, true
}

func isNewLineRequired(group []*ast.CommentGroup) bool {
if len(group[0].List) > 1 {
for _, item := range group[0].List {
if strings.HasPrefix(item.Text, "/*") {
return true
}
}
}

if len(group) < 2 {
return false
}
return group[0].End() >= group[1].Pos()
}

func countLines(text string) int {
if text == "" {
return 0
}

lines := 1
for i := 0; i < len(text); i++ {
if text[i] == '\n' {
lines++
} else if text[i] == '\r' {
lines++
if i+1 < len(text) && text[i+1] == '\n' {
i++
}
}
}
return lines
}

func handleStarBlock(header string) string {
return trimEachLine(header, func(s string) string {
var trimmed = strings.TrimSpace(s)
if !strings.HasPrefix(trimmed, "*") {
return s
}
if v, ok := strings.CutPrefix(trimmed, "* "); ok {
return v
} else {
var res, _ = strings.CutPrefix(trimmed, "*")
return res
}
})
}

func trimEachLine(input string, trimFunc func(string) string) string {
lines := strings.Split(input, "\n")
for i, line := range lines {
lines[i] = trimFunc(line)
}
return strings.Join(lines, "\n")
}
Loading