| package analyze |
| |
| import ( |
| "io/ioutil" |
| "path/filepath" |
| "regexp" |
| "strings" |
| "sync" |
| |
| "github.com/bazelbuild/rules_typescript/ts_auto_deps/platform" |
| "github.com/bazelbuild/rules_typescript/ts_auto_deps/workspace" |
| ) |
| |
| // ts_auto_depsImport represents a single import in a TypeScript source. |
| type ts_auto_depsImport struct { |
| // importPath can be an ES6 path ('./foo/bar'), but also a namespace ('goog:...'). |
| // This is the import path as it appears in the TypeScript source. |
| importPath string |
| // knownTarget is the (fully qualified) bazel target providing importPath. |
| // It's either found by locateMissingTargets or taken from a ts_auto_deps comment. |
| knownTarget string |
| location sourceLocation |
| } |
| |
| // resolvedPath is the path to the import relative to the root of the |
| // workspace. For example, an import of './foo' in the 'bar/baz' directory |
| // would have a path from root of 'bar/baz/foo'. |
| // |
| // Absolute imports have no resolvedPath since the physical location of |
| // these imports depends on the dependencies of the target the source |
| // location is a member of. For example, an import of 'foo/bar' would have |
| // no resolvedPath. |
| func (i *ts_auto_depsImport) resolvedPath() string { |
| if strings.HasPrefix(i.importPath, "./") || strings.HasPrefix(i.importPath, "../") { |
| // If the import is relative to the source location, use the source |
| // location to form a "canonical" path from the root. |
| return platform.Normalize(filepath.Clean(filepath.Join(filepath.Dir(i.location.sourcePath), i.importPath))) |
| } else if trim := strings.TrimPrefix(i.importPath, workspace.Name()+"/"); trim != i.importPath { |
| return trim |
| } |
| // The import is an absolute import and therefore does not have a definite |
| // resolved path. |
| return i.importPath |
| } |
| |
| // sourceLocation points to a position in a source file. |
| type sourceLocation struct { |
| // Workspace root relative source path. |
| sourcePath string |
| // offset and length are byte offsets, line is the 1-indexed source line (considering only \n as breaks). |
| offset, length, line int |
| } |
| |
| // extractAllImports extracts the TypeScript imports from paths. |
| // |
| // paths should be relative to root. The root will be joined to each path |
| // to construct a physical path to each file. |
| func extractAllImports(root string, paths []string) (map[string][]*ts_auto_depsImport, []error) { |
| debugf("extracting imports from TypeScript files relative to %q: %q", root, paths) |
| allImports := make(map[string][]*ts_auto_depsImport) |
| var ( |
| errors []error |
| mutex sync.Mutex |
| group sync.WaitGroup |
| ) |
| for _, path := range paths { |
| group.Add(1) |
| go func(path string) { |
| defer group.Done() |
| imports, err := extractImports(root, path) |
| // Lock the mutex to prevent concurrent writes. |
| mutex.Lock() |
| defer mutex.Unlock() |
| if err != nil { |
| errors = append(errors, err) |
| return |
| } |
| allImports[path] = imports |
| }(path) |
| } |
| group.Wait() |
| return allImports, errors |
| } |
| |
| // extractImports extracts the TypeScript imports from a single file. path |
| // should be a path from the root to the file. |
| func extractImports(root, path string) ([]*ts_auto_depsImport, error) { |
| d, err := ioutil.ReadFile(filepath.Join(root, path)) |
| if err != nil { |
| return nil, err |
| } |
| return parseImports(path, d), nil |
| } |
| |
| const ( |
| ts_auto_depsFrom = `^[ \t]*//[ \t]+ts_auto_deps:[^\n]*?from[ \t]+(?P<Target>//\S+)$` |
| importPreface = `^[ \t]*(?:import|export)\b\s*` |
| wildcardTerm = `\*(?:\s*as\s+\S+)?` // "as..." is optional to match exports. |
| identifiersClause = `(?:\{[^}]*\}|\S+|` + wildcardTerm + `)` |
| symbolsTerm = `(?:` + identifiersClause + `(?:,\s*` + identifiersClause + `)?\s*\bfrom\b\s*)?` |
| url = `['"](?P<URL>[^'";]+)['"]\s*;?` |
| namespaceComment = `(?:\s*//[ \t]*from[ \t]+(?P<Target>//\S+)$)?` |
| ) |
| |
| var importRE = regexp.MustCompile("(?ms)" + |
| "(?:" + ts_auto_depsFrom + ")|" + |
| "(?:" + importPreface + symbolsTerm + url + namespaceComment + ")") |
| |
| // parseImports scans contents for imports (ES6 modules, ts_auto_deps comments), and |
| // returns a list of ts_auto_depsImports. knownTarget is already filled in for imports |
| // that have ts_auto_deps comments. |
| func parseImports(sourcePath string, contents []byte) []*ts_auto_depsImport { |
| var imports []*ts_auto_depsImport |
| lastOffset := 0 |
| line := 1 |
| column := 1 |
| for _, matchIndices := range importRE.FindAllSubmatchIndex(contents, -1) { |
| imp := &ts_auto_depsImport{} |
| imports = append(imports, imp) |
| // matchIndices[0, 1]: full RE match |
| imp.location.sourcePath = sourcePath |
| for lastOffset < matchIndices[1] { |
| // Iterate to the *end* of the import statement. |
| // The ts_auto_deps comment must be placed at the end of the "import" statement. |
| // This offset has to be exactly the end of the import for ts_auto_deps later on |
| // to insert the '// from' comment in the correct line. |
| column++ |
| if contents[lastOffset] == '\n' { |
| line++ |
| column = 1 |
| } |
| lastOffset++ |
| } |
| imp.location.offset = matchIndices[0] |
| imp.location.length = matchIndices[1] - matchIndices[0] |
| imp.location.line = line |
| if matchIndices[2] >= 0 { |
| // matchIndices[2, 3]: Target for a // ts_auto_deps: ... from ... comment. |
| imp.knownTarget = string(contents[matchIndices[2]:matchIndices[3]]) |
| } else { |
| // matchIndices[4, 5]: URL in import x from 'url'; |
| imp.importPath = string(contents[matchIndices[4]:matchIndices[5]]) |
| } |
| if matchIndices[6] >= 0 { |
| // matchIndices[6, 7]: Target for a // from comment |
| imp.knownTarget = string(contents[matchIndices[6]:matchIndices[7]]) |
| } |
| } |
| return imports |
| } |