File: instance.go

package info (click to toggle)
golang-github-cue-lang-cue 0.14.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 19,644 kB
  • sloc: makefile: 20; sh: 15
file content (288 lines) | stat: -rw-r--r-- 9,113 bytes parent folder | download
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
// Copyright 2018 The CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package build

import (
	"fmt"
	pathpkg "path"
	"path/filepath"
	"strings"
	"unicode"

	"cuelang.org/go/cue/ast"
	"cuelang.org/go/cue/ast/astutil"
	"cuelang.org/go/cue/errors"
	"cuelang.org/go/cue/parser"
	"cuelang.org/go/cue/token"
	"cuelang.org/go/internal/mod/modfiledata"
)

// An Instance describes the collection of files, and its imports, necessary
// to build a CUE instance.
//
// A typical way to create an Instance is to use the cue/load package.
type Instance struct {
	ctxt *Context

	BuildFiles    []*File // files to be included in the build
	IgnoredFiles  []*File // files excluded for this build
	OrphanedFiles []*File // recognized file formats not part of any build
	InvalidFiles  []*File // could not parse these files
	UnknownFiles  []*File // unknown file types

	User bool // True if package was created from individual files.

	// Files contains the AST for all files part of this instance.
	// TODO: the intent is to deprecate this in favor of BuildFiles.
	Files []*ast.File

	loadFunc LoadFunc
	done     bool

	// PkgName is the name specified in the package clause.
	PkgName string
	hasName bool

	// ImportPath returns the unique path to identify an imported instance.
	//
	// Instances created with [Context.NewInstance] do not have an import path.
	ImportPath string

	// Imports lists the instances of all direct imports of this instance.
	Imports []*Instance

	// The Err for loading this package or nil on success. This does not
	// include any errors of dependencies. Incomplete will be set if there
	// were any errors in dependencies.
	Err errors.Error

	parent *Instance // TODO: for cycle detection

	// The following fields are for informative purposes and are not used by
	// the cue package to create an instance.

	// DisplayPath is a user-friendly version of the package or import path.
	DisplayPath string

	// Module defines the module name of a package. It must be defined if
	// the packages within the directory structure of the module are to be
	// imported by other packages, including those within the module.
	Module string

	// ModuleFile holds the actual module file data, if available.
	ModuleFile *modfiledata.File

	// Root is the root of the directory hierarchy, it may be "" if this an
	// instance has no imports.
	// If Module != "", this corresponds to the module root.
	// Root/pkg is the directory that holds third-party packages.
	Root string

	// Dir is the package directory. A package may also include files from
	// ancestor directories, up to the module file.
	Dir string

	// NOTICE: the below struct field tags may change in the future.

	// Incomplete reports whether any dependencies had an error.
	Incomplete bool `api:"alpha"`

	// Dependencies

	// ImportPaths gives the transitive dependencies of all imports.
	ImportPaths []string               `api:"alpha"`
	ImportPos   map[string][]token.Pos `api:"alpha"` // line information for Imports

	Deps       []string `api:"alpha"`
	DepsErrors []error  `api:"alpha"`
	Match      []string `api:"alpha"`
}

// RelPath reports the path of f relative to the root of the instance's module
// directory. The full path is returned if a relative path could not be found.
func (inst *Instance) RelPath(f *File) string {
	p, err := filepath.Rel(inst.Root, f.Filename)
	if err != nil {
		return f.Filename
	}
	return p
}

// ID returns the package ID unique for this module.
func (inst *Instance) ID() string {
	if s := inst.ImportPath; s != "" {
		return s
	}
	if inst.PkgName == "" {
		return "_"
	}
	s := fmt.Sprintf("%s:%s", inst.Module, inst.PkgName)
	return s
}

// Dependencies reports all Instances on which this instance depends.
func (inst *Instance) Dependencies() []*Instance {
	// TODO: as cyclic dependencies are not allowed, we could just not check.
	// Do for safety now and remove later if needed.
	return appendDependencies(nil, inst, map[*Instance]bool{})
}

func appendDependencies(a []*Instance, inst *Instance, done map[*Instance]bool) []*Instance {
	for _, d := range inst.Imports {
		if done[d] {
			continue
		}
		a = append(a, d)
		done[d] = true
		a = appendDependencies(a, d, done)
	}
	return a
}

// Abs converts relative path used in the one of the file fields to an
// absolute one.
func (inst *Instance) Abs(path string) string {
	if filepath.IsAbs(path) {
		return path
	}
	return filepath.Join(inst.Root, path)
}

func (inst *Instance) setPkg(pkg string) bool {
	if !inst.hasName {
		inst.hasName = true
		inst.PkgName = pkg
		return true
	}
	return false
}

// ReportError reports an error processing this instance.
func (inst *Instance) ReportError(err errors.Error) {
	inst.Err = errors.Append(inst.Err, err)
}

// Context defines the build context for this instance. All files defined
// in Syntax as well as all imported instances must be created using the
// same build context.
func (inst *Instance) Context() *Context {
	return inst.ctxt
}

func (inst *Instance) parse(name string, src interface{}) (*ast.File, error) {
	cfg := parser.NewConfig(parser.ParseComments)
	if inst.ModuleFile != nil && inst.ModuleFile.Language != nil {
		cfg = cfg.Apply(parser.Version(inst.ModuleFile.Language.Version))
	}
	if inst.ctxt != nil && inst.ctxt.parseFunc != nil {
		return inst.ctxt.parseFunc(name, src, cfg)
	}
	return parser.ParseFile(name, src, cfg)
}

// LookupImport defines a mapping from an ImportSpec's ImportPath to Instance.
func (inst *Instance) LookupImport(path string) *Instance {
	path = inst.expandPath(path)
	for _, inst := range inst.Imports {
		if inst.ImportPath == path {
			return inst
		}
	}
	return nil
}

func (inst *Instance) addImport(imp *Instance) {
	for _, inst := range inst.Imports {
		if inst.ImportPath == imp.ImportPath {
			if inst != imp {
				panic("import added multiple times with different instances")
			}
			return
		}
	}
	inst.Imports = append(inst.Imports, imp)
}

// AddFile adds the file with the given name to the list of files for this
// instance. The file may be loaded from the cache of the instance's context.
// It does not process the file's imports. The package name of the file must
// match the package name of the instance.
//
// Deprecated: use [Instance.AddSyntax] or wait for this to be renamed using a new
// signature.
func (inst *Instance) AddFile(filename string, src interface{}) error {
	file, err := inst.parse(filename, src)
	if err != nil {
		// should always be an errors.List, but just in case.
		err := errors.Promote(err, "error adding file")
		inst.ReportError(err)
		return err
	}

	return inst.AddSyntax(file)
}

// AddSyntax adds the given file to list of files for this instance. The package
// name of the file must match the package name of the instance.
func (inst *Instance) AddSyntax(file *ast.File) errors.Error {
	astutil.Resolve(file, func(pos token.Pos, msg string, args ...interface{}) {
		inst.Err = errors.Append(inst.Err, errors.Newf(pos, msg, args...))
	})
	pkg := file.PackageName()
	if pkg != "" && pkg != "_" && !inst.User && !inst.setPkg(pkg) && pkg != inst.PkgName {
		err := errors.Newf(file.Pos(),
			"package name %q conflicts with previous package name %q",
			pkg, inst.PkgName)
		inst.ReportError(err)
		return err
	}
	inst.Files = append(inst.Files, file)
	return nil
}

func (inst *Instance) expandPath(path string) string {
	isLocal := IsLocalImport(path)
	if isLocal {
		path = dirToImportPath(filepath.Join(inst.Dir, path))
	}
	return path
}

// dirToImportPath returns the pseudo-import path we use for a package
// outside the CUE path. It begins with _/ and then contains the full path
// to the directory. If the package lives in c:\home\gopher\my\pkg then
// the pseudo-import path is _/c_/home/gopher/my/pkg.
// Using a pseudo-import path like this makes the ./ imports no longer
// a special case, so that all the code to deal with ordinary imports works
// automatically.
func dirToImportPath(dir string) string {
	return pathpkg.Join("_", strings.Map(makeImportValid, filepath.ToSlash(dir)))
}

func makeImportValid(r rune) rune {
	// Should match Go spec, compilers, and ../../go/parser/parser.go:/isValidImport.
	const illegalChars = `!"#$%&'()*,:;<=>?[\]^{|}` + "`\uFFFD"
	if !unicode.IsGraphic(r) || unicode.IsSpace(r) || strings.ContainsRune(illegalChars, r) {
		return '_'
	}
	return r
}

// IsLocalImport reports whether the import path is
// a local import path, like ".", "..", "./foo", or "../foo".
func IsLocalImport(path string) bool {
	return path == "." || path == ".." ||
		strings.HasPrefix(path, "./") || strings.HasPrefix(path, "../")
}