Skip to content
Open
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
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Flags:
-c int
display offending line with this many lines of context (default -1)
-comment-style string
Comment style (line, block) (default "line")
Comment style (line, block, starred-block) (default "line")
-copyright-header-matcher string
Copyright header matcher regexp (used to detect existence of any copyright header) (default "(?i)copyright")
-cpuprofile string
Expand All @@ -89,7 +89,7 @@ Flags:
-diff
with -fix, don't update the files, but print a unified diff
-exclude string
Paths to exclude (doublestar or r!-prefixed regexp, comma-separated) (default "**/testdata/**")
Paths to exclude (doublestar or r!-prefixed regexp, comma-separated)
-fix
apply all suggested fixes
-flags
Expand Down Expand Up @@ -190,8 +190,20 @@ a file. The current supported options are:

golicenser supports configuring the comment type used for the license headers. The options are:


- `line` - C-style line comments (`// test`).
- `block` - C++-style block comments (`/* test */`)
- `block` - C++-style block comments (`/* test */`). Example:
```go
/*
test
*/
```
- `starred-block` - Aligned, starred block comments. Example:
```go
/*
* test
*/
```

### Matchers

Expand Down
29 changes: 26 additions & 3 deletions analysis.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,7 @@ func (a *analyzer) checkFile(pass *analysis.Pass, file *ast.File) error {
}
}

if header == "" || !a.headerMatcher.MatchString(header) {
if header == "" || isDirective(header) || !a.headerMatcher.MatchString(header) {
// License header is missing, generate a new one.
newHeader, err := a.header.Create(filename)
if err != nil {
Expand All @@ -209,8 +209,7 @@ func (a *analyzer) checkFile(pass *analysis.Pass, file *ast.File) error {
}
if modified {
pass.Report(analysis.Diagnostic{
Pos: headerPos,
End: headerEnd,
Pos: file.Package,
Message: "invalid license header",
SuggestedFixes: []analysis.SuggestedFix{{
Message: "update license header",
Expand All @@ -225,3 +224,27 @@ func (a *analyzer) checkFile(pass *analysis.Pass, file *ast.File) error {

return nil
}

// isDirective checks if a comment is a directive.
func isDirective(s string) bool {
if len(s) < 3 || s[0] != '/' || s[1] != '/' || s[2] == ' ' {
return false
}
s = s[2:]

// Match directives in format: "[a-z0-9]+:[a-z0-9]"
colon := strings.Index(s, ":")
if colon <= 0 || colon+1 >= len(s) {
return false
}
for i := range colon + 2 {
if i == colon {
continue
}
b := s[i]
if ('a' > b || b > 'z') && ('0' > b || b > '9') {
return false
}
}
return true
}
58 changes: 57 additions & 1 deletion analysis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ func TestAnalyzer(t *testing.T) {

t.Run("simple", func(t *testing.T) {
t.Parallel()

cfg := Config{
Header: HeaderOpts{
Template: "Copyright (c) {{.year}} {{.author}}",
Expand Down Expand Up @@ -193,6 +192,63 @@ func TestAnalyzer(t *testing.T) {
_ = analysistest.Run(t, packageDir, a)
})
})

t.Run("build directive with any matcher", func(t *testing.T) {
t.Parallel()
cfg := Config{
Header: HeaderOpts{
Template: "Copyright (c) {{.year}} {{.author}}",
Author: "Test",
YearMode: YearModeThisYear,
},
CopyrightHeaderMatcher: ".+",
}
a, err := NewAnalyzer(cfg)
if err != nil {
t.Fatalf("NewAnalyzer() err = %v", err)
}

packageDir := filepath.Join(analysistest.TestData(), "src/builddirective/")
_ = analysistest.Run(t, packageDir, a)
})

t.Run("comment style block", func(t *testing.T) {
t.Parallel()
cfg := Config{
Header: HeaderOpts{
Template: "Copyright (c) {{.year}} {{.author}}",
Author: "Test",
YearMode: YearModeThisYear,
CommentStyle: CommentStyleBlock,
},
}
a, err := NewAnalyzer(cfg)
if err != nil {
t.Fatalf("NewAnalyzer() err = %v", err)
}

packageDir := filepath.Join(analysistest.TestData(), "src/block/")
_ = analysistest.Run(t, packageDir, a)
})

t.Run("comment style starred block", func(t *testing.T) {
t.Parallel()
cfg := Config{
Header: HeaderOpts{
Template: "Copyright (c) {{.year}} {{.author}}",
Author: "Test",
YearMode: YearModeThisYear,
CommentStyle: CommentStyleStarredBlock,
},
}
a, err := NewAnalyzer(cfg)
if err != nil {
t.Fatalf("NewAnalyzer() err = %v", err)
}

packageDir := filepath.Join(analysistest.TestData(), "src/starred-block/")
_ = analysistest.Run(t, packageDir, a)
})
}

func TestNewAnalyzer(t *testing.T) {
Expand Down
2 changes: 1 addition & 1 deletion cmd/golicenser/golicenser.go
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ func init() {
flagSet.StringVar(&yearModeStr, "year-mode", golicenser.YearMode(0).String(),
"Year formatting mode (preserve, preserve-this-year-range, preserve-modified-range, this-year, last-modified, git-range, git-modified-years)")
flagSet.StringVar(&commentStyleStr, "comment-style", golicenser.CommentStyle(0).String(),
"Comment style (line, block)")
"Comment style (line, block, starred-block)")
flagSet.StringVar(&exclude, "exclude", "",
"Paths to exclude (doublestar or r!-prefixed regexp, comma-separated)")
flagSet.IntVar(&maxConcurrent, "max-concurrent", DefaultMaxConcurrent,
Expand Down
112 changes: 82 additions & 30 deletions header.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,13 @@ const (
CommentStyleLine CommentStyle = iota

// CommentStyleBlock uses C++-style block comments (/* test */).
// I strongly discourage using this as it is more idiomatic to use
// CommentStyleLine.
CommentStyleBlock

// CommentStyleStarredBlock uses aligned, starred block comments, e.g.
// /*
// * Content
// */
CommentStyleStarredBlock
)

// ParseCommentStyle parses a string representation of a comment style.
Expand All @@ -139,6 +143,8 @@ func ParseCommentStyle(s string) (CommentStyle, error) {
return CommentStyleLine, nil
case CommentStyleBlock.String():
return CommentStyleBlock, nil
case CommentStyleStarredBlock.String():
return CommentStyleStarredBlock, nil
default:
return 0, fmt.Errorf("invalid comment style: %q", s)
}
Expand All @@ -151,23 +157,13 @@ func (cs CommentStyle) String() string {
return "line"
case CommentStyleBlock:
return "block"
case CommentStyleStarredBlock:
return "starred-block"
default:
return ""
}
}

// detectCommentStyle attempts to detect the comment style from a comment.
func detectCommentStyle(s string) (CommentStyle, error) {
switch {
case strings.HasPrefix(s, "// "):
return CommentStyleLine, nil
case strings.HasPrefix(s, "/*\n"):
return CommentStyleBlock, nil
default:
return 0, fmt.Errorf("not a comment: %q", s)
}
}

// Render renders the string into a comment.
func (cs CommentStyle) Render(s string) string {
switch cs {
Expand All @@ -184,33 +180,90 @@ func (cs CommentStyle) Render(s string) string {
return b.String()
case CommentStyleBlock:
return "/*\n" + s + "\n*/\n"
case CommentStyleStarredBlock:
var b bytes.Buffer
b.WriteString("/*\n")
for _, l := range strings.Split(s, "\n") {
b.WriteString(" *")
if l != "" {
b.WriteRune(' ')
b.WriteString(l)
}
b.WriteRune('\n')
}
b.WriteString(" */\n")
return b.String()
default:
// Cannot render as a comment.
return s
}
}

// Parse parses the comment and returns the uncommented string.
func (cs CommentStyle) Parse(s string) string {
switch cs {
case CommentStyleLine:
// parseComment parses a comment and returns the comment content and detected
// comment style. An error will be returned if the comment cannot be parsed.
func parseComment(s string) (string, CommentStyle, error) {
s = strings.TrimSpace(s)
if len(s) < 2 {
return "", 0, fmt.Errorf("invalid comment: %q", s)
}

switch {
case s[0] == '/' && s[1] == '/':
var b bytes.Buffer
for i, l := range strings.Split(strings.TrimSuffix(s, "\n"), "\n") {
for i, l := range strings.Split(s, "\n") {
if i != 0 {
b.WriteRune('\n')
}
l = strings.TrimPrefix(l, "//")
l = l[2:]
if len(l) > 1 && l[0] == ' ' {
l = l[1:]
}
b.WriteString(l)
}
return b.String()
case CommentStyleBlock:
return strings.TrimSuffix(strings.TrimPrefix(s, "/*\n"), "\n*/\n")
return b.String(), CommentStyleLine, nil
case strings.HasPrefix(s, "/*") && strings.HasSuffix(s, "*/"):
s = strings.TrimSpace(s[2 : len(s)-2])
if len(s) < 2 {
return s, CommentStyleBlock, nil
}

var b bytes.Buffer
var starred bool
lines := strings.Split(s, "\n")
if l := lines[0]; len(l) > 0 {
if l[0] == '*' {
l = l[1:]
if len(l) > 0 && l[0] == ' ' {
l = l[1:]
}
starred = true
}
b.WriteString(l)
}

for _, l := range lines[1:] {
b.WriteRune('\n')
if strings.HasPrefix(l, " *") {
starred = true
if len(l) > 2 && l[2] == ' ' {
b.WriteString(l[3:])
continue
}
b.WriteString(l[2:])
continue
}

// Not a starred block comment, fallback to block and just return
// the raw comment content.
return s, CommentStyleBlock, nil
}

if !starred {
return b.String(), CommentStyleBlock, nil
}
return b.String(), CommentStyleStarredBlock, nil
default:
// Cannot parse as a comment.
return s
return "", 0, fmt.Errorf("cannot detect comment type: %q", s)
}
}

Expand Down Expand Up @@ -341,9 +394,9 @@ func (h *Header) Create(filename string) (string, error) {

// Update updates an existing license header if it matches the
func (h *Header) Update(filename, header string) (string, bool, error) {
cs, err := detectCommentStyle(header)
if err == nil {
header = cs.Parse(header)
header, cs, err := parseComment(header)
if err != nil {
return "", false, fmt.Errorf("parse header comment: %w", err)
Copy link
Owner Author

@joshuasing joshuasing Apr 20, 2025

Choose a reason for hiding this comment

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

We should ideally never fail to lint ever, so instead of failing here, if we can't parse the comment I think it makes sense to either ignore the file or generate a new header instead.

The only reason to fail should be bad user input or an error from a template, I think.

}
match := h.matcher.FindStringSubmatch(header)
if match == nil {
Expand Down Expand Up @@ -411,8 +464,7 @@ func (h *Header) Update(filename, header string) (string, bool, error) {
if err != nil {
return "", false, fmt.Errorf("render header: %w", err)
}
modified := newHeader != header || cs != h.commentStyle

modified := newHeader != header || h.commentStyle != cs
return h.commentStyle.Render(newHeader), modified, nil
}

Expand Down
Loading