github.com/yoheimuta/protolint@v0.49.8-0.20240515023657-4ecaebb7575d/internal/addon/rules/indentRule.go (about) 1 package rules 2 3 import ( 4 "strings" 5 "unicode" 6 7 "github.com/yoheimuta/go-protoparser/v4/parser" 8 "github.com/yoheimuta/go-protoparser/v4/parser/meta" 9 10 "github.com/yoheimuta/protolint/linter/report" 11 "github.com/yoheimuta/protolint/linter/rule" 12 "github.com/yoheimuta/protolint/linter/visitor" 13 ) 14 15 const ( 16 // Use an indent of 2 spaces. 17 // See https://developers.google.com/protocol-buffers/docs/style#standard-file-formatting 18 defaultStyle = " " 19 ) 20 21 // IndentRule enforces a consistent indentation style. 22 type IndentRule struct { 23 RuleWithSeverity 24 style string 25 notInsertNewline bool 26 fixMode bool 27 } 28 29 // NewIndentRule creates a new IndentRule. 30 func NewIndentRule( 31 severity rule.Severity, 32 style string, 33 notInsertNewline bool, 34 fixMode bool, 35 ) IndentRule { 36 if len(style) == 0 { 37 style = defaultStyle 38 } 39 40 return IndentRule{ 41 RuleWithSeverity: RuleWithSeverity{severity: severity}, 42 style: style, 43 notInsertNewline: notInsertNewline, 44 fixMode: fixMode, 45 } 46 } 47 48 // ID returns the ID of this rule. 49 func (r IndentRule) ID() string { 50 return "INDENT" 51 } 52 53 // Purpose returns the purpose of this rule. 54 func (r IndentRule) Purpose() string { 55 return "Enforces a consistent indentation style." 56 } 57 58 // IsOfficial decides whether or not this rule belongs to the official guide. 59 func (r IndentRule) IsOfficial() bool { 60 return true 61 } 62 63 // Apply applies the rule to the proto. 64 func (r IndentRule) Apply( 65 proto *parser.Proto, 66 ) ([]report.Failure, error) { 67 base, err := visitor.NewBaseFixableVisitor(r.ID(), true, proto, string(r.Severity())) 68 if err != nil { 69 return nil, err 70 } 71 72 v := &indentVisitor{ 73 BaseFixableVisitor: base, 74 style: r.style, 75 fixMode: r.fixMode, 76 notInsertNewline: r.notInsertNewline, 77 indentFixes: make(map[int][]indentFix), 78 } 79 return visitor.RunVisitor(v, proto, r.ID()) 80 } 81 82 type indentFix struct { 83 currentChars int 84 replacement string 85 level int 86 pos meta.Position 87 isLast bool 88 } 89 90 type indentVisitor struct { 91 *visitor.BaseFixableVisitor 92 style string 93 currentLevel int 94 95 fixMode bool 96 notInsertNewline bool 97 indentFixes map[int][]indentFix 98 } 99 100 func (v indentVisitor) Finally() error { 101 if v.fixMode { 102 return v.fix() 103 } 104 return nil 105 } 106 107 func (v indentVisitor) VisitEnum(e *parser.Enum) (next bool) { 108 v.validateIndentLeading(e.Meta.Pos) 109 defer func() { v.validateIndentLast(e.Meta.LastPos) }() 110 for _, comment := range e.Comments { 111 v.validateIndentLeading(comment.Meta.Pos) 112 } 113 114 defer v.nest()() 115 for _, body := range e.EnumBody { 116 body.Accept(v) 117 } 118 return false 119 } 120 121 func (v indentVisitor) VisitEnumField(f *parser.EnumField) (next bool) { 122 v.validateIndentLeading(f.Meta.Pos) 123 for _, comment := range f.Comments { 124 v.validateIndentLeading(comment.Meta.Pos) 125 } 126 return false 127 } 128 129 func (v indentVisitor) VisitExtend(e *parser.Extend) (next bool) { 130 v.validateIndentLeading(e.Meta.Pos) 131 defer func() { v.validateIndentLast(e.Meta.LastPos) }() 132 for _, comment := range e.Comments { 133 v.validateIndentLeading(comment.Meta.Pos) 134 } 135 136 defer v.nest()() 137 for _, body := range e.ExtendBody { 138 body.Accept(v) 139 } 140 return false 141 } 142 143 func (v indentVisitor) VisitField(f *parser.Field) (next bool) { 144 v.validateIndentLeading(f.Meta.Pos) 145 for _, comment := range f.Comments { 146 v.validateIndentLeading(comment.Meta.Pos) 147 } 148 return false 149 } 150 151 func (v indentVisitor) VisitGroupField(f *parser.GroupField) (next bool) { 152 v.validateIndentLeading(f.Meta.Pos) 153 defer func() { v.validateIndentLast(f.Meta.LastPos) }() 154 for _, comment := range f.Comments { 155 v.validateIndentLeading(comment.Meta.Pos) 156 } 157 158 defer v.nest()() 159 for _, body := range f.MessageBody { 160 body.Accept(v) 161 } 162 return false 163 } 164 165 func (v indentVisitor) VisitImport(i *parser.Import) (next bool) { 166 v.validateIndentLeading(i.Meta.Pos) 167 for _, comment := range i.Comments { 168 v.validateIndentLeading(comment.Meta.Pos) 169 } 170 return false 171 } 172 173 func (v indentVisitor) VisitMapField(m *parser.MapField) (next bool) { 174 v.validateIndentLeading(m.Meta.Pos) 175 for _, comment := range m.Comments { 176 v.validateIndentLeading(comment.Meta.Pos) 177 } 178 return false 179 } 180 181 func (v indentVisitor) VisitMessage(m *parser.Message) (next bool) { 182 v.validateIndentLeading(m.Meta.Pos) 183 defer func() { v.validateIndentLast(m.Meta.LastPos) }() 184 for _, comment := range m.Comments { 185 v.validateIndentLeading(comment.Meta.Pos) 186 } 187 188 defer v.nest()() 189 for _, body := range m.MessageBody { 190 body.Accept(v) 191 } 192 return false 193 } 194 195 func (v indentVisitor) VisitOneof(o *parser.Oneof) (next bool) { 196 v.validateIndentLeading(o.Meta.Pos) 197 defer func() { v.validateIndentLast(o.Meta.LastPos) }() 198 for _, comment := range o.Comments { 199 v.validateIndentLeading(comment.Meta.Pos) 200 } 201 202 defer v.nest()() 203 for _, field := range o.OneofFields { 204 field.Accept(v) 205 } 206 return false 207 } 208 209 func (v indentVisitor) VisitOneofField(f *parser.OneofField) (next bool) { 210 v.validateIndentLeading(f.Meta.Pos) 211 for _, comment := range f.Comments { 212 v.validateIndentLeading(comment.Meta.Pos) 213 } 214 return false 215 } 216 217 func (v indentVisitor) VisitOption(o *parser.Option) (next bool) { 218 v.validateIndentLeading(o.Meta.Pos) 219 for _, comment := range o.Comments { 220 v.validateIndentLeading(comment.Meta.Pos) 221 } 222 return false 223 } 224 225 func (v indentVisitor) VisitPackage(p *parser.Package) (next bool) { 226 v.validateIndentLeading(p.Meta.Pos) 227 for _, comment := range p.Comments { 228 v.validateIndentLeading(comment.Meta.Pos) 229 } 230 return false 231 } 232 233 func (v indentVisitor) VisitReserved(r *parser.Reserved) (next bool) { 234 v.validateIndentLeading(r.Meta.Pos) 235 for _, comment := range r.Comments { 236 v.validateIndentLeading(comment.Meta.Pos) 237 } 238 return false 239 } 240 241 func (v indentVisitor) VisitRPC(r *parser.RPC) (next bool) { 242 v.validateIndentLeading(r.Meta.Pos) 243 defer func() { 244 line := v.Fixer.Lines()[r.Meta.LastPos.Line-1] 245 runes := []rune(line) 246 for i := r.Meta.LastPos.Column - 2; 0 < i; i-- { 247 r := runes[i] 248 if r == '{' || r == ')' { 249 // skip validating the indentation when the line ends with {}, {};, or ); 250 return 251 } 252 if r == '}' || unicode.IsSpace(r) { 253 continue 254 } 255 break 256 } 257 v.validateIndentLast(r.Meta.LastPos) 258 }() 259 for _, comment := range r.Comments { 260 v.validateIndentLeading(comment.Meta.Pos) 261 } 262 263 defer v.nest()() 264 for _, body := range r.Options { 265 body.Accept(v) 266 } 267 return false 268 } 269 270 func (v indentVisitor) VisitService(s *parser.Service) (next bool) { 271 v.validateIndentLeading(s.Meta.Pos) 272 defer func() { v.validateIndentLast(s.Meta.LastPos) }() 273 for _, comment := range s.Comments { 274 v.validateIndentLeading(comment.Meta.Pos) 275 } 276 277 defer v.nest()() 278 for _, body := range s.ServiceBody { 279 body.Accept(v) 280 } 281 return false 282 } 283 284 func (v indentVisitor) VisitSyntax(s *parser.Syntax) (next bool) { 285 v.validateIndentLeading(s.Meta.Pos) 286 for _, comment := range s.Comments { 287 v.validateIndentLeading(comment.Meta.Pos) 288 } 289 return false 290 } 291 292 func (v indentVisitor) validateIndentLeading( 293 pos meta.Position, 294 ) { 295 v.validateIndent(pos, false) 296 } 297 298 func (v indentVisitor) validateIndentLast( 299 pos meta.Position, 300 ) { 301 v.validateIndent(pos, true) 302 } 303 304 func (v indentVisitor) validateIndent( 305 pos meta.Position, 306 isLast bool, 307 ) { 308 line := v.Fixer.Lines()[pos.Line-1] 309 leading := "" 310 for _, r := range string([]rune(line)[:pos.Column-1]) { 311 if unicode.IsSpace(r) { 312 leading += string(r) 313 } 314 } 315 316 indentation := strings.Repeat(v.style, v.currentLevel) 317 v.indentFixes[pos.Line-1] = append(v.indentFixes[pos.Line-1], indentFix{ 318 currentChars: len(leading), 319 replacement: indentation, 320 level: v.currentLevel, 321 pos: pos, 322 isLast: isLast, 323 }) 324 325 if leading == indentation { 326 return 327 } 328 if 1 < len(v.indentFixes[pos.Line-1]) && v.notInsertNewline { 329 return 330 } 331 if len(v.indentFixes[pos.Line-1]) == 1 { 332 v.AddFailuref( 333 pos, 334 `Found an incorrect indentation style "%s". "%s" is correct.`, 335 leading, 336 indentation, 337 ) 338 } else { 339 v.AddFailuref( 340 pos, 341 `Found a possible incorrect indentation style. Inserting a new line is recommended.`, 342 ) 343 } 344 } 345 346 func (v *indentVisitor) nest() func() { 347 v.currentLevel++ 348 return func() { 349 v.currentLevel-- 350 } 351 } 352 353 func (v indentVisitor) fix() error { 354 var shouldFixed bool 355 356 v.Fixer.ReplaceAll(func(lines []string) []string { 357 var fixedLines []string 358 for i, line := range lines { 359 lines := []string{line} 360 if fixes, ok := v.indentFixes[i]; ok { 361 lines[0] = fixes[0].replacement + line[fixes[0].currentChars:] 362 shouldFixed = true 363 364 if 1 < len(fixes) && !v.notInsertNewline { 365 // compose multiple lines in reverse order from right to left on one line. 366 var rlines []string 367 for j := len(fixes) - 1; 0 <= j; j-- { 368 indentation := strings.Repeat(v.style, fixes[j].level) 369 if fixes[j].isLast { 370 // deal with last position followed by ';'. See https://github.com/yoheimuta/protolint/issues/99 371 for line[fixes[j].pos.Column-1] == ';' { 372 fixes[j].pos.Column-- 373 } 374 } 375 376 endColumn := len(line) 377 if j < len(fixes)-1 { 378 endColumn = fixes[j+1].pos.Column - 1 379 } 380 text := line[fixes[j].pos.Column-1 : endColumn] 381 text = strings.TrimRightFunc(text, func(r rune) bool { 382 // removing right spaces is a possible side effect that users do not expect, 383 // but it's probably acceptable and usually recommended. 384 return unicode.IsSpace(r) 385 }) 386 387 rlines = append(rlines, indentation+text) 388 } 389 390 // sort the multiple lines in order 391 lines = []string{} 392 for j := len(rlines) - 1; 0 <= j; j-- { 393 lines = append(lines, rlines[j]) 394 } 395 } 396 } 397 fixedLines = append(fixedLines, lines...) 398 } 399 return fixedLines 400 }) 401 402 if !shouldFixed { 403 return nil 404 } 405 return v.BaseFixableVisitor.Finally() 406 }