Skip to content

Commit fd23627

Browse files
authored
header: add ability to provide regexps for variables (#6)
1 parent 6180759 commit fd23627

File tree

4 files changed

+395
-35
lines changed

4 files changed

+395
-35
lines changed

README.md

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,8 @@ Flags:
8080
display offending line with this many lines of context (default -1)
8181
-comment-style string
8282
Comment style (line, block) (default "line")
83+
-copyright-header-matcher string
84+
Copyright header matcher regexp (used to detect existence of any copyright header) (default "(?i)copyright")
8385
-cpuprofile string
8486
write CPU profile to this file
8587
-debug string
@@ -94,14 +96,12 @@ Flags:
9496
print analyzer flags in JSON
9597
-json
9698
emit JSON output
97-
-match-header-regexp string
98-
Match header regexp (used to detect any copyright headers) (default "(?i)copyright")
99-
-match-tmpl string
100-
Match license header template
101-
-match-tmpl-file string
102-
Match license header template file (used to detect existing license headers which may be updated)
103-
-match-tmpl-regexp
104-
Whether the provided match template is a regexp expression
99+
-matcher string
100+
License header matcher (This is template, when executed it must become valid regexp)
101+
-matcher-escape
102+
Whether to regexp-escape the matcher
103+
-matcher-file string
104+
License header matcher file)
105105
-max-concurrent int
106106
Maximum concurrent processes to use when processing files (default 32)
107107
-memprofile string
@@ -121,6 +121,8 @@ Flags:
121121
-v no effect (deprecated)
122122
-var string
123123
Template variables (e.g. a=Hello,b=Test)
124+
-var-regexp string
125+
Template variable regexps (e.g. 'a=(Hello|World),b=(?i)test'
124126
-year-mode string
125127
Year formatting mode (preserve, preserve-this-year-range, preserve-modified-range, this-year, last-modified, git-range, git-modified-years) (default "preserve")
126128
```

cmd/golicenser/golicenser.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ var (
4444
author string
4545
authorRegexp string
4646
variables string
47+
variableRegexps string
4748
yearModeStr string
4849
commentStyleStr string
4950
exclude string
@@ -65,6 +66,8 @@ func init() {
6566
flagSet.StringVar(&authorRegexp, "author-regexp", "",
6667
"Regexp to match copyright author (default: match author)")
6768
flagSet.StringVar(&variables, "var", "", "Template variables (e.g. a=Hello,b=Test)")
69+
flagSet.StringVar(&variableRegexps, "var-regexp", "",
70+
"Template variable regexps (e.g. 'a=(Hello|World),b=(?i)test'")
6871
flagSet.StringVar(&yearModeStr, "year-mode", golicenser.YearMode(0).String(),
6972
"Year formatting mode (preserve, preserve-this-year-range, preserve-modified-range, this-year, last-modified, git-range, git-modified-years)")
7073
flagSet.StringVar(&commentStyleStr, "comment-style", golicenser.CommentStyle(0).String(),
@@ -101,7 +104,7 @@ var analyzer = &analysis.Analyzer{
101104
//nolint:gosec // Reading user-defined file.
102105
b, err := os.ReadFile(matcherFile)
103106
if err != nil {
104-
log.Fatal("read match template file: ", err)
107+
log.Fatal("read matcher file: ", err)
105108
}
106109
matcher = string(b)
107110
} else {
@@ -111,14 +114,27 @@ var analyzer = &analysis.Analyzer{
111114
}
112115

113116
// Parse variables
114-
vars := make(map[string]any)
117+
vars := make(map[string]golicenser.Var)
115118
if variables != "" {
116119
for _, v := range strings.Split(variables, ",") {
117120
parts := strings.SplitN(v, "=", 2)
118121
if len(parts) != 2 {
119122
log.Fatal("invalid variable: ", v)
120123
}
121-
vars[parts[0]] = parts[1]
124+
vars[parts[0]] = golicenser.Var{Value: parts[1]}
125+
}
126+
}
127+
if variableRegexps != "" {
128+
for _, v := range strings.Split(variableRegexps, ",") {
129+
parts := strings.SplitN(v, "=", 2)
130+
if len(parts) != 2 {
131+
log.Fatal("invalid variable: ", v)
132+
}
133+
va, ok := vars[parts[0]]
134+
if !ok {
135+
log.Fatal("regexp for non-existent variable: ", v)
136+
}
137+
va.Regexp = parts[1]
122138
}
123139
}
124140

header.go

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,6 @@ import (
2424
"bytes"
2525
"fmt"
2626
"io"
27-
"maps"
2827
"path/filepath"
2928
"regexp"
3029
"strings"
@@ -221,7 +220,7 @@ type Header struct {
221220
matcher *regexp.Regexp
222221

223222
author string
224-
variables map[string]any
223+
variables map[string]Var
225224
yearMode YearMode
226225
commentStyle CommentStyle
227226
}
@@ -230,14 +229,24 @@ var tmplFuncMap = template.FuncMap{
230229
"basename": filepath.Base,
231230
}
232231

232+
// Var is a template variable.
233+
type Var struct {
234+
// Value is the variable value.
235+
Value string
236+
237+
// Regexp is a regexp used to match the variable value.
238+
// If empty, the regexp-escaped value of Value will be used.
239+
Regexp string
240+
}
241+
233242
// HeaderOpts are the options for creating a license header.
234243
type HeaderOpts struct {
235244
Template string
236245
Matcher string
237246
MatcherEscape bool
238247
Author string
239248
AuthorRegexp string
240-
Variables map[string]any
249+
Variables map[string]Var
241250
YearMode YearMode
242251
CommentStyle CommentStyle
243252
}
@@ -264,11 +273,23 @@ func NewHeader(opts HeaderOpts) (*Header, error) {
264273
"filename": "test",
265274
"year": "2025",
266275
}
267-
maps.Copy(m, opts.Variables)
276+
addVariables(m, opts.Variables)
268277
if err = t.Execute(io.Discard, m); err != nil {
269278
return nil, fmt.Errorf("execute template: %w", err)
270279
}
271280

281+
// Test compiling variable regexps.
282+
for name, v := range opts.Variables {
283+
switch v.Regexp {
284+
case "":
285+
v.Regexp = regexp.QuoteMeta(v.Value)
286+
default:
287+
if _, err = regexp.Compile(v.Regexp); err != nil {
288+
return nil, fmt.Errorf("compile %q regexp: %w", name, err)
289+
}
290+
}
291+
}
292+
272293
// Create author regexp
273294
authorRegexpStr := opts.AuthorRegexp
274295
if authorRegexpStr == "" {
@@ -284,15 +305,15 @@ func NewHeader(opts HeaderOpts) (*Header, error) {
284305
mt, err := template.New("").Funcs(tmplFuncMap).
285306
Option("missingkey=error").Parse(opts.Matcher)
286307
if err != nil {
287-
return nil, fmt.Errorf("new match template: %w", err)
308+
return nil, fmt.Errorf("new matcher template: %w", err)
288309
}
289310
matcher, err = headerMatcher(mt, opts.MatcherEscape, authorRegexp, opts.Variables)
290311
if err != nil {
291-
return nil, fmt.Errorf("create header matcher (with match template): %w", err)
312+
return nil, fmt.Errorf("create header matcher: %w", err)
292313
}
293314
} else {
294-
// If a match template wasn't provided, create a matcher using the
295-
// header template (regexp-escaped).
315+
// If a matcher wasn't provided, create a matcher using the header
316+
// template (regexp-escaped).
296317
matcher, err = headerMatcher(t, true, authorRegexp, opts.Variables)
297318
if err != nil {
298319
return nil, fmt.Errorf("create header matcher: %w", err)
@@ -399,7 +420,7 @@ func (h *Header) render(filename, year string) (string, error) {
399420
"filename": filename,
400421
"year": year,
401422
}
402-
maps.Copy(m, h.variables)
423+
addVariables(m, h.variables)
403424

404425
var b bytes.Buffer
405426
if err := h.tmpl.Execute(&b, m); err != nil {
@@ -408,32 +429,47 @@ func (h *Header) render(filename, year string) (string, error) {
408429
return b.String(), nil
409430
}
410431

411-
func headerMatcher(tmpl *template.Template, escapeTmpl bool, authorRegexp *regexp.Regexp, variables map[string]any) (*regexp.Regexp, error) {
412-
m := map[string]any{
413-
"author": "__AUTHOR__",
414-
"filename": "__FILENAME__",
415-
"year": "__YEAR__",
432+
func headerMatcher(tmpl *template.Template, escapeTmpl bool, authorRegexp *regexp.Regexp, variables map[string]Var) (*regexp.Regexp, error) {
433+
m := map[string]string{
434+
"author": "__VAR_author__",
435+
"filename": "__VAR_filename__",
436+
"year": "__VAR_year__",
437+
}
438+
regexps := map[string]string{
439+
"author": authorRegexp.String(),
440+
"filename": "(?P<filename>.+)",
441+
"year": regexpYears.String(),
442+
}
443+
for k, v := range variables {
444+
m[k] = "__VAR_" + k + "__"
445+
regexps[k] = v.Regexp
416446
}
417-
maps.Copy(m, variables)
418447

419448
// Execute matcher template.
420449
var b bytes.Buffer
421450
if err := tmpl.Execute(&b, m); err != nil {
422451
return nil, fmt.Errorf("execute template: %w", err)
423452
}
424453
headerExpr := b.String()
454+
455+
// Optionally escape the rendered template.
425456
if escapeTmpl {
426457
headerExpr = regexp.QuoteMeta(b.String())
427458
}
428459

429-
// Insert regexp matchers here, as there is a chance we escape the
430-
// headerExpr above.
431-
expr := strings.NewReplacer(
432-
"__AUTHOR__", authorRegexp.String(),
433-
"__FILENAME__", "(?P<filename>.+)",
434-
"__YEAR__", regexpYears.String(),
435-
).Replace(headerExpr)
460+
// Replace variable placeholders with regexp expressions.
461+
replacements := make([]string, 0, len(m)*2)
462+
for k, v := range m {
463+
replacements = append(replacements, v, regexps[k])
464+
}
465+
headerExpr = strings.NewReplacer(replacements...).Replace(headerExpr)
466+
467+
// Compile header matcher regexp.
468+
return regexp.Compile(headerExpr)
469+
}
436470

437-
// Compile header matching regexp.
438-
return regexp.Compile(expr)
471+
func addVariables(m map[string]any, vars map[string]Var) {
472+
for k, v := range vars {
473+
m[k] = v.Value
474+
}
439475
}

0 commit comments

Comments
 (0)