Skip to content

Commit 7ecaa42

Browse files
quirogasnoahdietz
andauthored
fix(cli): unexpected lint warning when providing multiple files (#1496)
* fix(cli): remove second resolveFilenames * chore(cli-test): test resolveImports * chore(cli): add import resolution function * chore(cli): consolidate file resolution --------- Co-authored-by: Noah Dietz <[email protected]>
1 parent 35be28f commit 7ecaa42

File tree

3 files changed

+224
-20
lines changed

3 files changed

+224
-20
lines changed

cmd/api-linter/cli.go

Lines changed: 79 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import (
1919
"errors"
2020
"fmt"
2121
"os"
22+
"path/filepath"
2223
"strings"
2324
"sync"
2425

@@ -79,7 +80,7 @@ func newCli(args []string) *cli {
7980
fs.StringArrayVar(&protoDescFlag, "descriptor-set-in", nil, "The file containing a FileDescriptorSet for searching proto imports.\nMay be specified multiple times.")
8081
fs.StringArrayVar(&ruleEnableFlag, "enable-rule", nil, "Enable a rule with the given name.\nMay be specified multiple times.")
8182
fs.StringArrayVar(&ruleDisableFlag, "disable-rule", nil, "Disable a rule with the given name.\nMay be specified multiple times.")
82-
fs.BoolVar(&listRulesFlag, "list-rules", false, "Print the rules and exit. Honors the output-format flag.")
83+
fs.BoolVar(&listRulesFlag, "list-rules", false, "Print the rules and exit. Honors the output-format flag.")
8384
fs.BoolVar(&debugFlag, "debug", false, "Run in debug mode. Panics will print stack.")
8485
fs.BoolVar(&ignoreCommentDisablesFlag, "ignore-comment-disables", false, "If set to true, disable comments will be ignored.\nThis is helpful when strict enforcement of AIPs are necessary and\nproto definitions should not be able to disable checks.")
8586

@@ -150,9 +151,12 @@ func (c *cli) lint(rules lint.RuleRegistry, configs lint.Configs) error {
150151
}
151152
var errorsWithPos []protoparse.ErrorWithPos
152153
var lock sync.Mutex
154+
155+
imports := resolveImports(c.ProtoImportPaths)
156+
153157
// Parse proto files into `protoreflect` file descriptors.
154158
p := protoparse.Parser{
155-
ImportPaths: append(c.ProtoImportPaths, "."),
159+
ImportPaths: imports,
156160
IncludeSourceCodeInfo: true,
157161
LookupImport: lookupImport,
158162
ErrorReporter: func(errorWithPos protoparse.ErrorWithPos) error {
@@ -165,21 +169,7 @@ func (c *cli) lint(rules lint.RuleRegistry, configs lint.Configs) error {
165169
},
166170
}
167171
// Resolve file absolute paths to relative ones.
168-
// Using supplied import paths first.
169-
protoFiles := c.ProtoFiles
170-
if len(c.ProtoImportPaths) > 0 {
171-
protoFiles, err = protoparse.ResolveFilenames(c.ProtoImportPaths, c.ProtoFiles...)
172-
if err != nil {
173-
return err
174-
}
175-
}
176-
// Then resolve again against ".", the local directory.
177-
// This is necessary because ResolveFilenames won't resolve a path if it
178-
// relative to *at least one* of the given import paths, which can result
179-
// in duplicate file parsing and compilation errors, as seen in #1465 and
180-
// #1471. So we resolve against local (default) and flag specified import
181-
// paths separately.
182-
protoFiles, err = protoparse.ResolveFilenames([]string{"."}, protoFiles...)
172+
protoFiles, err := protoparse.ResolveFilenames(p.ImportPaths, c.ProtoFiles...)
183173
if err != nil {
184174
return err
185175
}
@@ -305,3 +295,75 @@ func getOutputFormatFunc(formatType string) formatFunc {
305295
}
306296
return yaml.Marshal
307297
}
298+
299+
func resolveImports(imports []string) []string {
300+
// If no import paths are provided, default to the current directory.
301+
if len(imports) == 0 {
302+
return []string{"."}
303+
}
304+
305+
// Get the absolute path of the current working directory.
306+
cwd, err := os.Getwd()
307+
if err != nil {
308+
// Fallback: If we can't get CWD, return only the provided paths and "."
309+
seen := map[string]bool{
310+
".": true,
311+
}
312+
result := []string{"."} // Always include "."
313+
for _, p := range imports {
314+
if !seen[p] {
315+
seen[p] = true
316+
result = append(result, p)
317+
}
318+
}
319+
return result
320+
}
321+
322+
// Resolve the canonical path for the current working directory.
323+
// This helps with symlinks (e.g., /var vs /private/var on macOS).
324+
evaluatedCwd, err := filepath.EvalSymlinks(cwd)
325+
if err != nil {
326+
// Fallback to Clean if EvalSymlinks fails (e.g., path does not exist)
327+
evaluatedCwd = filepath.Clean(cwd)
328+
}
329+
330+
// Initialize resolvedImports with "." and track its canonical absolute path.
331+
resolvedImports := []string{"."}
332+
seenAbsolutePaths := map[string]bool{
333+
evaluatedCwd: true, // Mark canonical CWD as seen
334+
}
335+
336+
for _, p := range imports {
337+
absPath, err := filepath.Abs(p)
338+
if err != nil {
339+
// If we can't get the absolute path, treat it as an external path
340+
// and add it if not already seen (by its original string form).
341+
if !seenAbsolutePaths[p] {
342+
seenAbsolutePaths[p] = true
343+
resolvedImports = append(resolvedImports, p)
344+
}
345+
continue
346+
}
347+
348+
// Resolve the canonical path for the current import path.
349+
evaluatedAbsPath, err := filepath.EvalSymlinks(absPath)
350+
if err != nil {
351+
// Fallback to Clean if EvalSymlinks fails
352+
evaluatedAbsPath = filepath.Clean(absPath)
353+
}
354+
355+
// Check if the current import path's canonical form is the CWD's canonical form
356+
// or a subdirectory of it. If so, it's covered by ".", so we skip it.
357+
if evaluatedAbsPath == evaluatedCwd || strings.HasPrefix(evaluatedAbsPath, evaluatedCwd+string(os.PathSeparator)) {
358+
continue
359+
}
360+
361+
// Add the original path if its canonical absolute form has not been seen before.
362+
if !seenAbsolutePaths[evaluatedAbsPath] {
363+
seenAbsolutePaths[evaluatedAbsPath] = true
364+
resolvedImports = append(resolvedImports, p)
365+
}
366+
}
367+
368+
return resolvedImports
369+
}

cmd/api-linter/cli_test.go

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
package main
22

33
import (
4+
"os"
5+
"path/filepath"
6+
"sort"
47
"testing"
58

69
"github.com/google/go-cmp/cmp"
@@ -54,3 +57,145 @@ func TestNewCli(t *testing.T) {
5457
})
5558
}
5659
}
60+
61+
func TestResolveImports(t *testing.T) {
62+
// Save the original working directory and restore it at the end of the test.
63+
originalCWD, err := os.Getwd()
64+
if err != nil {
65+
t.Fatalf("Failed to get original working directory: %v", err)
66+
}
67+
defer func() {
68+
if err := os.Chdir(originalCWD); err != nil {
69+
t.Errorf("Failed to restore original working directory: %v", err)
70+
}
71+
}()
72+
73+
defaultSetup := func(t *testing.T, cwd, externalDir string) {}
74+
75+
tests := []struct {
76+
name string
77+
protoImportPaths func(cwd, externalDir string) []string
78+
setup func(t *testing.T, cwd, externalDir string)
79+
want func(externalDir string) []string
80+
}{
81+
{
82+
name: "NoProtoImportPaths",
83+
protoImportPaths: func(_, _ string) []string {
84+
return []string{}
85+
},
86+
setup: defaultSetup,
87+
want: func(_ string) []string {
88+
return []string{"."}
89+
},
90+
},
91+
{
92+
name: "ExplicitDot",
93+
protoImportPaths: func(_, _ string) []string {
94+
return []string{"."}
95+
},
96+
setup: defaultSetup,
97+
want: func(_ string) []string {
98+
return []string{"."}
99+
},
100+
},
101+
{
102+
name: "SubdirectoryOfCWD",
103+
protoImportPaths: func(_, _ string) []string {
104+
return []string{"test_dir"}
105+
},
106+
setup: func(t *testing.T, cwd, _ string) {
107+
if err := os.Mkdir(filepath.Join(cwd, "test_dir"), 0755); err != nil {
108+
t.Fatalf("Failed to create temp subdirectory: %v", err)
109+
}
110+
},
111+
want: func(_ string) []string {
112+
return []string{"."} // "test_dir" should be covered by "."
113+
},
114+
},
115+
{
116+
name: "SubdirectoryOfCWDWithDot",
117+
protoImportPaths: func(_, _ string) []string {
118+
return []string{".", "test_dir"}
119+
},
120+
setup: func(t *testing.T, cwd, _ string) {
121+
if err := os.Mkdir(filepath.Join(cwd, "test_dir"), 0755); err != nil {
122+
t.Fatalf("Failed to create temp subdirectory: %v", err)
123+
}
124+
},
125+
want: func(_ string) []string {
126+
return []string{"."}
127+
},
128+
},
129+
{
130+
name: "ExternalAbsolutePath",
131+
protoImportPaths: func(_, externalDir string) []string {
132+
return []string{externalDir}
133+
},
134+
setup: defaultSetup,
135+
want: func(externalDir string) []string {
136+
return []string{".", externalDir}
137+
},
138+
},
139+
{
140+
name: "MixedPaths",
141+
protoImportPaths: func(_, externalDir string) []string {
142+
return []string{"./relative_dir", externalDir, "test_dir"}
143+
},
144+
setup: func(t *testing.T, cwd, _ string) {
145+
if err := os.Mkdir(filepath.Join(cwd, "relative_dir"), 0755); err != nil {
146+
t.Fatalf("Failed to create relative_dir: %v", err)
147+
}
148+
if err := os.Mkdir(filepath.Join(cwd, "test_dir"), 0755); err != nil {
149+
t.Fatalf("Failed to create test_dir: %v", err)
150+
}
151+
},
152+
want: func(externalDir string) []string {
153+
return []string{".", externalDir}
154+
},
155+
},
156+
{
157+
name: "NonExistentRelativePath",
158+
protoImportPaths: func(_, _ string) []string {
159+
return []string{"non_existent_dir"}
160+
},
161+
setup: defaultSetup,
162+
want: func(_ string) []string {
163+
return []string{"."} // Should still resolve to just "." as non_existent_dir is relative to CWD
164+
},
165+
},
166+
{
167+
name: "CurrentDirAsAbsolutePath",
168+
protoImportPaths: func(cwd, _ string) []string {
169+
return []string{cwd}
170+
},
171+
setup: defaultSetup,
172+
want: func(_ string) []string {
173+
return []string{"."}
174+
},
175+
},
176+
}
177+
178+
for _, test := range tests {
179+
t.Run(test.name, func(t *testing.T) {
180+
cwd := t.TempDir()
181+
externalDir := t.TempDir()
182+
183+
if err := os.Chdir(cwd); err != nil {
184+
t.Fatalf("Failed to change directory to %q: %v", cwd, err)
185+
}
186+
187+
test.setup(t, cwd, externalDir)
188+
189+
protoImportPaths := test.protoImportPaths(cwd, externalDir)
190+
want := test.want(externalDir)
191+
192+
got := resolveImports(protoImportPaths)
193+
sort.Strings(got)
194+
sort.Strings(want)
195+
196+
if diff := cmp.Diff(want, got); diff != "" {
197+
t.Errorf("resolveImports() mismatch (-want +got):\n%s", diff)
198+
}
199+
})
200+
}
201+
}

cmd/api-linter/integration_test.go

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,9 +182,6 @@ func TestMultipleFilesFromParentDir(t *testing.T) {
182182
// This test addresses a previously found bug:
183183
// https://github.com/googleapis/api-linter/issues/1465
184184

185-
// Skipping until Issue # 1465 is addressed
186-
t.Skip("Skipping until Issue # 1465 is addressed")
187-
188185
projDir, err := os.MkdirTemp("", "proj")
189186
if err != nil {
190187
t.Fatal(err)

0 commit comments

Comments
 (0)