lint

package module
v0.14.2 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Apr 12, 2024 License: Apache-2.0 Imports: 22 Imported by: 1

README

Cadence-lint

The linter for Cadence. Find programming errors, bugs, stylistic errors and suspicious constructs.

How to Build

go build ./cmd/lint
Analyzing contracts of an account

To analyze all contracts of an account, specify the network and address. This requires you have the Flow CLI installed and configured (run flow init).

For example:

./lint -network mainnet -address 0x1654653399040a61
Analyzing a transaction

To analyze a transaction, specify the network and transaction ID. This requires you have the Flow CLI installed and configured (run flow init).

For example:

./lint -network mainnet -transaction 44fd8475eeded90d74e7594b10cf456b0866c78221e7f230fcfd4ba1155c542f
Only running some analyzers

By default, all available analyzers are run.

To list all available analyzers, run:

./lint -help

For example, to only run the reference-to-optional and the external-mutation analyzers, run:

./lint -network mainnet -address 0x1654653399040a61 \
    -analyze reference-to-optional \
    -analyze external-mutation
Analyzing contracts in a directory

To analyze all contracts in a directory, specify the path.

For example:

./lint -directory contracts

The files must be named with the .cdc extension and by their location ID of the program:

  • Contracts in accounts have the format A.<address>.<name>, e.g. A.e467b9dd11fa00df.FlowStorageFees, where
    • address: Address in hex format, e.g. e467b9dd11fa00df
    • name: The name of the contract, e.g FlowStorageFees
  • Transactions have the format t.<ID>, where
    • id: The ID of the transaction (its hash)
  • Scripts have the format s.<ID>, where
    • id: The ID of the script (its hash)
Analyzing contracts in a CSV file

To analyze all contracts in a CSV file, specify the path to the file.

For example:

./lint -csv contracts.csv

The CSV file must be in the following format:

  • Header: location,code
  • Columns:
    • location: The location ID of the program
      • Contracts in accounts have the format A.<address>.<name>, e.g. A.e467b9dd11fa00df.FlowStorageFees, where
        • address: Address in hex format, e.g. e467b9dd11fa00df
        • name: The name of the contract, e.g FlowStorageFees
      • Transactions have the format t.<ID>, where
        • id: The ID of the transaction (its hash)
      • Scripts have the format s.<ID>, where
        • id: The ID of the script (its hash)
    • code: The code of the contract, e.g. pub contract Test {}

Full example:

location,code
t.0000000000000000,"
import 0x1

transaction {
    prepare(signer: AuthAccount) {
        Test.hello()
    }
}
"
A.0000000000000001.Test,"
pub contract Test {

    pub fun hello() {
      log(""Hello, world!"")
    }
}
"

Documentation

Index

Constants

View Source
const (
	ReplacementCategory     = "replacement-hint"
	RemovalCategory         = "removal-hint"
	UpdateCategory          = "update recommended"
	UnnecessaryCastCategory = "unnecessary-cast-hint"
	DeprecatedCategory      = "deprecated"
)

Variables

View Source
var Analyzers = map[string]*analysis.Analyzer{}
View Source
var AuthAccountParameterAnalyzer = (func() *analysis.Analyzer {

	elementFilter := []ast.Element{
		(*ast.FunctionDeclaration)(nil),
		(*ast.SpecialFunctionDeclaration)(nil),
	}

	return &analysis.Analyzer{
		Description: "Detects functions with AuthAccount type parameters",
		Requires: []*analysis.Analyzer{
			analysis.InspectorAnalyzer,
		},
		Run: func(pass *analysis.Pass) interface{} {
			inspector := pass.ResultOf[analysis.InspectorAnalyzer].(*ast.Inspector)

			location := pass.Program.Location
			report := pass.Report

			inspector.Preorder(
				elementFilter,
				func(element ast.Element) {
					var parameterList *ast.ParameterList
					switch declaration := element.(type) {
					case *ast.FunctionDeclaration:
						parameterList = declaration.ParameterList
					case *ast.SpecialFunctionDeclaration:
						if declaration.DeclarationKind() == common.DeclarationKindInitializer {
							parameterList = declaration.FunctionDeclaration.ParameterList
						}
					}

					if parameterList == nil {
						return
					}

					for _, parameter := range parameterList.Parameters {
						nominalType, ok := parameter.TypeAnnotation.Type.(*ast.NominalType)
						if ok && nominalType.Identifier.Identifier == "AuthAccount" {
							report(
								analysis.Diagnostic{
									Location:         location,
									Range:            ast.NewRangeFromPositioned(nil, element),
									Category:         UpdateCategory,
									Message:          "It is an anti-pattern to pass AuthAccount to functions.",
									SecondaryMessage: "Consider using Capabilities instead.",
								},
							)
						}
					}
				},
			)

			return nil
		},
	}
})()
View Source
var DeprecatedMemberAnalyzer = (func() *analysis.Analyzer {

	elementFilter := []ast.Element{
		(*ast.MemberExpression)(nil),
	}

	return &analysis.Analyzer{
		Description: "Detects uses of deprecated members",
		Requires: []*analysis.Analyzer{
			analysis.InspectorAnalyzer,
		},
		Run: func(pass *analysis.Pass) interface{} {
			inspector := pass.ResultOf[analysis.InspectorAnalyzer].(*ast.Inspector)

			location := pass.Program.Location
			elaboration := pass.Program.Elaboration
			report := pass.Report

			inspector.Preorder(
				elementFilter,
				func(element ast.Element) {
					memberExpression, ok := element.(*ast.MemberExpression)
					if !ok {
						return
					}

					memberInfo, _ := elaboration.MemberExpressionMemberInfo(memberExpression)
					member := memberInfo.Member
					if member == nil {
						return
					}

					docStringMatch := docStringDeprecationWarningPattern.FindStringSubmatch(member.DocString)
					if docStringMatch == nil {
						return
					}

					memberName := memberInfo.Member.Identifier.Identifier

					identifierRange := ast.NewRangeFromPositioned(nil, memberExpression.Identifier)

					var suggestedFixes []analysis.SuggestedFix

					replacement := memberReplacement(memberInfo)
					if replacement != "" {
						suggestedFix := analysis.SuggestedFix{
							Message: "replace",
							TextEdits: []analysis.TextEdit{
								{
									Replacement: replacement,
									Range:       identifierRange,
								},
							},
						}
						suggestedFixes = append(suggestedFixes, suggestedFix)
					}

					report(
						analysis.Diagnostic{
							Location: location,
							Range:    identifierRange,
							Category: DeprecatedCategory,
							Message: fmt.Sprintf(
								"%s '%s' is deprecated",
								member.DeclarationKind.Name(),
								memberName,
							),
							SecondaryMessage: docStringMatch[1],
							SuggestedFixes:   suggestedFixes,
						},
					)
				},
			)

			return nil
		},
	}
})()
View Source
var NumberFunctionArgumentAnalyzer = (func() *analysis.Analyzer {

	elementFilter := []ast.Element{
		(*ast.IntegerExpression)(nil),
		(*ast.FixedPointExpression)(nil),
	}

	return &analysis.Analyzer{
		Description: "Detects redundant uses of number conversion functions.",
		Requires: []*analysis.Analyzer{
			analysis.InspectorAnalyzer,
		},
		Run: func(pass *analysis.Pass) interface{} {
			inspector := pass.ResultOf[analysis.InspectorAnalyzer].(*ast.Inspector)

			location := pass.Program.Location
			elaboration := pass.Program.Elaboration
			report := pass.Report

			inspector.Preorder(
				elementFilter,
				func(element ast.Element) {

					var diagnostic *analysis.Diagnostic

					switch expr := element.(type) {
					case *ast.IntegerExpression:
						argumentData := elaboration.NumberConversionArgumentTypes(expr)
						if argumentData.Type == nil {
							return
						}
						diagnostic = suggestIntegerLiteralConversionReplacement(
							expr,
							location,
							argumentData.Type,
							argumentData.Range,
						)

					case *ast.FixedPointExpression:
						argumentData := elaboration.NumberConversionArgumentTypes(expr)
						if argumentData.Type == nil {
							return
						}
						diagnostic = suggestFixedPointLiteralConversionReplacement(
							expr,
							location,
							argumentData.Type,
							argumentData.Range,
						)

					default:
						return
					}

					if diagnostic != nil {
						report(*diagnostic)
					}
				},
			)

			return nil
		},
	}
})()
View Source
var RedundantCastAnalyzer = (func() *analysis.Analyzer {

	elementFilter := []ast.Element{
		(*ast.CastingExpression)(nil),
	}

	return &analysis.Analyzer{
		Description: "Detects unnecessary cast expressions",
		Requires: []*analysis.Analyzer{
			analysis.InspectorAnalyzer,
		},
		Run: func(pass *analysis.Pass) interface{} {
			inspector := pass.ResultOf[analysis.InspectorAnalyzer].(*ast.Inspector)

			location := pass.Program.Location
			elaboration := pass.Program.Elaboration
			report := pass.Report

			inspector.Preorder(
				elementFilter,
				func(element ast.Element) {

					castingExpression, ok := element.(*ast.CastingExpression)
					if !ok {
						return
					}

					redundantType := elaboration.StaticCastTypes(castingExpression)
					if redundantType.ExprActualType != nil && isRedundantCast(
						castingExpression.Expression,
						redundantType.ExprActualType,
						redundantType.TargetType,
						redundantType.ExpectedType,
					) {
						report(
							analysis.Diagnostic{
								Location: location,
								Range:    ast.NewRangeFromPositioned(nil, castingExpression.TypeAnnotation),
								Category: UnnecessaryCastCategory,
								Message:  fmt.Sprintf("cast to `%s` is redundant", redundantType.TargetType),
							},
						)
						return
					}

					alwaysSucceedingTypes := elaboration.RuntimeCastTypes(castingExpression)
					if alwaysSucceedingTypes.Left != nil &&
						sema.IsSubType(alwaysSucceedingTypes.Left, alwaysSucceedingTypes.Right) {

						switch castingExpression.Operation {
						case ast.OperationFailableCast:
							report(
								analysis.Diagnostic{
									Location: location,
									Range:    ast.NewRangeFromPositioned(nil, castingExpression),
									Category: UnnecessaryCastCategory,
									Message: fmt.Sprintf("failable cast ('%s') from `%s` to `%s` always succeeds",
										ast.OperationFailableCast.Symbol(),
										alwaysSucceedingTypes.Left,
										alwaysSucceedingTypes.Right),
								},
							)

						case ast.OperationForceCast:
							report(
								analysis.Diagnostic{
									Location: location,
									Range:    ast.NewRangeFromPositioned(nil, castingExpression),
									Category: UnnecessaryCastCategory,
									Message: fmt.Sprintf("force cast ('%s') from `%s` to `%s` always succeeds",
										ast.OperationForceCast.Symbol(),
										alwaysSucceedingTypes.Left,
										alwaysSucceedingTypes.Right),
								},
							)

						default:
							panic(errors.NewUnreachableError())
						}
					}
				},
			)

			return nil
		},
	}
})()
View Source
var ReferenceOperatorAnalyzer = (func() *analysis.Analyzer {

	elementFilter := []ast.Element{
		(*ast.ReferenceExpression)(nil),
	}

	invalidOperatorRegexp := regexp.MustCompile(`.*\bas[?!].*`)

	return &analysis.Analyzer{
		Description: "Detects invalid operators in reference expressions. These will get rejected in a future release.",
		Requires: []*analysis.Analyzer{
			analysis.InspectorAnalyzer,
		},
		Run: func(pass *analysis.Pass) interface{} {
			inspector := pass.ResultOf[analysis.InspectorAnalyzer].(*ast.Inspector)

			location := pass.Program.Location
			code := pass.Program.Code
			report := pass.Report

			inspector.Preorder(
				elementFilter,
				func(element ast.Element) {

					referenceExpression, ok := element.(*ast.ReferenceExpression)
					if !ok {
						return
					}

					startOffset := referenceExpression.Expression.EndPosition(nil).Offset + 1
					endOffset := referenceExpression.Type.StartPosition().Offset - 1

					if !invalidOperatorRegexp.Match(code[startOffset:endOffset]) {
						return
					}

					report(
						analysis.Diagnostic{
							Location: location,
							Range:    ast.NewRangeFromPositioned(nil, element),
							Category: UpdateCategory,
							Message:  "incorrect reference operator used",
							SecondaryMessage: fmt.Sprintf(
								"use the '%s' operator",
								ast.OperationCast.Symbol(),
							),
						},
					)
				},
			)

			return nil
		},
	}
})()
View Source
var UnnecessaryForceAnalyzer = (func() *analysis.Analyzer {

	elementFilter := []ast.Element{
		(*ast.ForceExpression)(nil),
	}

	return &analysis.Analyzer{
		Description: "Detects unnecessary uses of the force operator",
		Requires: []*analysis.Analyzer{
			analysis.InspectorAnalyzer,
		},
		Run: func(pass *analysis.Pass) interface{} {
			inspector := pass.ResultOf[analysis.InspectorAnalyzer].(*ast.Inspector)

			location := pass.Program.Location
			elaboration := pass.Program.Elaboration
			report := pass.Report

			inspector.Preorder(
				elementFilter,
				func(element ast.Element) {

					forceExpression, ok := element.(*ast.ForceExpression)
					if !ok {
						return
					}

					valueType := elaboration.ForceExpressionType(forceExpression)
					if valueType == nil {
						return
					}

					_, ok = valueType.(*sema.OptionalType)
					if !ok {
						report(
							analysis.Diagnostic{
								Location: location,
								Range:    ast.NewRangeFromPositioned(nil, element),
								Category: RemovalCategory,
								Message:  "unnecessary force operator",
							},
						)
					}
				},
			)

			return nil
		},
	}
})()

Functions

func MemberIsDeprecated added in v0.9.0

func MemberIsDeprecated(docString string) bool

func RegisterAnalyzer added in v0.5.0

func RegisterAnalyzer(name string, analyzer *analysis.Analyzer)

func ReplacementHint

func ReplacementHint(
	expr ast.Expression,
	location common.Location,
	r ast.Range,
) *analysis.Diagnostic

Types

type CheckCastVisitor

type CheckCastVisitor struct {
	// contains filtered or unexported fields
}

func (*CheckCastVisitor) IsRedundantCast

func (d *CheckCastVisitor) IsRedundantCast(expr ast.Expression, exprInferredType, targetType sema.Type) bool

func (*CheckCastVisitor) VisitArrayExpression

func (d *CheckCastVisitor) VisitArrayExpression(expr *ast.ArrayExpression) bool

func (*CheckCastVisitor) VisitAttachExpression added in v0.5.0

func (d *CheckCastVisitor) VisitAttachExpression(_ *ast.AttachExpression) bool

func (*CheckCastVisitor) VisitBinaryExpression

func (d *CheckCastVisitor) VisitBinaryExpression(_ *ast.BinaryExpression) bool

func (*CheckCastVisitor) VisitBoolExpression

func (d *CheckCastVisitor) VisitBoolExpression(_ *ast.BoolExpression) bool

func (*CheckCastVisitor) VisitCastingExpression

func (d *CheckCastVisitor) VisitCastingExpression(_ *ast.CastingExpression) bool

func (*CheckCastVisitor) VisitConditionalExpression

func (d *CheckCastVisitor) VisitConditionalExpression(conditionalExpr *ast.ConditionalExpression) bool

func (*CheckCastVisitor) VisitCreateExpression

func (d *CheckCastVisitor) VisitCreateExpression(_ *ast.CreateExpression) bool

func (*CheckCastVisitor) VisitDestroyExpression

func (d *CheckCastVisitor) VisitDestroyExpression(_ *ast.DestroyExpression) bool

func (*CheckCastVisitor) VisitDictionaryExpression

func (d *CheckCastVisitor) VisitDictionaryExpression(expr *ast.DictionaryExpression) bool

func (*CheckCastVisitor) VisitFixedPointExpression

func (d *CheckCastVisitor) VisitFixedPointExpression(expr *ast.FixedPointExpression) bool

func (*CheckCastVisitor) VisitForceExpression

func (d *CheckCastVisitor) VisitForceExpression(_ *ast.ForceExpression) bool

func (*CheckCastVisitor) VisitFunctionExpression

func (d *CheckCastVisitor) VisitFunctionExpression(_ *ast.FunctionExpression) bool

func (*CheckCastVisitor) VisitIdentifierExpression

func (d *CheckCastVisitor) VisitIdentifierExpression(_ *ast.IdentifierExpression) bool

func (*CheckCastVisitor) VisitIndexExpression

func (d *CheckCastVisitor) VisitIndexExpression(_ *ast.IndexExpression) bool

func (*CheckCastVisitor) VisitIntegerExpression

func (d *CheckCastVisitor) VisitIntegerExpression(_ *ast.IntegerExpression) bool

func (*CheckCastVisitor) VisitInvocationExpression

func (d *CheckCastVisitor) VisitInvocationExpression(_ *ast.InvocationExpression) bool

func (*CheckCastVisitor) VisitMemberExpression

func (d *CheckCastVisitor) VisitMemberExpression(_ *ast.MemberExpression) bool

func (*CheckCastVisitor) VisitNilExpression

func (d *CheckCastVisitor) VisitNilExpression(_ *ast.NilExpression) bool

func (*CheckCastVisitor) VisitPathExpression

func (d *CheckCastVisitor) VisitPathExpression(_ *ast.PathExpression) bool

func (*CheckCastVisitor) VisitReferenceExpression

func (d *CheckCastVisitor) VisitReferenceExpression(_ *ast.ReferenceExpression) bool

func (*CheckCastVisitor) VisitStringExpression

func (d *CheckCastVisitor) VisitStringExpression(_ *ast.StringExpression) bool

func (*CheckCastVisitor) VisitUnaryExpression

func (d *CheckCastVisitor) VisitUnaryExpression(_ *ast.UnaryExpression) bool

func (*CheckCastVisitor) VisitVoidExpression

func (d *CheckCastVisitor) VisitVoidExpression(_ *ast.VoidExpression) bool

type Config

type Config struct {
	Analyzers  []*analysis.Analyzer
	Silent     bool
	UseColor   bool
	PrintError func(*Linter, error, common.Location)
}

type Linter

type Linter struct {
	Config Config

	Codes map[common.Location][]byte
	// contains filtered or unexported fields
}

func NewLinter

func NewLinter(config Config) *Linter

func (*Linter) AnalyzeAccount

func (l *Linter) AnalyzeAccount(address string, networkName string)

func (*Linter) AnalyzeCSV

func (l *Linter) AnalyzeCSV(path string)

func (*Linter) AnalyzeDirectory

func (l *Linter) AnalyzeDirectory(directory string)

func (*Linter) AnalyzeTransaction

func (l *Linter) AnalyzeTransaction(transactionID flow.Identifier, networkName string)

func (*Linter) PrettyPrintError

func (l *Linter) PrettyPrintError(err error, location common.Location)

Directories

Path Synopsis
cmd

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL