forked from jhump/protoreflect
/
resolve_files.go
170 lines (157 loc) · 5.35 KB
/
resolve_files.go
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
package protoparse
import (
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
var errNoImportPathsForAbsoluteFilePath = errors.New("must specify at least one import path if any absolute file paths are given")
// ResolveFilenames tries to resolve fileNames into paths that are relative to
// directories in the given importPaths. The returned slice has the results in
// the same order as they are supplied in fileNames.
//
// The resulting names should be suitable for passing to Parser.ParseFiles.
//
// If no import paths are given and any file name is absolute, this returns an
// error. If no import paths are given and all file names are relative, this
// returns the original file names. If a file name is already relative to one
// of the given import paths, it will be unchanged in the returned slice. If a
// file name given is relative to the current working directory, it will be made
// relative to one of the given import paths; but if it cannot be made relative
// (due to no matching import path), an error will be returned.
func ResolveFilenames(importPaths []string, fileNames ...string) ([]string, error) {
if len(importPaths) == 0 {
if containsAbsFilePath(fileNames) {
// We have to do this as otherwise parseProtoFiles can result in duplicate symbols.
// For example, assume we import "foo/bar/bar.proto" in a file "/home/alice/dev/foo/bar/baz.proto"
// as we call ParseFiles("/home/alice/dev/foo/bar/bar.proto","/home/alice/dev/foo/bar/baz.proto")
// with "/home/alice/dev" as our current directory. Due to the recursive nature of parseProtoFiles,
// it will discover the import "foo/bar/bar.proto" in the input file, and call parse on this,
// adding "foo/bar/bar.proto" to the parsed results, as well as "/home/alice/dev/foo/bar/bar.proto"
// from the input file list. This will result in a
// 'duplicate symbol SYMBOL: already defined as field in "/home/alice/dev/foo/bar/bar.proto'
// error being returned from ParseFiles.
return nil, errNoImportPathsForAbsoluteFilePath
}
return fileNames, nil
}
absImportPaths, err := absoluteFilePaths(importPaths)
if err != nil {
return nil, err
}
resolvedFileNames := make([]string, 0, len(fileNames))
for _, fileName := range fileNames {
resolvedFileName, err := resolveFilename(absImportPaths, fileName)
if err != nil {
return nil, err
}
resolvedFileNames = append(resolvedFileNames, resolvedFileName)
}
return resolvedFileNames, nil
}
func containsAbsFilePath(filePaths []string) bool {
for _, filePath := range filePaths {
if filepath.IsAbs(filePath) {
return true
}
}
return false
}
func absoluteFilePaths(filePaths []string) ([]string, error) {
absFilePaths := make([]string, 0, len(filePaths))
for _, filePath := range filePaths {
absFilePath, err := canonicalize(filePath)
if err != nil {
return nil, err
}
absFilePaths = append(absFilePaths, absFilePath)
}
return absFilePaths, nil
}
func canonicalize(filePath string) (string, error) {
absPath, err := filepath.Abs(filePath)
if err != nil {
return "", err
}
// this is kind of gross, but it lets us construct a resolved path even if some
// path elements do not exist (a single call to filepath.EvalSymlinks would just
// return an error, ENOENT, in that case).
head := absPath
tail := ""
for {
noLinks, err := filepath.EvalSymlinks(head)
if err == nil {
if tail != "" {
return filepath.Join(noLinks, tail), nil
}
return noLinks, nil
}
if tail == "" {
tail = filepath.Base(head)
} else {
tail = filepath.Join(filepath.Base(head), tail)
}
head = filepath.Dir(head)
if head == "." {
// ran out of path elements to try to resolve
return absPath, nil
}
}
}
const dotPrefix = "." + string(filepath.Separator)
const dotDotPrefix = ".." + string(filepath.Separator)
func resolveFilename(absImportPaths []string, fileName string) (string, error) {
if filepath.IsAbs(fileName) {
return resolveAbsFilename(absImportPaths, fileName)
}
if !strings.HasPrefix(fileName, dotPrefix) && !strings.HasPrefix(fileName, dotDotPrefix) {
// Use of . and .. are assumed to be relative to current working
// directory. So if those aren't present, check to see if the file is
// relative to an import path.
for _, absImportPath := range absImportPaths {
absFileName := filepath.Join(absImportPath, fileName)
_, err := os.Stat(absFileName)
if err != nil {
continue
}
// found it! it was relative to this import path
return fileName, nil
}
}
// must be relative to current working dir
return resolveAbsFilename(absImportPaths, fileName)
}
func resolveAbsFilename(absImportPaths []string, fileName string) (string, error) {
absFileName, err := canonicalize(fileName)
if err != nil {
return "", err
}
for _, absImportPath := range absImportPaths {
if isDescendant(absImportPath, absFileName) {
resolvedPath, err := filepath.Rel(absImportPath, absFileName)
if err != nil {
return "", err
}
return resolvedPath, nil
}
}
return "", fmt.Errorf("%s does not reside in any import path", fileName)
}
// isDescendant returns true if file is a descendant of dir. Both dir and file must
// be cleaned, absolute paths.
func isDescendant(dir, file string) bool {
dir = filepath.Clean(dir)
cur := file
for {
d := filepath.Dir(cur)
if d == dir {
return true
}
if d == "." || d == cur {
// we've run out of path elements
return false
}
cur = d
}
}