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
40 changes: 32 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,15 @@ addlicense requires go 1.16 or later.

addlicense [flags] pattern [pattern ...]

-c copyright holder (default "Google LLC")
-check check only mode: verify presence of license headers and exit with non-zero code if missing
-f license file
-ignore file patterns to ignore, for example: -ignore **/*.go -ignore vendor/**
-l license type: apache, bsd, mit, mpl (default "apache")
-s Include SPDX identifier in license header. Set -s=only to only include SPDX identifier.
-v verbose mode: print the name of the files that are modified
-y copyright year(s) (default is the current year)
-c copyright holder (default "Google LLC")
-check check only mode: verify presence of license headers and exit with non-zero code if missing
-comment-style override the comment style for a file extension, for example: -comment-style h://,ts:docblock
-f license file
-ignore file patterns to ignore, for example: -ignore **/*.go -ignore vendor/**
-l license type: apache, bsd, mit, mpl (default "apache")
-s Include SPDX identifier in license header. Set -s=only to only include SPDX identifier.
-v verbose mode: print the name of the files that are modified
-y copyright year(s) (default is the current year)

The pattern argument can be provided multiple times, and may also refer
to single files. Directories are processed recursively.
Expand All @@ -36,6 +37,29 @@ all subdirectories:
The `-ignore` flag can use any pattern [supported by
doublestar](https://github.com/bmatcuk/doublestar#patterns).

The `-comment-style` flag can be used to override the default comment style for one or more supported file extensions. For example:

addlicense -comment-style h://,ts:docblock .

Here `.h` files get `//` line comments, and `.ts` files get a `/** */` block.

The available styles are:

| Style | Renders as |
| ------------ | ------------------- |
| `//` | `// text` |
| `#` | `# text` |
| `;;` | `;; text` |
| `%` | `% text` |
| `--` | `-- text` |
| `vim` | `" text` |
| `block` | `/*` … ` * text` … ` */` |
| `docblock` | `/**` … ` * text` … ` */` |
| `html` | `<!-- text -->` |
| `jinja` | `{# text #}` |
| `ocaml` | `(** text *)` |
| `powershell` | `<# text #>` |

## Running in a Docker Container

The simplest way to get the addlicense docker image is to pull from GitHub
Expand Down
149 changes: 132 additions & 17 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ var (
skipExtensionFlags stringSlice
ignorePatterns stringSlice
spdx spdxFlag
commentOverrides commentStyleFlag

holder = flag.String("c", "Google LLC", "copyright holder")
license = flag.String("l", "apache", "license type: apache, bsd, mit, mpl")
Expand All @@ -70,6 +71,7 @@ func init() {
flag.Var(&skipExtensionFlags, "skip", "[deprecated: see -ignore] file extensions to skip, for example: -skip rb -skip go")
flag.Var(&ignorePatterns, "ignore", "file patterns to ignore, for example: -ignore **/*.go -ignore vendor/**")
flag.Var(&spdx, "s", "Include SPDX identifier in license header. Set -s=only to only include SPDX identifier.")
flag.Var(&commentOverrides, "comment-style", "override the comment style for a file extension, as comma-separated ext:style pairs, for example: -comment-style h://,ts:docblock")
}

// stringSlice stores the results of a repeated command line flag as a string slice.
Expand Down Expand Up @@ -107,6 +109,97 @@ func (i *spdxFlag) Set(value string) error {
return nil
}

// commentStyleFlag accumulates -comment-style overrides as a map of normalized
// file extension to comment style label.
type commentStyleFlag map[string]commentStyle

func (i *commentStyleFlag) String() string {
if len(*i) == 0 {
return ""
}
v := ""
for ext, style := range *i {
v += fmt.Sprintf("%s:%s,", ext, style)
}
return v[:len(v)-1]
}

// Set parses one -comment-style value, a comma-separated list of "ext:style"
// pairs, and records each override. It may be called more than once when the
// flag is repeated.
func (i *commentStyleFlag) Set(value string) error {
if *i == nil {
*i = commentStyleFlag{}
}
for _, entry := range strings.Split(value, ",") {
parts := strings.SplitN(entry, ":", 2)
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
return fmt.Errorf("error: flag 'comment-style' expects \"ext:style[,ext:style]\", got %q", entry)
}
ext := strings.ToLower(parts[0])
style := parts[1]

if !strings.HasPrefix(ext, ".") {
// normalize extension to start with a dot, for example "go" becomes ".go"
// this matches the behavior of fileExtension()
ext = "." + ext
}

(*i)[ext] = commentStyle(style)
}
return nil
}

// commentStyle identifies a comment formatting style by label. Its string value
// is what users pass to -comment-style.
type commentStyle string

const (
styleDoubleSlash commentStyle = "//"
styleSingleHash commentStyle = "#"
styleDoubleSemicolon commentStyle = ";;"
styleSinglePercent commentStyle = "%"
styleDoubleDash commentStyle = "--"
styleVim commentStyle = "vim" // Vimscript: leading "
styleBlock commentStyle = "block" // /* * */
styleDocBlock commentStyle = "docblock" // /** * */
styleHTML commentStyle = "html"
styleJinja commentStyle = "jinja"
styleOCaml commentStyle = "ocaml"
stylePowerShell commentStyle = "powershell"
)

func (cs *commentStyle) affixes() (top string, mid string, bot string) {
switch *cs {
case styleDoubleSlash:
return "", "// ", ""
case styleSingleHash:
return "", "# ", ""
case styleDoubleSemicolon:
return "", ";; ", ""
case styleSinglePercent:
return "", "% ", ""
case styleDoubleDash:
return "", "-- ", ""
case styleVim:
return "", `" `, ""
case styleBlock:
return "/*", " * ", " */"
case styleDocBlock:
return "/**", " * ", " */"
case styleHTML:
return "<!--", " ", "-->"
case styleJinja:
return "{#", "", "#}"
case styleOCaml:
return "(**", " ", "*)"
case stylePowerShell:
return "<#", " ", "#>"
default:
return "", "", ""
}
}

func main() {
flag.Parse()
if flag.NArg() == 0 {
Expand All @@ -125,6 +218,13 @@ func main() {
}
}

// verify that all -comment-style overrides are valid
for _, label := range commentOverrides {
if top, mid, bot := label.affixes(); top == "" && mid == "" && bot == "" {
log.Fatalf("-comment-style %q is not valid", label)
}
}

// map legacy license values
if t, ok := legacyLicenseTypes[*license]; ok {
*license = t
Expand Down Expand Up @@ -285,25 +385,30 @@ func fileHasLicense(path string) (bool, error) {
// licenseHeader populates the provided license template with data, and returns
// it with the proper prefix for the file type specified by path. The file does
// not need to actually exist, only its name is used to determine the prefix.
//
// The comment style is the one set via -comment-style for the file's extension,
// or the built-in default from defaultStyleLabel. A nil result with a nil error
// means the file type is unrecognized and should be skipped.
func licenseHeader(path string, tmpl *template.Template, data licenseData) ([]byte, error) {
var lic []byte
var err error
base := strings.ToLower(filepath.Base(path))
ext := fileExtension(base)

var style commentStyle

// When adding an extension, also add it to TestLicenseHeader in main_test.go
switch fileExtension(base) {
switch ext {
case
".c", ".h",
".gv",
".java",
".kt", ".kts",
".scala":
lic, err = executeTemplate(tmpl, data, "/*", " * ", " */")
style = styleBlock
case
".css", ".scss", ".sass", ".less",
".js", ".mjs", ".cjs", ".jsx",
".ts", ".tsx":
lic, err = executeTemplate(tmpl, data, "/**", " * ", " */")
style = styleDocBlock
case
".cc", ".cpp", ".hh", ".hpp",
".cs",
Expand All @@ -318,7 +423,7 @@ func licenseHeader(path string, tmpl *template.Template, data licenseData) ([]by
".rs",
".swift",
".v", ".sv":
lic, err = executeTemplate(tmpl, data, "", "// ", "")
style = styleDoubleSlash
case
".awk",
".buckconfig", "buck",
Expand All @@ -339,41 +444,51 @@ func licenseHeader(path string, tmpl *template.Template, data licenseData) ([]by
".tf",
".toml",
".yaml", ".yml":
lic, err = executeTemplate(tmpl, data, "", "# ", "")
style = styleSingleHash
case
".el",
".lisp",
".scm":
lic, err = executeTemplate(tmpl, data, "", ";; ", "")
style = styleDoubleSemicolon
case ".erl":
lic, err = executeTemplate(tmpl, data, "", "% ", "")
style = styleSinglePercent
case
".hs",
".lua",
".sql", ".sdl":
lic, err = executeTemplate(tmpl, data, "", "-- ", "")
style = styleDoubleDash
case
".html", ".htm",
".vue",
".svelte",
".wxi", ".wxl", ".wxs",
".xml":
lic, err = executeTemplate(tmpl, data, "<!--", " ", "-->")
style = styleHTML
case ".j2", ".jinja2", ".jinja":
lic, err = executeTemplate(tmpl, data, "{#", "", "#}")
style = styleJinja
case ".ml", ".mli", ".mll", ".mly":
lic, err = executeTemplate(tmpl, data, "(**", " ", "*)")
style = styleOCaml
case ".ps1", ".psm1":
lic, err = executeTemplate(tmpl, data, "<#", " ", "#>")
style = stylePowerShell
case ".vim":
lic, err = executeTemplate(tmpl, data, "", `" `, "")
style = styleVim
default:
// handle various cmake files
if base == "cmakelists.txt" || strings.HasSuffix(base, ".cmake.in") || strings.HasSuffix(base, ".cmake") {
lic, err = executeTemplate(tmpl, data, "", "# ", "")
style = styleSingleHash
} else {
// unknown file type, skip
return nil, nil
}
}
return lic, err

if userStyle, ok := commentOverrides[ext]; ok {
style = userStyle
}

top, mid, bot := style.affixes()

return executeTemplate(tmpl, data, top, mid, bot)
}

// fileExtension returns the file extension of name, or the full name if there
Expand Down
96 changes: 96 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import (
"os"
"os/exec"
"path/filepath"
"reflect"
"strings"
"testing"
"text/template"
Expand Down Expand Up @@ -555,3 +556,98 @@ func TestFileMatches(t *testing.T) {
}
}
}

// Test that -comment-style overrides change the comment style for an extension,
// while non-overridden extensions keep their built-in defaults.
func TestLicenseHeaderOverrides(t *testing.T) {
tpl := template.Must(template.New("").Parse("{{.Holder}}{{.Year}}{{.SPDXID}}"))
data := licenseData{Holder: "H", Year: "Y", SPDXID: "S"}

commentOverrides = commentStyleFlag{
".h": styleDoubleSlash, // default is single-star block
".ts": styleBlock, // default is docblock
".go": styleDocBlock, // default is //
}
defer func() { commentOverrides = nil }()

tests := []struct {
path string
want string
}{
{"f.h", "// HYS\n\n"},
{"f.ts", "/*\n * HYS\n */\n\n"},
{"f.go", "/**\n * HYS\n */\n\n"},
{"f.c", "/*\n * HYS\n */\n\n"}, // not overridden: default block style
}
for _, tt := range tests {
header, _ := licenseHeader(tt.path, tpl, data)
if got := string(header); got != tt.want {
t.Errorf("licenseHeader(%q) with overrides returned: %q, want: %q", tt.path, got, tt.want)
}
}
}

// Test parsing of the -comment-style flag value.
func TestCommentStyleFlagSet(t *testing.T) {
tests := []struct {
name string
values []string // each element is one -comment-style flag occurrence
want map[string]commentStyle
wantErr bool
}{
{"single line style", []string{"h://"}, map[string]commentStyle{".h": "//"}, false},
{"comma separated", []string{"h://,ts:docblock"}, map[string]commentStyle{".h": "//", ".ts": "docblock"}, false},
{"repeated flag accumulates", []string{"h://", "py:#"}, map[string]commentStyle{".h": "//", ".py": "#"}, false},
{"missing colon", []string{"hslashes"}, nil, true},
{"empty extension", []string{":#"}, nil, true},
{"empty style", []string{"h:"}, nil, true},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
c := commentStyleFlag{}
var err error
for _, v := range tt.values {
if err = c.Set(v); err != nil {
break
}
}
if tt.wantErr {
if err == nil {
t.Fatalf("Set(%v) expected error, got nil (result %v)", tt.values, c)
}
return
}
if err != nil {
t.Fatalf("Set(%v) unexpected error: %v", tt.values, err)
}
if !reflect.DeepEqual(map[string]commentStyle(c), tt.want) {
t.Errorf("Set(%v) = %v, want %v", tt.values, map[string]commentStyle(c), tt.want)
}
})
}
}

// Test that the -comment-style flag changes the emitted header end-to-end.
func TestCommentStyleOverride(t *testing.T) {
if os.Getenv("RUNME") != "" {
main()
return
}

tmp := tempDir(t)
t.Logf("tmp dir: %s", tmp)
samplefile := filepath.Join(tmp, "file.c")
const sampleLicensed = "testdata/comment_style_file.c"

run(t, "cp", "testdata/initial/file.c", samplefile)
cmd := exec.Command(os.Args[0],
"-test.run=TestCommentStyleOverride",
"-l", "apache", "-c", "Google LLC", "-y", "2018",
"-comment-style", "c://", samplefile,
)
cmd.Env = []string{"RUNME=1"}
if out, err := cmd.CombinedOutput(); err != nil {
t.Fatalf("%v\n%s", err, out)
}
run(t, "diff", samplefile, sampleLicensed)
}
Loading