Skip to content

Commit

Permalink
Merge pull request #1456 from onflow/jribbink/backport-lint
Browse files Browse the repository at this point in the history
Backport Add `flow cadence lint` command (#1448) to `master`
  • Loading branch information
jribbink authored Mar 15, 2024
2 parents 0f21381 + 5362897 commit b19f215
Show file tree
Hide file tree
Showing 11 changed files with 1,238 additions and 19 deletions.
4 changes: 0 additions & 4 deletions cmd/flow/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -156,8 +156,4 @@ func main() {
if err := cmd.Execute(); err != nil {
util.Exit(1, err.Error())
}

if status := *test.TestCommand.Status; status > 0 {
os.Exit(int(status))
}
}
4 changes: 2 additions & 2 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ require (
github.com/getsentry/sentry-go v0.27.0
github.com/go-git/go-git/v5 v5.11.0
github.com/gosuri/uilive v0.0.4
github.com/logrusorgru/aurora/v4 v4.0.0
github.com/manifoldco/promptui v0.9.0
github.com/onflow/cadence v0.42.9
github.com/onflow/cadence-tools/languageserver v0.33.3
Expand Down Expand Up @@ -154,7 +155,6 @@ require (
github.com/libp2p/go-msgio v0.3.0 // indirect
github.com/lmars/go-slip10 v0.0.0-20190606092855-400ba44fee12 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
github.com/logrusorgru/aurora/v4 v4.0.0 // indirect
github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.13 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand Down Expand Up @@ -199,7 +199,7 @@ require (
github.com/prometheus/procfs v0.12.0 // indirect
github.com/psiemens/graceland v1.0.0 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/rivo/uniseg v0.4.4 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/rogpeppe/go-internal v1.11.0 // indirect
github.com/rs/cors v1.8.0 // indirect
github.com/sethvargo/go-retry v0.2.3 // indirect
Expand Down
3 changes: 2 additions & 1 deletion go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1038,8 +1038,9 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis=
github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/rjeczalik/notify v0.9.1/go.mod h1:rKwnCoCGeuQnwBtTSPL9Dad03Vh2n40ePRrjvIXnJho=
github.com/robertkrimen/otto v0.0.0-20170205013659-6a77b7cbc37d/go.mod h1:xvqspoSXJTIpemEonrMDFq6XzwHYYgToXWj5eRX1OtY=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
Expand Down
1 change: 1 addition & 0 deletions internal/cadence/cadence.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,4 +44,5 @@ var Cmd = &cobra.Command{

func init() {
Cmd.AddCommand(languageserver.Cmd)
lintCommand.AddToParent(Cmd)
}
236 changes: 236 additions & 0 deletions internal/cadence/lint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,236 @@
/*
* Flow CLI
*
* Copyright 2019 Dapper Labs, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package cadence

import (
"fmt"
"strings"

"golang.org/x/exp/slices"

"github.com/logrusorgru/aurora/v4"
"github.com/spf13/cobra"

"github.com/onflow/cadence/tools/analysis"
"github.com/onflow/flowkit"
"github.com/onflow/flowkit/output"

"github.com/onflow/flow-cli/internal/command"
)

type lintFlagsCollection struct{}

type fileResult struct {
FilePath string
Diagnostics []analysis.Diagnostic
}

type lintResult struct {
Results []fileResult
exitCode int
}

var _ command.ResultWithExitCode = &lintResult{}

var lintFlags = lintFlagsCollection{}

var lintCommand = &command.Command{
Cmd: &cobra.Command{
Use: "lint [files]",
Short: "Lint Cadence code to identify potential issues or errors",
Example: "flow cadence lint **/*.cdc",
Args: cobra.MinimumNArgs(1),
},
Flags: &lintFlags,
RunS: lint,
}

type severity string

const (
errorSeverity severity = "error"
warningSeverity severity = "warning"
)

func lint(
args []string,
globalFlags command.GlobalFlags,
logger output.Logger,
flow flowkit.Services,
state *flowkit.State,
) (command.Result, error) {
filePaths := args
result, err := lintFiles(state, filePaths...)
if err != nil {
return nil, err
}

return result, nil
}

func lintFiles(
state *flowkit.State,
filePaths ...string,
) (
*lintResult,
error,
) {
l := newLinter(state)
results := make([]fileResult, 0)
exitCode := 0

for _, location := range filePaths {
diagnostics, err := l.lintFile(location)
if err != nil {
return nil, err
}

// Sort for consistent output
sortDiagnostics(diagnostics)
results = append(results, fileResult{
FilePath: location,
Diagnostics: diagnostics,
})

// Set the exitCode to 1 if any of the diagnostics are error-level
// In the future, this may be configurable
for _, diagnostic := range diagnostics {
severity := getDiagnosticSeverity(diagnostic)
if severity == errorSeverity {
exitCode = 1
break
}
}
}

return &lintResult{
Results: results,
exitCode: exitCode,
}, nil
}

func getDiagnosticSeverity(
diagnostic analysis.Diagnostic,
) severity {
if isErrorDiagnostic(diagnostic) {
return errorSeverity
}
return warningSeverity
}

// Sort diagnostics in order of precedence: start pos -> category -> message
func sortDiagnostics(
diagnostics []analysis.Diagnostic,
) {
slices.SortFunc(diagnostics, func(a analysis.Diagnostic, b analysis.Diagnostic) int {
if r := a.Range.StartPos.Offset - b.Range.StartPos.Offset; r != 0 {
return r
}

if r := strings.Compare(a.Category, b.Category); r != 0 {
return r
}

return strings.Compare(a.Message, b.Message)
})
}

func renderDiagnostic(diagnostic analysis.Diagnostic) string {
categoryColor := aurora.RedFg
if getDiagnosticSeverity(diagnostic) == warningSeverity {
categoryColor = aurora.YellowFg
}

startPos := diagnostic.Range.StartPos
locationText := fmt.Sprintf("%s:%d:%d:", diagnostic.Location.String(), startPos.Line, startPos.Column)
categoryText := fmt.Sprintf("%s:", diagnostic.Category)

return fmt.Sprintf("%s %s %s",
aurora.Gray(12, locationText).String(),
aurora.Colorize(categoryText, categoryColor).String(),
diagnostic.Message,
)
}

func (r *lintResult) countProblems() (int, int) {
numErrors := 0
numWarnings := 0
for _, result := range r.Results {
for _, diagnostic := range result.Diagnostics {
if isErrorDiagnostic(diagnostic) {
numErrors++
} else {
numWarnings++
}
}
}
return numErrors, numWarnings
}

func (r *lintResult) String() string {
var sb strings.Builder

for _, result := range r.Results {
for _, diagnostic := range result.Diagnostics {
sb.WriteString(fmt.Sprintf("%s\n\n", renderDiagnostic(diagnostic)))
}
}

var color aurora.Color
numErrors, numWarnings := r.countProblems()
if numErrors > 0 {
color = aurora.RedFg
} else if numWarnings > 0 {
color = aurora.YellowFg
}

total := numErrors + numWarnings
if total > 0 {
sb.WriteString(aurora.Colorize(fmt.Sprintf("%d %s (%d %s, %d %s)", total, pluralize("problem", total), numErrors, pluralize("error", numErrors), numWarnings, pluralize("warning", numWarnings)), color).String())
} else {
sb.WriteString(aurora.Green("Lint passed").String())
}

return sb.String()
}

func (r *lintResult) JSON() interface{} {
return r
}

func (r *lintResult) Oneliner() string {
numErrors, numWarnings := r.countProblems()
total := numErrors + numWarnings

if total > 0 {
return fmt.Sprintf("%d %s (%d %s, %d %s)", total, pluralize("problem", total), numErrors, pluralize("error", numErrors), numWarnings, pluralize("warning", numWarnings))
}
return "Lint passed"
}

func (r *lintResult) ExitCode() int {
return r.exitCode
}

func pluralize(word string, count int) string {
if count == 1 {
return word
}
return word + "s"
}
Loading

0 comments on commit b19f215

Please sign in to comment.