forked from tetafro/godot
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathgodot.go
135 lines (116 loc) · 3.56 KB
/
godot.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
// Package godot checks if comments contain a period at the end of the last
// sentence if needed.
package godot
import (
"fmt"
"go/ast"
"go/token"
"io/ioutil"
"os"
"regexp"
"sort"
"strings"
)
// NOTE: Line and column indexes are 1-based.
// NOTE: Errors `invalid line number inside comment...` should never happen.
// Their goal is to prevent panic, if there's a bug with array indexes.
// Issue contains a description of linting error and a recommended replacement.
type Issue struct {
Pos token.Position
Message string
Replacement string
}
// position is a position inside a comment (might be multiline comment).
type position struct {
line int // starts at 1
column int // starts at 1, byte count
}
// comment is an internal representation of AST comment entity with additional
// data attached. The latter is used for creating a full replacement for
// the line with issues.
type comment struct {
lines []string // unmodified lines from file
text string // concatenated `lines` with special parts excluded
start token.Position // position of the first symbol in comment
decl bool // whether comment is a declaration comment
}
// Run runs this linter on the provided code.
func Run(file *ast.File, fset *token.FileSet, settings Settings) ([]Issue, error) {
pf, err := newParsedFile(file, fset)
if err == errEmptyInput || err == errUnsuitableInput {
return nil, nil
}
if err != nil {
return nil, fmt.Errorf("parse input file: %v", err)
}
exclude := make([]*regexp.Regexp, len(settings.Exclude))
for i := 0; i < len(settings.Exclude); i++ {
exclude[i], err = regexp.Compile(settings.Exclude[i])
if err != nil {
return nil, fmt.Errorf("invalid regexp: %v", err)
}
}
comments := pf.getComments(settings.Scope, exclude)
issues := checkComments(comments, settings)
sortIssues(issues)
return issues, nil
}
// Fix fixes all issues and returns new version of file content.
func Fix(path string, file *ast.File, fset *token.FileSet, settings Settings) ([]byte, error) {
// Read file
content, err := ioutil.ReadFile(path) // nolint: gosec
if err != nil {
return nil, fmt.Errorf("read file: %v", err)
}
if len(content) == 0 {
return nil, nil
}
issues, err := Run(file, fset, settings)
if err != nil {
return nil, fmt.Errorf("run linter: %v", err)
}
// slice -> map
m := map[int]Issue{}
for _, iss := range issues {
m[iss.Pos.Line] = iss
}
// Replace lines from issues
fixed := make([]byte, 0, len(content))
for i, line := range strings.Split(string(content), "\n") {
newline := line
if iss, ok := m[i+1]; ok {
newline = iss.Replacement
}
fixed = append(fixed, []byte(newline+"\n")...)
}
fixed = fixed[:len(fixed)-1] // trim last "\n"
return fixed, nil
}
// Replace rewrites original file with it's fixed version.
func Replace(path string, file *ast.File, fset *token.FileSet, settings Settings) error {
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("check file: %v", err)
}
mode := info.Mode()
fixed, err := Fix(path, file, fset, settings)
if err != nil {
return fmt.Errorf("fix issues: %v", err)
}
if err := ioutil.WriteFile(path, fixed, mode); err != nil {
return fmt.Errorf("write file: %v", err)
}
return nil
}
// sortIssues sorts by filename, line and column.
func sortIssues(iss []Issue) {
sort.Slice(iss, func(i, j int) bool {
if iss[i].Pos.Filename != iss[j].Pos.Filename {
return iss[i].Pos.Filename < iss[j].Pos.Filename
}
if iss[i].Pos.Line != iss[j].Pos.Line {
return iss[i].Pos.Line < iss[j].Pos.Line
}
return iss[i].Pos.Column < iss[j].Pos.Column
})
}