Skip to content

Commit 8746277

Browse files
authored
Merge pull request #79 from willnorris/spdx
Add support for SPDX style headers
2 parents 6d92264 + 97ae522 commit 8746277

File tree

4 files changed

+262
-48
lines changed

4 files changed

+262
-48
lines changed

main.go

+61-37
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ Flags:
4949

5050
var (
5151
skipExtensionFlags skipExtensionFlag
52+
spdx spdxFlag
5253

5354
holder = flag.String("c", "Google LLC", "copyright holder")
5455
license = flag.String("l", "apache", "license type: apache, bsd, mit, mpl")
@@ -58,6 +59,15 @@ var (
5859
checkonly = flag.Bool("check", false, "check only mode: verify presence of license headers and exit with non-zero code if missing")
5960
)
6061

62+
func init() {
63+
flag.Usage = func() {
64+
fmt.Fprintln(os.Stderr, helpText)
65+
flag.PrintDefaults()
66+
}
67+
flag.Var(&skipExtensionFlags, "skip", "To skip files to check/add the header file, for example: -skip rb -skip go")
68+
flag.Var(&spdx, "s", "Include SPDX identifier in license header. Set -s=only to only include SPDX identifier.")
69+
}
70+
6171
type skipExtensionFlag []string
6272

6373
func (i *skipExtensionFlag) String() string {
@@ -69,41 +79,54 @@ func (i *skipExtensionFlag) Set(value string) error {
6979
return nil
7080
}
7181

72-
func main() {
73-
flag.Usage = func() {
74-
fmt.Fprintln(os.Stderr, helpText)
75-
flag.PrintDefaults()
82+
// spdxFlag defines the line flag behavior for specifying SPDX support.
83+
type spdxFlag string
84+
85+
const (
86+
spdxOff spdxFlag = ""
87+
spdxOn spdxFlag = "true" // value set by flag package on bool flag
88+
spdxOnly spdxFlag = "only"
89+
)
90+
91+
// IsBoolFlag causes a bare '-s' flag to be set as the string 'true'. This
92+
// allows the use of the bare '-s' or setting a string '-s=only'.
93+
func (i *spdxFlag) IsBoolFlag() bool { return true }
94+
func (i *spdxFlag) String() string { return string(*i) }
95+
96+
func (i *spdxFlag) Set(value string) error {
97+
v := spdxFlag(value)
98+
if v != spdxOn && v != spdxOnly {
99+
return fmt.Errorf("error: flag 's' expects '%v' or '%v'", spdxOn, spdxOnly)
76100
}
77-
flag.Var(&skipExtensionFlags, "skip", "To skip files to check/add the header file, for example: -skip rb -skip go")
101+
*i = v
102+
return nil
103+
}
104+
105+
func main() {
78106
flag.Parse()
79107
if flag.NArg() == 0 {
80108
flag.Usage()
81109
os.Exit(1)
82110
}
83111

84-
data := &copyrightData{
112+
// map legacy license values
113+
if t, ok := legacyLicenseTypes[*license]; ok {
114+
*license = t
115+
}
116+
117+
data := licenseData{
85118
Year: *year,
86119
Holder: *holder,
120+
SPDXID: *license,
87121
}
88122

89-
var t *template.Template
90-
if *licensef != "" {
91-
d, err := ioutil.ReadFile(*licensef)
92-
if err != nil {
93-
log.Printf("license file: %v", err)
94-
os.Exit(1)
95-
}
96-
t, err = template.New("").Parse(string(d))
97-
if err != nil {
98-
log.Printf("license file: %v", err)
99-
os.Exit(1)
100-
}
101-
} else {
102-
t = licenseTemplate[*license]
103-
if t == nil {
104-
log.Printf("unknown license: %s", *license)
105-
os.Exit(1)
106-
}
123+
tpl, err := fetchTemplate(*license, *licensef, spdx)
124+
if err != nil {
125+
log.Fatal(err)
126+
}
127+
t, err := template.New("").Parse(tpl)
128+
if err != nil {
129+
log.Fatal(err)
107130
}
108131

109132
// process at most 1000 files in parallel
@@ -189,7 +212,7 @@ func walk(ch chan<- *file, start string) {
189212
// addLicense add a license to the file if missing.
190213
//
191214
// It returns true if the file was updated.
192-
func addLicense(path string, fmode os.FileMode, tmpl *template.Template, data *copyrightData) (bool, error) {
215+
func addLicense(path string, fmode os.FileMode, tmpl *template.Template, data licenseData) (bool, error) {
193216
var lic []byte
194217
var err error
195218
lic, err = licenseHeader(path, tmpl, data)
@@ -227,32 +250,32 @@ func fileHasLicense(path string) (bool, error) {
227250
return hasLicense(b) || isGenerated(b), nil
228251
}
229252

230-
func licenseHeader(path string, tmpl *template.Template, data *copyrightData) ([]byte, error) {
253+
func licenseHeader(path string, tmpl *template.Template, data licenseData) ([]byte, error) {
231254
var lic []byte
232255
var err error
233256
switch fileExtension(path) {
234257
default:
235258
return nil, nil
236259
case ".c", ".h", ".gv":
237-
lic, err = prefix(tmpl, data, "/*", " * ", " */")
260+
lic, err = executeTemplate(tmpl, data, "/*", " * ", " */")
238261
case ".js", ".mjs", ".cjs", ".jsx", ".tsx", ".css", ".scss", ".sass", ".tf", ".ts":
239-
lic, err = prefix(tmpl, data, "/**", " * ", " */")
262+
lic, err = executeTemplate(tmpl, data, "/**", " * ", " */")
240263
case ".cc", ".cpp", ".cs", ".go", ".hh", ".hpp", ".java", ".m", ".mm", ".proto", ".rs", ".scala", ".swift", ".dart", ".groovy", ".kt", ".kts", ".v", ".sv":
241-
lic, err = prefix(tmpl, data, "", "// ", "")
264+
lic, err = executeTemplate(tmpl, data, "", "// ", "")
242265
case ".py", ".sh", ".yaml", ".yml", ".dockerfile", "dockerfile", ".rb", "gemfile", ".tcl", ".bzl":
243-
lic, err = prefix(tmpl, data, "", "# ", "")
266+
lic, err = executeTemplate(tmpl, data, "", "# ", "")
244267
case ".el", ".lisp":
245-
lic, err = prefix(tmpl, data, "", ";; ", "")
268+
lic, err = executeTemplate(tmpl, data, "", ";; ", "")
246269
case ".erl":
247-
lic, err = prefix(tmpl, data, "", "% ", "")
270+
lic, err = executeTemplate(tmpl, data, "", "% ", "")
248271
case ".hs", ".sql", ".sdl":
249-
lic, err = prefix(tmpl, data, "", "-- ", "")
272+
lic, err = executeTemplate(tmpl, data, "", "-- ", "")
250273
case ".html", ".xml", ".vue":
251-
lic, err = prefix(tmpl, data, "<!--", " ", "-->")
274+
lic, err = executeTemplate(tmpl, data, "<!--", " ", "-->")
252275
case ".php":
253-
lic, err = prefix(tmpl, data, "", "// ", "")
276+
lic, err = executeTemplate(tmpl, data, "", "// ", "")
254277
case ".ml", ".mli", ".mll", ".mly":
255-
lic, err = prefix(tmpl, data, "(**", " ", "*)")
278+
lic, err = executeTemplate(tmpl, data, "(**", " ", "*)")
256279
}
257280
return lic, err
258281
}
@@ -308,5 +331,6 @@ func hasLicense(b []byte) bool {
308331
n = len(b)
309332
}
310333
return bytes.Contains(bytes.ToLower(b[:n]), []byte("copyright")) ||
311-
bytes.Contains(bytes.ToLower(b[:n]), []byte("mozilla public"))
334+
bytes.Contains(bytes.ToLower(b[:n]), []byte("mozilla public")) ||
335+
bytes.Contains(bytes.ToLower(b[:n]), []byte("SPDX-License-Identifier"))
312336
}

testdata/custom.tpl

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Copyright {{.Year}} {{.Holder}}
2+
3+
Custom License Template

tmpl.go

+58-11
Original file line numberDiff line numberDiff line change
@@ -19,27 +19,69 @@ import (
1919
"bytes"
2020
"fmt"
2121
"html/template"
22+
"io/ioutil"
2223
"strings"
2324
"unicode"
2425
)
2526

26-
var licenseTemplate = make(map[string]*template.Template)
27+
var licenseTemplate = map[string]string{
28+
"Apache-2.0": tmplApache,
29+
"MIT": tmplMIT,
30+
"bsd": tmplBSD,
31+
"MPL-2.0": tmplMPL,
32+
}
2733

28-
func init() {
29-
licenseTemplate["apache"] = template.Must(template.New("").Parse(tmplApache))
30-
licenseTemplate["mit"] = template.Must(template.New("").Parse(tmplMIT))
31-
licenseTemplate["bsd"] = template.Must(template.New("").Parse(tmplBSD))
32-
licenseTemplate["mpl"] = template.Must(template.New("").Parse(tmplMPL))
34+
// maintain backwards compatibility by mapping legacy license types to their
35+
// SPDX equivalents.
36+
var legacyLicenseTypes = map[string]string{
37+
"apache": "Apache-2.0",
38+
"mit": "MIT",
39+
"mpl": "MPL-2.0",
3340
}
3441

35-
type copyrightData struct {
36-
Year string
37-
Holder string
42+
// licenseData specifies the data used to fill out a license template.
43+
type licenseData struct {
44+
Year string // Copyright year(s).
45+
Holder string // Name of the copyright holder.
46+
SPDXID string // SPDX Identifier
3847
}
3948

40-
// prefix will execute a license template t with data d
49+
// fetchTemplate returns the license template for the specified license and
50+
// optional templateFile. If templateFile is provided, the license is read
51+
// from the specified file. Otherwise, a template is loaded for the specified
52+
// license, if recognized.
53+
func fetchTemplate(license string, templateFile string, spdx spdxFlag) (string, error) {
54+
var t string
55+
if spdx == spdxOnly {
56+
t = tmplSPDX
57+
} else if templateFile != "" {
58+
d, err := ioutil.ReadFile(templateFile)
59+
if err != nil {
60+
return "", fmt.Errorf("license file: %w", err)
61+
}
62+
63+
t = string(d)
64+
} else {
65+
t = licenseTemplate[license]
66+
if t == "" {
67+
if spdx == spdxOn {
68+
// unknown license, but SPDX headers requested
69+
t = tmplSPDX
70+
} else {
71+
return "", fmt.Errorf("unknown license: %q. Include the '-s' flag to request SPDX style headers using this license.", license)
72+
}
73+
} else if spdx == spdxOn {
74+
// append spdx headers to recognized license
75+
t = t + spdxSuffix
76+
}
77+
}
78+
79+
return t, nil
80+
}
81+
82+
// executeTemplate will execute a license template t with data d
4183
// and prefix the result with top, middle and bottom.
42-
func prefix(t *template.Template, d *copyrightData, top, mid, bot string) ([]byte, error) {
84+
func executeTemplate(t *template.Template, d licenseData, top, mid, bot string) ([]byte, error) {
4385
var buf bytes.Buffer
4486
if err := t.Execute(&buf, d); err != nil {
4587
return nil, err
@@ -99,3 +141,8 @@ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.`
99141
const tmplMPL = `This Source Code Form is subject to the terms of the Mozilla Public
100142
License, v. 2.0. If a copy of the MPL was not distributed with this
101143
file, You can obtain one at https://mozilla.org/MPL/2.0/.`
144+
145+
const tmplSPDX = `{{ if and .Year .Holder }}Copyright {{.Year}} {{.Holder}}
146+
{{ end }}SPDX-License-Identifier: {{.SPDXID}}`
147+
148+
const spdxSuffix = "\n\nSPDX-License-Identifier: {{.SPDXID}}"

tmpl_test.go

+140
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package main
16+
17+
import (
18+
"errors"
19+
"html/template"
20+
"os"
21+
"testing"
22+
)
23+
24+
func init() {
25+
// ensure that pre-defined templates must parse
26+
template.Must(template.New("").Parse(tmplApache))
27+
template.Must(template.New("").Parse(tmplMIT))
28+
template.Must(template.New("").Parse(tmplBSD))
29+
template.Must(template.New("").Parse(tmplMPL))
30+
}
31+
32+
func TestFetchTemplate(t *testing.T) {
33+
tests := []struct {
34+
description string // test case description
35+
license string // license passed to fetchTemplate
36+
templateFile string // templatefile passed to fetchTemplate
37+
spdx spdxFlag // spdx value passed to fetchTemplate
38+
wantTemplate string // expected returned template
39+
wantErr error // expected returned error
40+
}{
41+
// custom template files
42+
{
43+
"non-existant template file",
44+
"",
45+
"/does/not/exist",
46+
spdxOff,
47+
"",
48+
os.ErrNotExist,
49+
},
50+
{
51+
"custom template file",
52+
"",
53+
"testdata/custom.tpl",
54+
spdxOff,
55+
"Copyright {{.Year}} {{.Holder}}\n\nCustom License Template\n",
56+
nil,
57+
},
58+
59+
{
60+
"unknown license",
61+
"unknown",
62+
"",
63+
spdxOff,
64+
"",
65+
errors.New(`unknown license: "unknown". Include the '-s' flag to request SPDX style headers using this license.`),
66+
},
67+
68+
// pre-defined license templates, no SPDX
69+
{
70+
"apache license template",
71+
"Apache-2.0",
72+
"",
73+
spdxOff,
74+
tmplApache,
75+
nil,
76+
},
77+
{
78+
"mit license template",
79+
"MIT",
80+
"",
81+
spdxOff,
82+
tmplMIT,
83+
nil,
84+
},
85+
{
86+
"bsd license template",
87+
"bsd",
88+
"",
89+
spdxOff,
90+
tmplBSD,
91+
nil,
92+
},
93+
{
94+
"mpl license template",
95+
"MPL-2.0",
96+
"",
97+
spdxOff,
98+
tmplMPL,
99+
nil,
100+
},
101+
102+
// SPDX variants
103+
{
104+
"apache license template with SPDX added",
105+
"Apache-2.0",
106+
"",
107+
spdxOn,
108+
tmplApache + spdxSuffix,
109+
nil,
110+
},
111+
{
112+
"apache license template with SPDX only",
113+
"Apache-2.0",
114+
"",
115+
spdxOnly,
116+
tmplSPDX,
117+
nil,
118+
},
119+
{
120+
"unknown license with SPDX only",
121+
"unknown",
122+
"",
123+
spdxOnly,
124+
tmplSPDX,
125+
nil,
126+
},
127+
}
128+
129+
for _, tt := range tests {
130+
t.Run(tt.description, func(t *testing.T) {
131+
tpl, err := fetchTemplate(tt.license, tt.templateFile, tt.spdx)
132+
if tt.wantErr != nil && (err == nil || (!errors.Is(err, tt.wantErr) && err.Error() != tt.wantErr.Error())) {
133+
t.Fatalf("fetchTemplate(%q, %q) returned error: %#v, want %#v", tt.license, tt.templateFile, err, tt.wantErr)
134+
}
135+
if tpl != tt.wantTemplate {
136+
t.Errorf("fetchTemplate(%q, %q) returned template: %q, want %q", tt.license, tt.templateFile, tpl, tt.wantTemplate)
137+
}
138+
})
139+
}
140+
}

0 commit comments

Comments
 (0)