Skip to content

Commit efc2266

Browse files
Bug fixes (#11)
* `Do`: Return tuple `([]UnsatisfiedRule, error)` * Add `UnsatisfiedRules.String()` * `[]UnsatisfiedRule` to `UnsatisfiedRules`
1 parent af833a7 commit efc2266

File tree

3 files changed

+142
-120
lines changed

3 files changed

+142
-120
lines changed

cli/main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
// Run:
2-
// git diff | go run cli/main.go
2+
// git diff | go run cli/main.go --verbose
33

44
package main
55

difflint.go

Lines changed: 78 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package difflint
33
import (
44
"fmt"
55
"io"
6+
"os"
67
"path/filepath"
78
"strings"
89

@@ -49,10 +50,6 @@ type LintOptions struct {
4950
// TemplatesFromFile returns the directive templates for the given file type.
5051
func (o *LintOptions) TemplatesFromFile(file string) ([]string, error) {
5152
fileType := strings.TrimPrefix(filepath.Ext(file), ".")
52-
if fileType == "" {
53-
return nil, errors.Errorf("file %q has no extension", file)
54-
}
55-
5653
templateIndices, ok := o.FileExtMap[fileType]
5754
if !ok {
5855
templateIndices = []int{o.DefaultTemplate}
@@ -87,6 +84,7 @@ type UnsatisfiedRule struct {
8784
UnsatisfiedTargets map[int]struct{}
8885
}
8986

87+
// UnsatisfiedRules is a list of unsatisfied rules.
9088
type UnsatisfiedRules []UnsatisfiedRule
9189

9290
// String returns a string representation of the unsatisfied rules.
@@ -122,6 +120,45 @@ type LintResult struct {
122120
UnsatisfiedRules UnsatisfiedRules
123121
}
124122

123+
// Walk walks the file tree rooted at root, calling callback for each file or
124+
// directory in the tree, including root.
125+
func Walk(root string, include []string, exclude []string, callback filepath.WalkFunc) error {
126+
isHidden := func(path string) bool {
127+
components := strings.Split(path, string(os.PathSeparator))
128+
for _, component := range components {
129+
if strings.HasPrefix(component, ".") && component != "." && component != ".." {
130+
return true
131+
}
132+
}
133+
return false
134+
}
135+
136+
return filepath.Walk(root, func(path string, info os.FileInfo, err error) error {
137+
if err != nil {
138+
return err
139+
}
140+
141+
if info.IsDir() {
142+
return nil
143+
}
144+
145+
if isHidden(path) {
146+
return nil
147+
}
148+
149+
included, err := Include(path, include, exclude)
150+
if err != nil {
151+
return err
152+
}
153+
154+
if !included {
155+
return nil
156+
}
157+
158+
return callback(path, info, nil)
159+
})
160+
}
161+
125162
// Lint lints the given hunks against the given rules and returns the result.
126163
func Lint(o LintOptions) (*LintResult, error) {
127164
// Parse the diff hunks.
@@ -131,18 +168,33 @@ func Lint(o LintOptions) (*LintResult, error) {
131168
}
132169

133170
// Parse rules from hunks.
134-
rulesMap, _, err := RulesMapFromHunks(hunks, o)
171+
rulesMap, presentTargetsMap, err := RulesMapFromHunks(hunks, o)
135172
if err != nil {
136173
return nil, errors.Wrap(err, "failed to parse rules from hunks")
137174
}
138175

139176
// Collect the rules that are not satisfied.
140-
unsatisfiedRules, err := Check(rulesMap)
177+
unsatisfiedRules, err := Check(rulesMap, presentTargetsMap)
141178
if err != nil {
142179
return nil, errors.Wrap(err, "failed to check rules")
143180
}
144181

145-
return &LintResult{UnsatisfiedRules: unsatisfiedRules}, nil
182+
// Filter out rules that are not intended to be included in the output.
183+
var filteredUnsatisfiedRules UnsatisfiedRules
184+
for _, rule := range unsatisfiedRules {
185+
included, err := Include(rule.Rule.Hunk.File, o.Include, o.Exclude)
186+
if err != nil {
187+
return nil, errors.Wrap(err, "failed to check if file is included")
188+
}
189+
190+
if !included {
191+
continue
192+
}
193+
194+
filteredUnsatisfiedRules = append(filteredUnsatisfiedRules, rule)
195+
}
196+
197+
return &LintResult{UnsatisfiedRules: filteredUnsatisfiedRules}, nil
146198
}
147199

148200
// TargetKey returns the key for the given target.
@@ -176,52 +228,35 @@ func isRelativeToCurrentDirectory(path string) bool {
176228
}
177229

178230
// Check returns the list of unsatisfied rules for the given map of rules.
179-
func Check(rulesMap map[string][]Rule) (UnsatisfiedRules, error) {
231+
func Check(rulesMap map[string][]Rule, targetsMap map[string]struct{}) (UnsatisfiedRules, error) {
180232
var unsatisfiedRules UnsatisfiedRules
181-
for pathnameA, rulesA := range rulesMap {
182-
outer:
183-
for i, ruleA := range rulesA {
184-
// Skip if ruleA is not present or if it has no targets.
185-
if len(ruleA.Targets) == 0 || !ruleA.Present {
186-
continue
187-
}
188233

189-
for pathnameB, rulesB := range rulesMap {
190-
inner:
191-
for j, ruleB := range rulesB {
192-
// Skip if both rules are present or if ruleA is the same as ruleB.
193-
if ruleB.Present || (pathnameA == pathnameB && i == j) {
194-
continue
195-
}
196-
197-
// Given that ruleA is present and ruleB is not present, check if ruleA
198-
// is satisfied by ruleB.
199-
unsatisfiedTargetIndices := make(map[int]struct{})
200-
for k, target := range ruleA.Targets {
201-
// ruleA is satisfied by ruleB if ruleB matches a target of ruleA.
202-
satisfied := target.ID == ruleB.ID && ((target.File == nil && pathnameA == pathnameB) || (*target.File == pathnameB))
203-
if satisfied {
204-
continue inner
205-
}
206-
207-
// Otherwise, add the target index to the list of unsatisfied targets.
208-
unsatisfiedTargetIndices[k] = struct{}{}
209-
}
210-
211-
unsatisfiedRules = append(unsatisfiedRules, UnsatisfiedRule{
212-
Rule: ruleA,
213-
UnsatisfiedTargets: unsatisfiedTargetIndices,
214-
})
215-
continue outer
234+
// Check each rule.
235+
for _, rules := range rulesMap {
236+
for _, rule := range rules {
237+
unsatisfiedTargets := make(map[int]struct{}, len(rule.Targets))
238+
for i, target := range rule.Targets {
239+
key := TargetKey(rule.Hunk.File, target)
240+
if _, ok := targetsMap[key]; rule.Present != ok {
241+
unsatisfiedTargets[i] = struct{}{}
216242
}
217243
}
244+
245+
// If there are unsatisfied targets, then the rule is unsatisfied.
246+
if len(unsatisfiedTargets) > 0 {
247+
unsatisfiedRules = append(unsatisfiedRules, UnsatisfiedRule{
248+
Rule: rule,
249+
UnsatisfiedTargets: unsatisfiedTargets,
250+
})
251+
}
218252
}
219253
}
220254

255+
// Return the unordered list of unsatisfied rules.
221256
return unsatisfiedRules, nil
222257
}
223258

224-
// Entrypoint for the difflint command.
259+
// Do is the difflint command's entrypoint.
225260
func Do(r io.Reader, include, exclude []string, extMapPath string) (UnsatisfiedRules, error) {
226261
// Parse options.
227262
extMap := NewExtMap(extMapPath)
@@ -239,25 +274,6 @@ func Do(r io.Reader, include, exclude []string, extMapPath string) (UnsatisfiedR
239274
return nil, errors.Wrap(err, "failed to lint hunks")
240275
}
241276

242-
// If there are no unsatisfied rules, return nil.
243-
if len(result.UnsatisfiedRules) == 0 {
244-
return nil, nil
245-
}
246-
247-
// Print the unsatisfied rules.
248-
var included bool
249-
for _, rule := range result.UnsatisfiedRules {
250-
// Skip if the rule is not intended to be included in the output.
251-
included, err = Include(rule.Hunk.File, include, exclude)
252-
if err != nil {
253-
return nil, errors.Wrap(err, "failed to check if file is included")
254-
}
255-
256-
if !included {
257-
continue
258-
}
259-
}
260-
261277
return result.UnsatisfiedRules, nil
262278
}
263279

rules.go

Lines changed: 63 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ package difflint
33
import (
44
"log"
55
"os"
6-
"sync"
76

87
"github.com/pkg/errors"
98
)
@@ -34,9 +33,8 @@ type Rule struct {
3433
}
3534

3635
// RulesMapFromHunks parses rules from the given hunks by file name and
37-
// returns the map of rules.
36+
// returns the map of rules and the set of all the target keys that are present.
3837
func RulesMapFromHunks(hunks []Hunk, options LintOptions) (map[string][]Rule, map[string]struct{}, error) {
39-
// Separate hunks by file name and construct a set of all the target keys that exist.
4038
targetsMap := make(map[string]struct{}, len(hunks))
4139
rangesMap := make(map[string][]Range, len(hunks))
4240
for _, hunk := range hunks {
@@ -49,87 +47,95 @@ func RulesMapFromHunks(hunks []Hunk, options LintOptions) (map[string][]Rule, ma
4947
rangesMap[hunk.File] = []Range{hunk.Range}
5048
}
5149

52-
// Populate rules map.
5350
rulesMap := make(map[string][]Rule, len(hunks))
54-
visited := make(map[string]struct{})
55-
var wg sync.WaitGroup
56-
for pathname, ranges := range rangesMap {
57-
rules, err := RulesFromFile(pathname, ranges, visited, &wg, options)
51+
err := Walk(".", nil, nil, func(file string, info os.FileInfo, err error) error {
5852
if err != nil {
59-
return nil, nil, err
53+
return err
54+
}
55+
56+
f, err := os.Open(file)
57+
if err != nil {
58+
return errors.Wrapf(err, "failed to open file %s", file)
59+
}
60+
defer f.Close()
61+
62+
templates, err := options.TemplatesFromFile(file)
63+
if err != nil {
64+
return errors.Wrapf(err, "failed to parse templates for file %s", file)
65+
}
66+
67+
tokens, err := lex(f, lexOptions{file, templates})
68+
if err != nil {
69+
return errors.Wrapf(err, "failed to lex file %s", file)
70+
}
71+
72+
rules, err := parseRules(file, tokens, rangesMap[file])
73+
if err != nil {
74+
return errors.Wrapf(err, "failed to parse rules for file %s", file)
6075
}
6176

6277
for _, rule := range rules {
63-
targetsMap[TargetKey(pathname, Target{ID: rule.ID})] = struct{}{}
78+
if rule.Hunk.File != file {
79+
continue
80+
}
81+
82+
ranges, ok := rangesMap[file]
83+
if !ok {
84+
continue
85+
}
86+
87+
for _, rng := range ranges {
88+
if !Intersects(rule.Hunk.Range, rng) {
89+
continue
90+
}
91+
92+
key := TargetKey(file, Target{
93+
File: &rule.Hunk.File,
94+
ID: rule.ID,
95+
})
96+
targetsMap[key] = struct{}{}
97+
}
6498
}
6599

66-
rulesMap[pathname] = rules
67-
}
100+
if len(rules) > 0 {
101+
rulesMap[file] = rules
102+
}
68103

69-
wg.Wait()
104+
return nil
105+
})
106+
if err != nil {
107+
return nil, nil, errors.Wrap(err, "failed to walk files")
108+
}
70109

71110
return rulesMap, targetsMap, nil
72111
}
73112

74113
// RulesFromFile parses rules from the given file and returns the list of rules.
75-
func RulesFromFile(pathname string, ranges []Range, visited map[string]struct{}, wg *sync.WaitGroup, options LintOptions) ([]Rule, error) {
76-
visited[pathname] = struct{}{}
77-
114+
func RulesFromFile(file string, ranges []Range, options LintOptions) ([]Rule, error) {
78115
// Parse rules for the file.
79-
log.Println("Parsing rules for file", pathname)
80-
file, err := os.Open(pathname)
116+
log.Println("parsing rules for file", file)
117+
f, err := os.Open(file)
81118
if err != nil {
82-
return nil, errors.Wrapf(err, "failed to open file %s", pathname)
119+
return nil, errors.Wrapf(err, "failed to open file %s", file)
83120
}
84121

85-
defer file.Close()
122+
defer f.Close()
86123

87-
templates, err := options.TemplatesFromFile(pathname)
124+
templates, err := options.TemplatesFromFile(file)
88125
if err != nil {
89-
return nil, errors.Wrapf(err, "failed to parse templates for file %s", pathname)
126+
return nil, errors.Wrapf(err, "failed to parse templates for file %s", file)
90127
}
91128

92-
tokens, err := lex(file, lexOptions{
93-
file: pathname,
94-
templates: templates,
95-
})
129+
tokens, err := lex(f, lexOptions{file, templates})
96130
if err != nil {
97-
return nil, errors.Wrapf(err, "failed to lex file %s", pathname)
131+
return nil, errors.Wrapf(err, "failed to lex file %s", file)
98132
}
99133

100-
rules, err := parseRules(pathname, tokens, ranges)
134+
rules, err := parseRules(file, tokens, ranges)
101135
if err != nil {
102-
return nil, errors.Wrapf(err, "failed to parse rules for file %s", pathname)
103-
}
104-
105-
var innerWg sync.WaitGroup // WaitGroup for inner goroutines
106-
for _, rule := range rules {
107-
for _, target := range rule.Targets {
108-
if target.File == nil {
109-
continue
110-
}
111-
112-
innerWg.Add(1)
113-
go func(pathname string) {
114-
defer innerWg.Done()
115-
116-
if _, ok := visited[pathname]; ok {
117-
return
118-
}
119-
120-
moreRules, err := RulesFromFile(pathname, nil, visited, &innerWg, options)
121-
if err != nil {
122-
return
123-
}
124-
125-
rules = append(rules, moreRules...)
126-
}(*target.File)
127-
}
136+
return nil, errors.Wrapf(err, "failed to parse rules for file %s", file)
128137
}
129138

130-
// Wait for all inner goroutines to complete before returning.
131-
innerWg.Wait()
132-
133139
// Add rules to the map.
134140
return rules, nil
135141
}

0 commit comments

Comments
 (0)