summaryrefslogtreecommitdiff
path: root/vendor/github.com/mmcloughlin/avo/buildtags/buildtags.go
blob: 8fd61e10d30ec8fc5f3581bfdb0723938f0a93df (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
// Package buildtags provides types for representing and manipulating build constraints.
//
// In Go, build constraints are represented as comments in source code together with file naming conventions. For example
//
//	// +build linux,386 darwin,!cgo
//	// +build !purego
//
// Any terms provided in the filename can be thought of as an implicit extra
// constraint comment line. Collectively, these are referred to as
// ``constraints''. Each line is a ``constraint''. Within each constraint the
// space-separated terms are ``options'', and within that the comma-separated
// items are ``terms'' which may be negated with at most one exclaimation mark.
//
// These represent a boolean formulae. The constraints are evaluated as the AND
// of constraint lines; a constraint is evaluated as the OR of its options and
// an option is evaluated as the AND of its terms. Overall build constraints are
// a boolean formula that is an AND of ORs of ANDs.
//
// This level of complexity is rarely used in Go programs. Therefore this
// package aims to provide access to all these layers of nesting if required,
// but make it easy to forget about for basic use cases too.
package buildtags

import (
	"errors"
	"fmt"
	"strings"
	"unicode"
)

// Reference: https://github.com/golang/go/blob/204a8f55dc2e0ac8d27a781dab0da609b98560da/src/go/build/doc.go#L73-L92
//
//	// A build constraint is evaluated as the OR of space-separated options;
//	// each option evaluates as the AND of its comma-separated terms;
//	// and each term is an alphanumeric word or, preceded by !, its negation.
//	// That is, the build constraint:
//	//
//	//	// +build linux,386 darwin,!cgo
//	//
//	// corresponds to the boolean formula:
//	//
//	//	(linux AND 386) OR (darwin AND (NOT cgo))
//	//
//	// A file may have multiple build constraints. The overall constraint is the AND
//	// of the individual constraints. That is, the build constraints:
//	//
//	//	// +build linux darwin
//	//	// +build 386
//	//
//	// corresponds to the boolean formula:
//	//
//	//	(linux OR darwin) AND 386
//

// Interface represents a build constraint.
type Interface interface {
	ConstraintsConvertable
	fmt.GoStringer
	Evaluate(v map[string]bool) bool
	Validate() error
}

// ConstraintsConvertable can be converted to a Constraints object.
type ConstraintsConvertable interface {
	ToConstraints() Constraints
}

// ConstraintConvertable can be converted to a Constraint.
type ConstraintConvertable interface {
	ToConstraint() Constraint
}

// OptionConvertable can be converted to an Option.
type OptionConvertable interface {
	ToOption() Option
}

// Constraints represents the AND of a list of Constraint lines.
type Constraints []Constraint

// And builds Constraints that will be true if all of its constraints are true.
func And(cs ...ConstraintConvertable) Constraints {
	constraints := Constraints{}
	for _, c := range cs {
		constraints = append(constraints, c.ToConstraint())
	}
	return constraints
}

// ToConstraints returns cs.
func (cs Constraints) ToConstraints() Constraints { return cs }

// Validate validates the constraints set.
func (cs Constraints) Validate() error {
	for _, c := range cs {
		if err := c.Validate(); err != nil {
			return err
		}
	}
	return nil
}

// Evaluate the boolean formula represented by cs under the given assignment of
// tag values. This is the AND of the values of the constituent Constraints.
func (cs Constraints) Evaluate(v map[string]bool) bool {
	r := true
	for _, c := range cs {
		r = r && c.Evaluate(v)
	}
	return r
}

// GoString represents Constraints as +build comment lines.
func (cs Constraints) GoString() string {
	s := ""
	for _, c := range cs {
		s += c.GoString()
	}
	return s
}

// Constraint represents the OR of a list of Options.
type Constraint []Option

// Any builds a Constraint that will be true if any of its options are true.
func Any(opts ...OptionConvertable) Constraint {
	c := Constraint{}
	for _, opt := range opts {
		c = append(c, opt.ToOption())
	}
	return c
}

// ParseConstraint parses a space-separated list of options.
func ParseConstraint(expr string) (Constraint, error) {
	c := Constraint{}
	for _, field := range strings.Fields(expr) {
		opt, err := ParseOption(field)
		if err != nil {
			return c, err
		}
		c = append(c, opt)
	}
	return c, nil
}

// ToConstraints returns the list of constraints containing just c.
func (c Constraint) ToConstraints() Constraints { return Constraints{c} }

// ToConstraint returns c.
func (c Constraint) ToConstraint() Constraint { return c }

// Validate validates the constraint.
func (c Constraint) Validate() error {
	for _, o := range c {
		if err := o.Validate(); err != nil {
			return err
		}
	}
	return nil
}

// Evaluate the boolean formula represented by c under the given assignment of
// tag values. This is the OR of the values of the constituent Options.
func (c Constraint) Evaluate(v map[string]bool) bool {
	r := false
	for _, o := range c {
		r = r || o.Evaluate(v)
	}
	return r
}

// GoString represents the Constraint as one +build comment line.
func (c Constraint) GoString() string {
	s := "// +build"
	for _, o := range c {
		s += " " + o.GoString()
	}
	return s + "\n"
}

// Option represents the AND of a list of Terms.
type Option []Term

// Opt builds an Option from the list of Terms.
func Opt(terms ...Term) Option {
	return Option(terms)
}

// ParseOption parses a comma-separated list of terms.
func ParseOption(expr string) (Option, error) {
	opt := Option{}
	for _, t := range strings.Split(expr, ",") {
		opt = append(opt, Term(t))
	}
	return opt, opt.Validate()
}

// ToConstraints returns Constraints containing just this option.
func (o Option) ToConstraints() Constraints { return o.ToConstraint().ToConstraints() }

// ToConstraint returns a Constraint containing just this option.
func (o Option) ToConstraint() Constraint { return Constraint{o} }

// ToOption returns o.
func (o Option) ToOption() Option { return o }

// Validate validates o.
func (o Option) Validate() error {
	for _, t := range o {
		if err := t.Validate(); err != nil {
			return fmt.Errorf("invalid term \"%s\": %s", t, err)
		}
	}
	return nil
}

// Evaluate the boolean formula represented by o under the given assignment of
// tag values. This is the AND of the values of the constituent Terms.
func (o Option) Evaluate(v map[string]bool) bool {
	r := true
	for _, t := range o {
		r = r && t.Evaluate(v)
	}
	return r
}

// GoString represents the Option as a comma-separated list of terms.
func (o Option) GoString() string {
	var ts []string
	for _, t := range o {
		ts = append(ts, t.GoString())
	}
	return strings.Join(ts, ",")
}

// Term is an atomic term in a build constraint: an identifier or its negation.
type Term string

// Not returns a term for the negation of ident.
func Not(ident string) Term {
	return Term("!" + ident)
}

// ToConstraints returns Constraints containing just this term.
func (t Term) ToConstraints() Constraints { return t.ToOption().ToConstraints() }

// ToConstraint returns a Constraint containing just this term.
func (t Term) ToConstraint() Constraint { return t.ToOption().ToConstraint() }

// ToOption returns an Option containing just this term.
func (t Term) ToOption() Option { return Option{t} }

// IsNegated reports whether t is the negation of an identifier.
func (t Term) IsNegated() bool { return strings.HasPrefix(string(t), "!") }

// Name returns the identifier for this term.
func (t Term) Name() string {
	return strings.TrimPrefix(string(t), "!")
}

// Validate the term.
func (t Term) Validate() error {
	// Reference: https://github.com/golang/go/blob/204a8f55dc2e0ac8d27a781dab0da609b98560da/src/cmd/go/internal/imports/build.go#L110-L112
	//
	//		if strings.HasPrefix(name, "!!") { // bad syntax, reject always
	//			return false
	//		}
	//
	if strings.HasPrefix(string(t), "!!") {
		return errors.New("at most one '!' allowed")
	}

	if len(t.Name()) == 0 {
		return errors.New("empty tag name")
	}

	// Reference: https://github.com/golang/go/blob/204a8f55dc2e0ac8d27a781dab0da609b98560da/src/cmd/go/internal/imports/build.go#L121-L127
	//
	//		// Tags must be letters, digits, underscores or dots.
	//		// Unlike in Go identifiers, all digits are fine (e.g., "386").
	//		for _, c := range name {
	//			if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
	//				return false
	//			}
	//		}
	//
	for _, c := range t.Name() {
		if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
			return fmt.Errorf("character '%c' disallowed in tags", c)
		}
	}

	return nil
}

// Evaluate the term under the given set of identifier values.
func (t Term) Evaluate(v map[string]bool) bool {
	return (t.Validate() == nil) && (v[t.Name()] == !t.IsNegated())
}

// GoString returns t.
func (t Term) GoString() string { return string(t) }

// SetTags builds a set where the given list of identifiers are true.
func SetTags(idents ...string) map[string]bool {
	v := map[string]bool{}
	for _, ident := range idents {
		v[ident] = true
	}
	return v
}