blob: fb00fa6521a885dd230c5c176605aeab90034eb0 [file] [log] [blame]
Marcel van Lohuizenbc4d65d2018-12-10 15:40:02 +01001// Copyright 2018 The CUE Authors
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
15package load
16
17import (
18 "bytes"
19 "fmt"
20 "strings"
21 "unicode"
22)
23
24// matchFileTest reports whether the file with the given name in the given directory
25// matches the context and would be included in a Package created by ImportDir
26// of that directory.
27//
28// matchFileTest considers the name of the file and may use cfg.Build.OpenFile to
29// read some or all of the file's content.
30func matchFileTest(cfg *Config, dir, name string) (match bool, err error) {
31 match, _, _, err = matchFile(cfg, dir, name, false, false, nil)
32 return
33}
34
35// matchFile determines whether the file with the given name in the given directory
36// should be included in the package being constructed.
37// It returns the data read from the file.
38// If returnImports is true and name denotes a CUE file, matchFile reads
39// until the end of the imports (and returns that data) even though it only
40// considers text until the first non-comment.
41// If allTags is non-nil, matchFile records any encountered build tag
42// by setting allTags[tag] = true.
43func matchFile(cfg *Config, dir, name string, returnImports, allFiles bool, allTags map[string]bool) (match bool, data []byte, filename string, err error) {
44 if strings.HasPrefix(name, "_") ||
45 strings.HasPrefix(name, ".") {
46 return
47 }
48
49 i := strings.LastIndex(name, ".")
50 if i < 0 {
51 i = len(name)
52 }
53 ext := name[i:]
54
55 switch ext {
56 case cueSuffix:
57 // tentatively okay - read to make sure
58 default:
59 // skip
60 return
61 }
62
63 filename = cfg.fileSystem.joinPath(dir, name)
64 f, err := cfg.fileSystem.openFile(filename)
65 if err != nil {
66 return
67 }
68
69 if strings.HasSuffix(filename, cueSuffix) {
70 data, err = readImports(f, false, nil)
71 } else {
72 data, err = readComments(f)
73 }
74 f.Close()
75 if err != nil {
76 err = fmt.Errorf("read %s: %v", filename, err)
77 return
78 }
79
80 // Look for +build comments to accept or reject the file.
81 if !shouldBuild(cfg, data, allTags) && !allFiles {
82 return
83 }
84
85 match = true
86 return
87}
88
89// shouldBuild reports whether it is okay to use this file,
90// The rule is that in the file's leading run of // comments
91// and blank lines, which must be followed by a blank line
92// (to avoid including a Go package clause doc comment),
93// lines beginning with '// +build' are taken as build directives.
94//
95// The file is accepted only if each such line lists something
96// matching the file. For example:
97//
98// // +build windows linux
99//
100// marks the file as applicable only on Windows and Linux.
101//
102// If shouldBuild finds a //go:binary-only-package comment in the file,
103// it sets *binaryOnly to true. Otherwise it does not change *binaryOnly.
104//
105func shouldBuild(cfg *Config, content []byte, allTags map[string]bool) bool {
106 // Pass 1. Identify leading run of // comments and blank lines,
107 // which must be followed by a blank line.
108 end := 0
109 p := content
110 for len(p) > 0 {
111 line := p
112 if i := bytes.IndexByte(line, '\n'); i >= 0 {
113 line, p = line[:i], p[i+1:]
114 } else {
115 p = p[len(p):]
116 }
117 line = bytes.TrimSpace(line)
118 if len(line) == 0 { // Blank line
119 end = len(content) - len(p)
120 continue
121 }
122 if !bytes.HasPrefix(line, slashslash) { // Not comment line
123 break
124 }
125 }
126 content = content[:end]
127
128 // Pass 2. Process each line in the run.
129 p = content
130 allok := true
131 for len(p) > 0 {
132 line := p
133 if i := bytes.IndexByte(line, '\n'); i >= 0 {
134 line, p = line[:i], p[i+1:]
135 } else {
136 p = p[len(p):]
137 }
138 line = bytes.TrimSpace(line)
139 if bytes.HasPrefix(line, slashslash) {
140 line = bytes.TrimSpace(line[len(slashslash):])
141 if len(line) > 0 && line[0] == '+' {
142 // Looks like a comment +line.
143 f := strings.Fields(string(line))
144 if f[0] == "+build" {
145 ok := false
146 for _, tok := range f[1:] {
147 if doMatch(cfg, tok, allTags) {
148 ok = true
149 }
150 }
151 if !ok {
152 allok = false
153 }
154 }
155 }
156 }
157 }
158
159 return allok
160}
161
162// doMatch reports whether the name is one of:
163//
164// tag (if tag is listed in cfg.Build.BuildTags or cfg.Build.ReleaseTags)
165// !tag (if tag is not listed in cfg.Build.BuildTags or cfg.Build.ReleaseTags)
166// a comma-separated list of any of these
167//
168func doMatch(cfg *Config, name string, allTags map[string]bool) bool {
169 if name == "" {
170 if allTags != nil {
171 allTags[name] = true
172 }
173 return false
174 }
175 if i := strings.Index(name, ","); i >= 0 {
176 // comma-separated list
177 ok1 := doMatch(cfg, name[:i], allTags)
178 ok2 := doMatch(cfg, name[i+1:], allTags)
179 return ok1 && ok2
180 }
181 if strings.HasPrefix(name, "!!") { // bad syntax, reject always
182 return false
183 }
184 if strings.HasPrefix(name, "!") { // negation
185 return len(name) > 1 && !doMatch(cfg, name[1:], allTags)
186 }
187
188 if allTags != nil {
189 allTags[name] = true
190 }
191
192 // Tags must be letters, digits, underscores or dots.
193 // Unlike in CUE identifiers, all digits are fine (e.g., "386").
194 for _, c := range name {
195 if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' {
196 return false
197 }
198 }
199
200 // other tags
201 for _, tag := range cfg.BuildTags {
202 if tag == name {
203 return true
204 }
205 }
206 for _, tag := range cfg.releaseTags {
207 if tag == name {
208 return true
209 }
210 }
211
212 return false
213}