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  }