forked from skeema/skeema
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cmd_lint.go
211 lines (193 loc) · 7.5 KB
/
cmd_lint.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
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
package main
import (
"fmt"
log "github.com/sirupsen/logrus"
"github.com/skeema/mybase"
"github.com/skeema/skeema/dumper"
"github.com/skeema/skeema/fs"
"github.com/skeema/skeema/linter"
"github.com/skeema/skeema/workspace"
"github.com/skeema/tengo"
)
func init() {
summary := "Check for problems in filesystem representation of database objects"
desc := "Checks for problems in filesystem representation of database objects. A set of " +
"linter rules are run against all objects. Each rule may be configured to " +
"generate an error, a warning, or be ignored entirely. Statements that contain " +
"invalid SQL, or otherwise return an error from the database, are always flagged " +
"as linter errors.\n\n" +
"By default, this command also reformats statements to their canonical form, " +
"just like `skeema format`.\n\n" +
"This command relies on accessing database instances to test the SQL DDL in a " +
"temporary location. See the --workspace option for more information.\n\n" +
"You may optionally pass an environment name as a CLI arg. This will affect " +
"which section of .skeema config files is used for linter configuration and " +
"workspace selection. For example, running `skeema lint staging` will " +
"apply config directives from the [staging] section of config files, as well as " +
"any sectionless directives at the top of the file. If no environment name is " +
"supplied, the default is \"production\".\n\n" +
"An exit code of 0 will be returned if no errors or warnings were emitted and all " +
"files were already formatted properly; 1 if any warnings were emitted and/or " +
"some files were reformatted; or 2+ if any errors were emitted for any reason."
cmd := mybase.NewCommand("lint", summary, desc, LintHandler)
linter.AddCommandOptions(cmd)
cmd.AddOption(mybase.BoolOption("format", 0, true, "Reformat SQL statements to match canonical SHOW CREATE"))
cmd.AddOption(mybase.BoolOption("strip-partitioning", 0, false, "Remove PARTITION BY clauses from *.sql files").Hidden())
workspace.AddCommandOptions(cmd)
cmd.AddArg("environment", "production", false)
CommandSuite.AddSubCommand(cmd)
}
// LintHandler is the handler method for `skeema lint`
func LintHandler(cfg *mybase.Config) error {
dir, err := fs.ParseDir(".", cfg)
if err != nil {
return err
}
result := lintWalker(dir, 5)
switch {
case len(result.Exceptions) > 0:
exitCode := CodeFatalError
for _, err := range result.Exceptions {
if _, ok := err.(linter.ConfigError); ok {
exitCode = CodeBadConfig
}
}
return NewExitValue(exitCode, "Skipped %s due to fatal errors",
countAndNoun(len(result.Exceptions), "operation", "operations"),
)
case result.ErrorCount > 0 && result.WarningCount > 0:
return NewExitValue(CodeFatalError, "Found %s and %s",
countAndNoun(result.ErrorCount, "error", "errors"),
countAndNoun(result.WarningCount, "warning", "warnings"),
)
case result.ErrorCount > 0:
return NewExitValue(CodeFatalError, "Found %s",
countAndNoun(result.ErrorCount, "error", "errors"),
)
case result.WarningCount > 0:
return NewExitValue(CodePartialError, "Found %s",
countAndNoun(result.WarningCount, "warning", "warnings"),
)
case result.ReformatCount > 0:
return NewExitValue(CodeDifferencesFound, "")
}
return nil
}
func lintWalker(dir *fs.Dir, maxDepth int) *linter.Result {
if dir.ParseError != nil {
log.Error(fmt.Sprintf("Skipping directory %s due to error: %s", dir.RelPath(), dir.ParseError))
return linter.BadConfigResult(dir, dir.ParseError)
}
log.Infof("Linting %s", dir)
result := lintDir(dir)
for _, err := range result.Exceptions {
log.Error(fmt.Sprintf("Skipping directory %s due to error: %s", dir.RelPath(), err))
}
for _, annotation := range result.Annotations {
annotation.Log()
}
for _, dl := range result.DebugLogs {
log.Debug(dl)
}
// Don't recurse into subdirs if there was something fatally wrong
if len(result.Exceptions) > 0 {
return result
}
var subdirErr error
if subdirs, err := dir.Subdirs(); err != nil {
subdirErr = fmt.Errorf("Cannot list subdirs of %s: %s", dir, err)
} else if len(subdirs) > 0 && maxDepth <= 0 {
subdirErr = fmt.Errorf("Not walking subdirs of %s: max depth reached", dir)
} else {
for _, sub := range subdirs {
result.Merge(lintWalker(sub, maxDepth-1))
}
}
if subdirErr != nil {
log.Error(subdirErr)
result.Fatal(subdirErr)
}
return result
}
// lintDir lints all logical schemas in dir, optionally also reformatting
// SQL statements along the way. A combined result for the directory is
// returned. This function does not recurse into subdirs.
func lintDir(dir *fs.Dir) *linter.Result {
opts, err := linter.OptionsForDir(dir)
if err != nil && len(dir.LogicalSchemas) > 0 {
return linter.BadConfigResult(dir, err)
}
// Get workspace options for dir. This involves connecting to the first
// defined instance, unless configured to use local Docker.
var wsOpts workspace.Options
if len(dir.LogicalSchemas) > 0 {
var inst *tengo.Instance
if wsType, _ := dir.Config.GetEnum("workspace", "temp-schema", "docker"); wsType != "docker" || !dir.Config.Changed("flavor") {
if inst, err = dir.FirstInstance(); err != nil {
return linter.BadConfigResult(dir, err)
} else if inst == nil {
return linter.BadConfigResult(dir, fmt.Errorf("No host defined for environment %q", dir.Config.Get("environment")))
}
}
if wsOpts, err = workspace.OptionsForDir(dir, inst); err != nil {
return linter.BadConfigResult(dir, err)
}
}
result := &linter.Result{}
for n, logicalSchema := range dir.LogicalSchemas {
// Convert the logical schema from the filesystem into a real schema, using a
// workspace
wsSchema, err := workspace.ExecLogicalSchema(logicalSchema, wsOpts)
if err != nil {
result.Fatal(err)
continue
}
result.AnnotateStatementErrors(wsSchema.Failures, opts)
// Reformat statements if requested. This must be done prior to checking for
// problems. Otherwise, the line offsets in annotations can be wrong.
// TODO: support format for multiple logical schemas per dir
if dir.Config.GetBool("format") && n == 0 {
dumpOpts := dumper.Options{
IncludeAutoInc: true,
IgnoreTable: opts.IgnoreTable,
}
if dir.Config.GetBool("strip-partitioning") {
dumpOpts.Partitioning = tengo.PartitioningRemove
}
dumpOpts.IgnoreKeys(wsSchema.FailedKeys())
result.ReformatCount, err = dumper.DumpSchema(wsSchema.Schema, dir, dumpOpts)
if err != nil {
log.Errorf("Skipping format operation for %s: %s", dir, err)
}
}
// Check for problems
subresult := linter.CheckSchema(wsSchema, opts)
result.Merge(subresult)
}
// Add warnings for any unsupported combinations of schema names, for example
// USE commands or dbname prefixes in CREATEs in a dir that also configures
// schema name in .skeema
result.AnnotateMixedSchemaNames(dir, opts)
// Add warning annotations for unparseable statements (unless we hit an
// exception, in which case skip it to avoid extra noise!)
if len(result.Exceptions) == 0 {
for _, stmt := range dir.IgnoredStatements {
note := linter.Note{
Summary: "Unable to parse statement",
Message: "Ignoring unsupported or unparseable SQL statement",
}
result.Annotate(stmt, linter.SeverityWarning, "", note)
}
}
// Make sure the problem messages have a deterministic order.
result.SortByFile()
return result
}
func countAndNoun(n int, singular, plural string) string {
if n == 1 {
return fmt.Sprintf("1 %s", singular)
} else if n == 0 {
return fmt.Sprintf("no %s", plural)
}
return fmt.Sprintf("%d %s", n, plural)
}