github.com/yoheimuta/protolint@v0.49.8-0.20240515023657-4ecaebb7575d/linter/fixer/fixer.go (about)

     1  package fixer
     2  
     3  import (
     4  	"bytes"
     5  	"io/ioutil"
     6  	"strings"
     7  
     8  	"github.com/yoheimuta/go-protoparser/v4/lexer"
     9  	"github.com/yoheimuta/go-protoparser/v4/parser/meta"
    10  
    11  	"github.com/yoheimuta/go-protoparser/v4/parser"
    12  
    13  	"github.com/yoheimuta/protolint/internal/osutil"
    14  )
    15  
    16  // TextEdit represents the replacement of the code between Pos and End with the new text.
    17  type TextEdit struct {
    18  	Pos     int
    19  	End     int // Inclusive. If the target is abc, pos and end are 1 and 3, respectively.
    20  	NewText []byte
    21  }
    22  
    23  // Fixer provides the ways to operate the proto content.
    24  type Fixer interface {
    25  	// NOTE: This method is insufficient to process unexpected multi-line contents.
    26  	ReplaceText(line int, old, new string)
    27  	ReplaceAll(proc func(lines []string) []string)
    28  
    29  	SearchAndReplace(startPos meta.Position, lex func(lex *lexer.Lexer) TextEdit) error
    30  	ReplaceContent(proc func(content []byte) []byte)
    31  
    32  	Lines() []string
    33  }
    34  
    35  // Fixing adds the way to modify the proto file to Fixer.
    36  type Fixing interface {
    37  	Fixer
    38  	Finally() error
    39  }
    40  
    41  // NewFixing creates a fixing, depending on fixMode.
    42  func NewFixing(fixMode bool, proto *parser.Proto) (Fixing, error) {
    43  	if fixMode {
    44  		return NewBaseFixing(proto.Meta.Filename)
    45  	}
    46  	return NopFixing{}, nil
    47  }
    48  
    49  // BaseFixing implements Fixing.
    50  type BaseFixing struct {
    51  	content    []byte
    52  	lineEnding string
    53  	fileName   string
    54  	textEdits  []TextEdit
    55  }
    56  
    57  // NewBaseFixing creates a BaseFixing.
    58  func NewBaseFixing(protoFileName string) (*BaseFixing, error) {
    59  	content, err := ioutil.ReadFile(protoFileName)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  
    64  	// Regardless of the actual dominant line ending, the fixer will go with LF
    65  	// because the parser recognizes only LF as a line ending.
    66  	//
    67  	// It will work for most cases like used LF, CRLF, and a mix of LF and CRLF.
    68  	// See also https://github.com/yoheimuta/protolint/issues/280.
    69  	lineEnding := "\n"
    70  
    71  	return &BaseFixing{
    72  		content:    content,
    73  		lineEnding: lineEnding,
    74  		fileName:   protoFileName,
    75  	}, nil
    76  }
    77  
    78  // ReplaceText replaces the text at the line.
    79  func (f *BaseFixing) ReplaceText(line int, old, new string) {
    80  	lines := strings.Split(string(f.content), f.lineEnding)
    81  	lines[line-1] = strings.Replace(lines[line-1], old, new, 1)
    82  	f.content = []byte(strings.Join(lines, f.lineEnding))
    83  }
    84  
    85  // ReplaceAll replaces the lines.
    86  func (f *BaseFixing) ReplaceAll(proc func(lines []string) []string) {
    87  	lines := strings.Split(string(f.content), f.lineEnding)
    88  	lines = proc(lines)
    89  	f.content = []byte(strings.Join(lines, f.lineEnding))
    90  }
    91  
    92  // SearchAndReplace locates text edits and replaces with them.
    93  func (f *BaseFixing) SearchAndReplace(startPos meta.Position, lex func(lex *lexer.Lexer) TextEdit) error {
    94  	r := bytes.NewReader(f.content)
    95  	_, err := r.Seek(int64(startPos.Offset), 0)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	l := lexer.NewLexer(r)
   101  	t := lex(l)
   102  	t.Pos += startPos.Offset
   103  	t.End += startPos.Offset
   104  	f.textEdits = append(f.textEdits, t)
   105  	return nil
   106  }
   107  
   108  // ReplaceContent replaces entire content.
   109  func (f *BaseFixing) ReplaceContent(proc func(content []byte) []byte) {
   110  	f.content = proc(f.content)
   111  }
   112  
   113  // Lines returns the line format of f.content.
   114  func (f *BaseFixing) Lines() []string {
   115  	return strings.Split(string(f.content), f.lineEnding)
   116  }
   117  
   118  // Finally writes the fixed content to the file.
   119  func (f *BaseFixing) Finally() error {
   120  	diff := 0
   121  	for _, t := range f.textEdits {
   122  		t.Pos += diff
   123  		t.End += diff
   124  		f.content = append(f.content[:t.Pos], append(t.NewText, f.content[t.End+1:]...)...)
   125  		diff += len(t.NewText) - (t.End - t.Pos + 1)
   126  	}
   127  	return osutil.WriteExistingFile(f.fileName, f.content)
   128  }
   129  
   130  // Replace records a textedit to replace the old with the next later.
   131  func (f *BaseFixing) Replace(t TextEdit) {
   132  	f.textEdits = append(f.textEdits, t)
   133  }
   134  
   135  // Content returns f.content.
   136  func (f *BaseFixing) Content() []byte {
   137  	return f.content
   138  }
   139  
   140  // LineEnding is a detected line ending.
   141  func (f *BaseFixing) LineEnding() string {
   142  	return f.lineEnding
   143  }
   144  
   145  // NopFixing does nothing.
   146  type NopFixing struct{}
   147  
   148  // ReplaceText noop
   149  func (f NopFixing) ReplaceText(line int, old, new string) {}
   150  
   151  // ReplaceAll noop
   152  func (f NopFixing) ReplaceAll(proc func(lines []string) []string) {}
   153  
   154  // SearchAndReplace noop
   155  func (f NopFixing) SearchAndReplace(startPos meta.Position, lex func(lexer *lexer.Lexer) TextEdit) error {
   156  	return nil
   157  }
   158  
   159  // ReplaceContent noop.
   160  func (f NopFixing) ReplaceContent(proc func(content []byte) []byte) {}
   161  
   162  // Lines noop.
   163  func (f NopFixing) Lines() []string { return []string{} }
   164  
   165  // Finally noop
   166  func (f NopFixing) Finally() error { return nil }