github.com/blend/go-sdk@v1.20220411.3/profanity/profanity.go (about) 1 /* 2 3 Copyright (c) 2022 - Present. Blend Labs, Inc. All rights reserved 4 Use of this source code is governed by a MIT license that can be found in the LICENSE file. 5 6 */ 7 8 package profanity 9 10 import ( 11 "fmt" 12 "io" 13 "os" 14 "path/filepath" 15 "strings" 16 17 "gopkg.in/yaml.v3" 18 19 "github.com/blend/go-sdk/ansi" 20 "github.com/blend/go-sdk/ex" 21 ) 22 23 // New creates a new profanity engine with a given set of config options. 24 func New(options ...Option) *Profanity { 25 var p Profanity 26 for _, option := range options { 27 option(&p) 28 } 29 return &p 30 } 31 32 // Profanity parses rules from the filesystem and applies them to a given root path. 33 // Creating a full rules set. 34 type Profanity struct { 35 Config Config 36 Stdout io.Writer 37 Stderr io.Writer 38 } 39 40 // Process processes the profanity rules. 41 func (p *Profanity) Process() error { 42 p.Verbosef("using rules file: %q", p.Config.RulesFileOrDefault()) 43 if ruleFilter := p.Config.Rules.String(); ruleFilter != "" { 44 p.Verbosef("using rule filter: %s", ruleFilter) 45 } 46 if fileFilter := p.Config.Files.String(); fileFilter != "" { 47 p.Verbosef("using file filter: %s", fileFilter) 48 } 49 if dirFilter := p.Config.Dirs.String(); dirFilter != "" { 50 p.Verbosef("using dir filter: %s", dirFilter) 51 } 52 err := p.Walk(p.Config.RootOrDefault()) 53 if err != nil { 54 p.Verbosef("profanity %s!", ansi.Red("failed")) 55 return err 56 } 57 p.Verbosef("profanity %s!", ansi.Green("ok")) 58 return nil 59 } 60 61 // Walk walks a given path, inheriting a set of rules. 62 func (p *Profanity) Walk(path string, rules ...RuleSpec) error { 63 dirs, files, err := ListDir(path) 64 if err != nil { 65 return ex.New("profanity; invalid walk path", ex.OptMessagef("path: %q", path), ex.OptInner(err)) 66 } 67 68 var didFail bool 69 var fullFilePath string 70 for _, file := range files { 71 if file.Name() == p.Config.RulesFileOrDefault() { 72 fullFilePath = filepath.Join(path, file.Name()) 73 p.Debugf("reading rules file: %q", filepath.Join(path, fullFilePath)) 74 foundRules, err := p.ReadRuleSpecsFile(fullFilePath) 75 if err != nil { 76 return err 77 } 78 rules = append(rules, foundRules...) 79 } 80 } 81 82 for _, file := range files { 83 if file.Name() == p.Config.RulesFileOrDefault() { 84 continue 85 } 86 87 fullFilePath = filepath.Join(path, file.Name()) 88 if p.Config.Files.Allow(fullFilePath) { 89 contents, err := os.ReadFile(fullFilePath) 90 if err != nil { 91 return err 92 } 93 for _, rule := range rules { 94 if p.Config.Rules.Allow(rule.ID) { 95 if rule.Files.Allow(fullFilePath) { 96 p.Debugf("%s; checking %s", rule.ID, fullFilePath) 97 res := rule.Check(fullFilePath, contents) 98 if res.Err != nil { 99 return res.Err 100 } 101 if !res.OK { 102 didFail = true 103 p.Errorf("%v\n", p.FormatRuleResultFailure(rule, res)) 104 if p.Config.ExitFirstOrDefault() { 105 return ErrFailure 106 } 107 } 108 } 109 } 110 } 111 } 112 } 113 114 var fullDirPath string 115 for _, dir := range dirs { 116 if dir.Name() == ".git" { 117 continue 118 } 119 if strings.HasPrefix(dir.Name(), "_") { 120 continue 121 } 122 fullDirPath = filepath.Join(path, dir.Name()) 123 if p.Config.Dirs.Allow(fullDirPath) { 124 if err := p.Walk(fullDirPath, rules...); err != nil { 125 if err != ErrFailure || p.Config.ExitFirstOrDefault() { 126 return err 127 } 128 didFail = true 129 } 130 } 131 } 132 133 if didFail { 134 return ErrFailure 135 } 136 return nil 137 } 138 139 // ReadRuleSpecsFile reads rules from a file path. 140 // 141 // It is expected to be passed the fully qualified path for the rules file. 142 func (p *Profanity) ReadRuleSpecsFile(filename string) (rules []RuleSpec, err error) { 143 contents, readErr := os.Open(filename) 144 if readErr != nil { 145 err = ex.New(readErr, ex.OptMessagef("file: %s", filename)) 146 return 147 } 148 defer contents.Close() 149 rules, err = p.ReadRuleSpecsFromReader(filename, contents) 150 return 151 } 152 153 // ReadRuleSpecsFromReader reads rules from a reader. 154 func (p *Profanity) ReadRuleSpecsFromReader(filename string, reader io.Reader) (rules []RuleSpec, err error) { 155 fileRules := make(RuleSpecFile) 156 decoder := yaml.NewDecoder(reader) 157 decoder.KnownFields(true) 158 yamlErr := decoder.Decode(&fileRules) 159 if yamlErr != nil { 160 err = ex.New("cannot unmarshal rules file", ex.OptMessagef("file: %s", filename), ex.OptInnerClass(yamlErr)) 161 return 162 } 163 for _, rule := range fileRules.Rules() { 164 rule.SourceFile = filename 165 if validationErr := rule.Validate(); validationErr != nil { 166 p.Debugf("rule file %q fails validation", filename) 167 err = validationErr 168 return 169 } 170 rules = append(rules, rule) 171 } 172 return 173 } 174 175 // FormatRuleResultFailure formats a rule result with the rule that produced it. 176 func (p Profanity) FormatRuleResultFailure(r RuleSpec, rr RuleResult) error { 177 if rr.OK { 178 return nil 179 } 180 var lines []string 181 lines = append(lines, fmt.Sprintf("%s:%d", ansi.Bold(ansi.ColorWhite, rr.File), rr.Line)) 182 lines = append(lines, fmt.Sprintf("\t%s: %s", ansi.LightBlack("id"), r.ID)) 183 if r.Description != "" { 184 lines = append(lines, fmt.Sprintf("\t%s: %s", ansi.LightBlack("description"), r.Description)) 185 } 186 lines = append(lines, fmt.Sprintf("\t%s: %s", ansi.LightBlack("status"), ansi.Red("failed"))) 187 lines = append(lines, fmt.Sprintf("\t%s: %s", ansi.LightBlack("rule"), rr.Message)) 188 return fmt.Errorf(strings.Join(lines, "\n")) 189 } 190 191 // Verbosef prints a verbose message. 192 func (p *Profanity) Verbosef(format string, args ...interface{}) { 193 if p.Config.VerboseOrDefault() { 194 p.Printf("[VERBOSE] "+format+"\n", args...) 195 } 196 } 197 198 // Debugf prints a debug message. 199 func (p *Profanity) Debugf(format string, args ...interface{}) { 200 if p.Config.DebugOrDefault() { 201 p.Printf("[DEBUG] "+format+"\n", args...) 202 } 203 } 204 205 // Printf writes to the output stream. 206 func (p *Profanity) Printf(format string, args ...interface{}) { 207 if p.Stdout != nil { 208 fmt.Fprintf(p.Stdout, format, args...) 209 } 210 } 211 212 // Errorf writes to the error output stream. 213 func (p *Profanity) Errorf(format string, args ...interface{}) { 214 if p.Stderr != nil { 215 fmt.Fprintf(p.Stderr, format, args...) 216 } 217 }