diff --git a/lint/analyzer.go b/lint/analyzer.go index 495cc732..265a9f73 100644 --- a/lint/analyzer.go +++ b/lint/analyzer.go @@ -31,6 +31,7 @@ const ( UpdateCategory = "update recommended" UnnecessaryCastCategory = "unnecessary-cast-hint" DeprecatedCategory = "deprecated" + CadenceV1Category = "cadence-v1" ) var Analyzers = map[string]*analysis.Analyzer{} diff --git a/lint/analyzers_test.go b/lint/analyzers_test.go index f4d81f19..2830454e 100644 --- a/lint/analyzers_test.go +++ b/lint/analyzers_test.go @@ -24,6 +24,7 @@ import ( "github.com/stretchr/testify/require" "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/sema" "github.com/onflow/cadence/tools/analysis" "github.com/onflow/cadence-tools/lint" @@ -32,6 +33,27 @@ import ( var testLocation = common.StringLocation("test") func testAnalyzers(t *testing.T, code string, analyzers ...*analysis.Analyzer) []analysis.Diagnostic { + return testAnalyzersAdvanced(t, code, nil, analyzers...) +} + +func testAnalyzersWithCheckerError(t *testing.T, code string, analyzers ...*analysis.Analyzer) ([]analysis.Diagnostic, *sema.CheckerError) { + var checkerErr *sema.CheckerError + diagnostics := testAnalyzersAdvanced(t, code, func(config *analysis.Config) { + config.HandleCheckerError = func(err analysis.ParsingCheckingError, checker *sema.Checker) error { + require.NotNil(t, checker) + require.Equal(t, err.ImportLocation(), testLocation) + + require.ErrorAs(t, err, &checkerErr) + require.Len(t, checkerErr.Errors, 1) + return nil + } + }, analyzers...) + + require.NotNil(t, checkerErr) + return diagnostics, checkerErr +} + +func testAnalyzersAdvanced(t *testing.T, code string, setCustomConfigOptions func(config *analysis.Config), analyzers ...*analysis.Analyzer) []analysis.Diagnostic { config := analysis.NewSimpleConfig( lint.LoadMode, @@ -42,6 +64,10 @@ func testAnalyzers(t *testing.T, code string, analyzers ...*analysis.Analyzer) [ nil, ) + if setCustomConfigOptions != nil { + setCustomConfigOptions(config) + } + programs, err := analysis.Load(config, testLocation) require.NoError(t, err) diff --git a/lint/cadence_v1_analyzer.go b/lint/cadence_v1_analyzer.go new file mode 100644 index 00000000..bfb9ae70 --- /dev/null +++ b/lint/cadence_v1_analyzer.go @@ -0,0 +1,349 @@ +/* + * Cadence-lint - The Cadence linter + * + * Copyright 2019-2022 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 lint + +import ( + "fmt" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/runtime/sema" + "github.com/onflow/cadence/tools/analysis" +) + +type cadenceV1Analyzer struct { + program *analysis.Program + elaboration *sema.Elaboration + report func(analysis.Diagnostic) + inspector *ast.Inspector +} + +var CadenceV1Analyzer = (func() *analysis.Analyzer { + return &analysis.Analyzer{ + Description: "Detects uses of removed features in Cadence 1.0", + Requires: []*analysis.Analyzer{ + analysis.InspectorAnalyzer, + }, + Run: func(pass *analysis.Pass) interface{} { + analyzer := newCadenceV1Analyzer(pass) + analyzer.AnalyzeAll() + return nil + }, + } +})() + +func init() { + RegisterAnalyzer( + "cadence-v1", + CadenceV1Analyzer, + ) +} + +func newCadenceV1Analyzer(pass *analysis.Pass) *cadenceV1Analyzer { + return &cadenceV1Analyzer{ + program: pass.Program, + elaboration: pass.Program.Checker.Elaboration, + report: pass.Report, + inspector: pass.ResultOf[analysis.InspectorAnalyzer].(*ast.Inspector), + } +} + +func (v *cadenceV1Analyzer) AnalyzeAll() { + // Analyze account members removed in Cadence 1.0 + v.analyzeRemovedAccountMembers() + // Analyze any type identifiers removed in Cadence 1.0 + v.analyzeRemovedTypeIdentifiers() + // Analyze use of removed `destroy` function for resources + v.analyzeResourceDestructors() +} + +func (v *cadenceV1Analyzer) analyzeRemovedAccountMembers() { + v.inspector.Preorder( + []ast.Element{ + (*ast.MemberExpression)(nil), + }, + func(element ast.Element) { + memberExpression, ok := element.(*ast.MemberExpression) + if !ok { + return + } + + memberInfo, _ := v.elaboration.MemberExpressionMemberAccessInfo(memberExpression) + unwrappedType := unwrapReferenceType(memberInfo.AccessedType) + if unwrappedType != sema.AccountType { + return + } + + identifier := memberExpression.Identifier + switch identifier.String() { + // acct.save() + case "save": + v.newDiagnostic( + identifier, + "`save` has been replaced by the new Storage API.", + "C1.0-StorageAPI-Save", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#account-access-got-improved-55", + ).WithSimpleReplacement("storage.save").Report() + + // acct.linkAccount() + case "linkAccount": + v.newDiagnostic( + identifier, + "`linkAccount` has been replaced by the Capability Controller API.", + "C1.0-StorageAPI-LinkAccount", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + ).Report() + + // acct.link() + case "link": + v.newDiagnostic( + identifier, + "`link` has been replaced by the Capability Controller API.", + "C1.0-StorageAPI-Link", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + ).Report() + + // acct.unlink() + case "unlink": + v.newDiagnostic( + identifier, + "`unlink` has been replaced by the Capability Controller API.", + "C1.0-StorageAPI-Unlink", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + ).Report() + + // acct.getCapability<&A>() + case "getCapability": + v.newDiagnostic( + identifier, + "`getCapability` has been replaced by the Capability Controller API.", + "C1.0-CapabilityAPI-GetCapability", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + ).WithSimpleReplacement("capabilities.get").Report() + + // acct.getLinkTarget() + case "getLinkTarget": + v.newDiagnostic( + identifier, + "`getLinkTarget` has been replaced by the Capability Controller API.", + "C1.0-CapabilityAPI-GetLinkTarget", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + ).Report() + + // acct.addPublicKey() + case "addPublicKey": + v.newDiagnostic( + identifier, + "`addPublicKey` has been removed in favour of the new Key Management API. Please use `keys.add` instead.", + "C1.0-KeyAPI-AddPublicKey", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + ).Report() + + // acct.removePublicKey() + case "removePublicKey": + v.newDiagnostic( + identifier, + "`removePublicKey` has been removed in favour of the new Key Management API.\nPlease use `keys.revoke` instead.", + "C1.0-KeyAPI-RemovePublicKey", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#deprecated-key-management-api-got-removed-60", + ).WithSimpleReplacement("keys.revoke").Report() + } + }, + ) +} + +func (v *cadenceV1Analyzer) analyzeRemovedTypeIdentifiers() { + v.inspectTypeAnnotations(func(typeAnnotation *ast.TypeAnnotation) { + switch typeAnnotation.Type.String() { + case "AuthAccount": + v. + newDiagnostic( + typeAnnotation.Type, + "`AuthAccount` has been removed in Cadence 1.0. Please use an authorized `&Account` reference with necessary entitlements instead.", + "C1.0-AuthAccount", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#account-access-got-improved-55", + ). + WithSimpleReplacement("&Account"). + Report() + case "PublicAccount": + v. + newDiagnostic( + typeAnnotation.Type, + "`PublicAccount` has been removed in Cadence 1.0. Please use an `&Account` reference instead.", + "C1.0-PublicAccount", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#account-access-got-improved-55", + ). + WithSimpleReplacement("&Account"). + Report() + } + }) +} + +func (v *cadenceV1Analyzer) analyzeResourceDestructors() { + v.inspector.WithStack( + []ast.Element{ + (*ast.SpecialFunctionDeclaration)(nil), + }, + func(element ast.Element, push bool, stack []ast.Element) bool { + declaration := element.(*ast.SpecialFunctionDeclaration) + if declaration.DeclarationIdentifier().Identifier != "destroy" { + return true + } + + if len(stack) < 2 { + return true + } + + parent, ok := stack[len(stack)-2].(*ast.CompositeDeclaration) + if !ok { + return true + } + + if parent.Kind() != common.CompositeKindResource { + return true + } + + v.reportRemovedResourceDestructor(declaration) + return false + }, + ) +} + +func (v *cadenceV1Analyzer) reportRemovedResourceDestructor( + declaration *ast.SpecialFunctionDeclaration, +) { + shouldSuggestRemoval := func() bool { + functionDeclaration := declaration.FunctionDeclaration + if !functionDeclaration.FunctionBlock.HasStatements() { + return true + } + + for _, statement := range functionDeclaration.FunctionBlock.Block.Statements { + expressionStatement, ok := statement.(*ast.ExpressionStatement) + if !ok { + return false + } + + if _, ok := expressionStatement.Expression.(*ast.DestroyExpression); !ok { + return false + } + } + + return true + } + + diagnostic := v.newDiagnostic( + declaration, + "`destroy` keyword has been removed. Nested resources will now be implicitly destroyed with their parent. A `ResourceDestroyed` event can be configured to be emitted to notify clients of the destruction.", + "C1.0-ResourceDestruction", + "https://forum.flow.com/t/update-on-cadence-1-0/5197#force-destruction-of-resources-101", + ) + + if shouldSuggestRemoval() { + diagnostic.WithSimpleReplacement("") + } + diagnostic.Report() +} + +// Type annotations are not part of traversal, so we need to inspect them separately +func (v *cadenceV1Analyzer) inspectTypeAnnotations(f func(typeAnnotation *ast.TypeAnnotation)) { + // Filter out nil type annotations + var processAnnotation func(annotation *ast.TypeAnnotation) + processAnnotation = func(annotation *ast.TypeAnnotation) { + if annotation == nil { + return + } + + switch t := annotation.Type.(type) { + case *ast.InstantiationType: + // We need to process the type arguments of an instantiation type + for _, typeArgument := range t.TypeArguments { + processAnnotation(typeArgument) + } + } + + f(annotation) + } + + // Helper function to process a parameter list + processParameterList := func(parameterList *ast.ParameterList) { + for _, parameter := range parameterList.Parameters { + processAnnotation(parameter.TypeAnnotation) + } + } + + v.inspector.Preorder( + []ast.Element{ + (*ast.FieldDeclaration)(nil), + (*ast.FunctionExpression)(nil), + (*ast.CastingExpression)(nil), + (*ast.FunctionDeclaration)(nil), + (*ast.TransactionDeclaration)(nil), + (*ast.VariableDeclaration)(nil), + (*ast.SpecialFunctionDeclaration)(nil), + (*ast.InvocationExpression)(nil), + }, + func(element ast.Element) { + switch declaration := element.(type) { + case *ast.FieldDeclaration: + processAnnotation(declaration.TypeAnnotation) + case *ast.FunctionExpression: + processAnnotation(declaration.ReturnTypeAnnotation) + processParameterList(declaration.ParameterList) + case *ast.CastingExpression: + processAnnotation(declaration.TypeAnnotation) + case *ast.FunctionDeclaration: + processAnnotation(declaration.ReturnTypeAnnotation) + processParameterList(declaration.ParameterList) + case *ast.TransactionDeclaration: + processParameterList(declaration.ParameterList) + case *ast.VariableDeclaration: + processAnnotation(declaration.TypeAnnotation) + case *ast.SpecialFunctionDeclaration: + processParameterList(declaration.FunctionDeclaration.ParameterList) + case *ast.InvocationExpression: + for _, argument := range declaration.TypeArguments { + processAnnotation(argument) + } + } + }, + ) +} + +func (v *cadenceV1Analyzer) newDiagnostic( + position ast.HasPosition, + message string, + code string, + docURL string, +) *diagnostic { + return newDiagnostic( + v.program.Location, + v.report, + fmt.Sprintf("[Cadence 1.0] %s", message), + ast.NewRangeFromPositioned(nil, position), + ).WithCode(code).WithURL(docURL).WithCategory(CadenceV1Category) +} + +// Helpers +func unwrapReferenceType(t sema.Type) sema.Type { + if refType, ok := t.(*sema.ReferenceType); ok { + return refType.Type + } + return t +} diff --git a/lint/cadence_v1_analyzer_test.go b/lint/cadence_v1_analyzer_test.go new file mode 100644 index 00000000..d488e12c --- /dev/null +++ b/lint/cadence_v1_analyzer_test.go @@ -0,0 +1,689 @@ +/* + * Cadence-lint - The Cadence linter + * + * Copyright 2019-2022 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 lint_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/errors" + "github.com/onflow/cadence/runtime/parser" + "github.com/onflow/cadence/runtime/sema" + "github.com/onflow/cadence/tools/analysis" + + "github.com/onflow/cadence-tools/lint" +) + +func TestCadenceV1Analyzer(t *testing.T) { + + t.Parallel() + + t.Run("account.save()", func(t *testing.T) { + + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) contract Test { + init() { + self.account.save() + } + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredMemberError *sema.NotDeclaredMemberError + require.ErrorAs(t, err, ¬DeclaredMemberError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `save` has been replaced by the new Storage API.", + Code: "C1.0-StorageAPI-Save", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#account-access-got-improved-55", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Replace with `storage.save`", + TextEdits: []ast.TextEdit{ + { + Replacement: "storage.save", + Insertion: "", + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 66, + Line: 4, + Column: 21, + }, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 66, + Line: 4, + Column: 21, + }, + }, + }}, + diagnostics, + ) + }) + + t.Run("account.linkAccount()", func(t *testing.T) { + + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) contract Test { + init() { + self.account.linkAccount() + } + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredMemberError *sema.NotDeclaredMemberError + require.ErrorAs(t, err, ¬DeclaredMemberError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `linkAccount` has been replaced by the Capability Controller API.", + Code: "C1.0-StorageAPI-LinkAccount", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + SuggestedFixes: []analysis.SuggestedFix{}, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 73, + Line: 4, + Column: 28, + }, + }, + }}, + diagnostics, + ) + }) + + t.Run("account.link()", func(t *testing.T) { + + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) contract Test { + init() { + self.account.link() + } + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredMemberError *sema.NotDeclaredMemberError + require.ErrorAs(t, err, ¬DeclaredMemberError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `link` has been replaced by the Capability Controller API.", + Code: "C1.0-StorageAPI-Link", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + SuggestedFixes: []analysis.SuggestedFix{}, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 66, + Line: 4, + Column: 21, + }, + }, + }, + }, + diagnostics, + ) + }) + + t.Run("account.unlink()", func(t *testing.T) { + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) contract Test { + init() { + self.account.unlink() + } + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredMemberError *sema.NotDeclaredMemberError + require.ErrorAs(t, err, ¬DeclaredMemberError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `unlink` has been replaced by the Capability Controller API.", + Code: "C1.0-StorageAPI-Unlink", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + SuggestedFixes: []analysis.SuggestedFix{}, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 68, + Line: 4, + Column: 23, + }, + }, + }, + }, + diagnostics, + ) + }) + + t.Run("account.getCapability()", func(t *testing.T) { + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) contract Test { + init() { + self.account.getCapability() + } + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredMemberError *sema.NotDeclaredMemberError + require.ErrorAs(t, err, ¬DeclaredMemberError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `getCapability` has been replaced by the Capability Controller API.", + Code: "C1.0-CapabilityAPI-GetCapability", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Replace with `capabilities.get`", + TextEdits: []ast.TextEdit{ + { + Replacement: "capabilities.get", + Insertion: "", + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 75, + Line: 4, + Column: 30, + }, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 75, + Line: 4, + Column: 30, + }, + }, + }, + }, + diagnostics, + ) + }) + + t.Run("account.getLinkTarget()", func(t *testing.T) { + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) contract Test { + init() { + self.account.getLinkTarget() + } + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredMemberError *sema.NotDeclaredMemberError + require.ErrorAs(t, err, ¬DeclaredMemberError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `getLinkTarget` has been replaced by the Capability Controller API.", + Code: "C1.0-CapabilityAPI-GetLinkTarget", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + SuggestedFixes: []analysis.SuggestedFix{}, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 75, + Line: 4, + Column: 30, + }, + }, + }, + }, + diagnostics, + ) + }) + + t.Run("account.addPublicKey()", func(t *testing.T) { + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) contract Test { + init() { + self.account.addPublicKey() + } + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredMemberError *sema.NotDeclaredMemberError + require.ErrorAs(t, err, ¬DeclaredMemberError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `addPublicKey` has been removed in favour of the new Key Management API. Please use `keys.add` instead.", + Code: "C1.0-KeyAPI-AddPublicKey", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#capability-controller-api-replaced-existing-linking-based-capability-api-82", + SuggestedFixes: []analysis.SuggestedFix{}, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 74, + Line: 4, + Column: 29, + }, + }, + }, + }, diagnostics, + ) + }) + + t.Run("account.removePublicKey()", func(t *testing.T) { + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) contract Test { + init() { + self.account.removePublicKey() + } + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredMemberError *sema.NotDeclaredMemberError + require.ErrorAs(t, err, ¬DeclaredMemberError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `removePublicKey` has been removed in favour of the new Key Management API.\nPlease use `keys.revoke` instead.", + Code: "C1.0-KeyAPI-RemovePublicKey", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#deprecated-key-management-api-got-removed-60", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Replace with `keys.revoke`", + TextEdits: []ast.TextEdit{ + { + Replacement: "keys.revoke", + Insertion: "", + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 77, + Line: 4, + Column: 32, + }, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 63, + Line: 4, + Column: 18, + }, + EndPos: ast.Position{ + Offset: 77, + Line: 4, + Column: 32, + }, + }, + }}, + diagnostics, + ) + }) + + t.Run("resource destruction", func(t *testing.T) { + t.Parallel() + + var checkerErr *sema.CheckerError + var parserError errors.ParentError + diagnostics := testAnalyzersAdvanced(t, + ` + access(all) contract Test { + access(all) resource R { + init() {} + destroy() { + // i'm bad :) + } + } + init() {} + } + `, + func(config *analysis.Config) { + config.HandleCheckerError = func(err analysis.ParsingCheckingError, checker *sema.Checker) error { + require.Equal(t, err.ImportLocation(), testLocation) + require.NotNil(t, checker) + require.ErrorAs(t, err, &checkerErr) + return nil + } + config.HandleParserError = func(err analysis.ParsingCheckingError, program *ast.Program) error { + require.Equal(t, err.ImportLocation(), testLocation) + require.ErrorAs(t, err, &parserError) + return nil + } + }, + lint.CadenceV1Analyzer, + ) + + // Ensure that the checker error and parser error exist + require.NotNil(t, checkerErr) + require.NotNil(t, parserError) + + // Ensure that the checker error is of the correct type + var unknownSpecialFunctionError *sema.UnknownSpecialFunctionError + require.Len(t, checkerErr.ChildErrors(), 1) + require.ErrorAs(t, checkerErr, &unknownSpecialFunctionError) + + // Ensure that the parser error is of the correct type + var invalidDestructorError *parser.CustomDestructorError + require.Len(t, parserError.ChildErrors(), 1) + require.ErrorAs(t, parserError, &invalidDestructorError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `destroy` keyword has been removed. Nested resources will now be implicitly destroyed with their parent. A `ResourceDestroyed` event can be configured to be emitted to notify clients of the destruction.", + Code: "C1.0-ResourceDestruction", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#force-destruction-of-resources-101", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Remove code", + TextEdits: []ast.TextEdit{ + { + Replacement: "", + Insertion: "", + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 81, + Line: 5, + Column: 5, + }, + EndPos: ast.Position{ + Offset: 118, + Line: 7, + Column: 5, + }, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 81, + Line: 5, + Column: 5, + }, + EndPos: ast.Position{ + Offset: 118, + Line: 7, + Column: 5, + }, + }, + }, + }, + diagnostics, + ) + }) + + t.Run("AuthAccount type", func(t *testing.T) { + t.Parallel() + + var notDeclaredError *sema.NotDeclaredError + diagnostics, checkerErr := testAnalyzersWithCheckerError(t, + ` + transaction () { + prepare(signer: AuthAccount) {} + } + `, + lint.CadenceV1Analyzer, + ) + require.ErrorAs(t, checkerErr, ¬DeclaredError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `AuthAccount` has been removed in Cadence 1.0. Please use an authorized `&Account` reference with necessary entitlements instead.", + Code: "C1.0-AuthAccount", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#account-access-got-improved-55", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Replace with `&Account`", + TextEdits: []ast.TextEdit{ + { + Replacement: "&Account", + Insertion: "", + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 41, + Line: 3, + Column: 20, + }, + EndPos: ast.Position{ + Offset: 51, + Line: 3, + Column: 30, + }, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 41, + Line: 3, + Column: 20, + }, + EndPos: ast.Position{ + Offset: 51, + Line: 3, + Column: 30, + }, + }, + }, + }, + diagnostics, + ) + }) + + t.Run("PublicAccount type", func(t *testing.T) { + t.Parallel() + + diagnostics, err := testAnalyzersWithCheckerError(t, + ` + access(all) fun main(addr: Address) { + let account: PublicAccount = getAccount(addr) + } + `, + lint.CadenceV1Analyzer, + ) + + var notDeclaredError *sema.NotDeclaredError + require.ErrorAs(t, err, ¬DeclaredError) + + require.Equal( + t, + []analysis.Diagnostic{ + { + Location: testLocation, + Category: lint.CadenceV1Category, + Message: "[Cadence 1.0] `PublicAccount` has been removed in Cadence 1.0. Please use an `&Account` reference instead.", + Code: "C1.0-PublicAccount", + URL: "https://forum.flow.com/t/update-on-cadence-1-0/5197#account-access-got-improved-55", + SuggestedFixes: []analysis.SuggestedFix{ + { + Message: "Replace with `&Account`", + TextEdits: []ast.TextEdit{ + { + Replacement: "&Account", + Insertion: "", + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 59, + Line: 3, + Column: 17, + }, + EndPos: ast.Position{ + Offset: 71, + Line: 3, + Column: 29, + }, + }, + }, + }, + }, + }, + Range: ast.Range{ + StartPos: ast.Position{ + Offset: 59, + Line: 3, + Column: 17, + }, + EndPos: ast.Position{ + Offset: 71, + Line: 3, + Column: 29, + }, + }, + }, + }, + diagnostics, + ) + }) +} diff --git a/lint/diagnostic.go b/lint/diagnostic.go new file mode 100644 index 00000000..ce815247 --- /dev/null +++ b/lint/diagnostic.go @@ -0,0 +1,86 @@ +/* + * Cadence-lint - The Cadence linter + * + * Copyright 2019-2022 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 lint + +import ( + "github.com/onflow/cadence/runtime/ast" + "github.com/onflow/cadence/runtime/common" + "github.com/onflow/cadence/tools/analysis" +) + +type diagnostic struct { + diagnostic analysis.Diagnostic + report func(analysis.Diagnostic) +} + +func newDiagnostic( + location common.Location, + report func(analysis.Diagnostic), + message string, + position ast.Range, +) *diagnostic { + return &diagnostic{ + diagnostic: analysis.Diagnostic{ + Location: location, + SuggestedFixes: []analysis.SuggestedFix{}, + Range: position, + Message: message, + }, + report: report, + } +} + +func (d *diagnostic) WithCode(code string) *diagnostic { + d.diagnostic.Code = code + return d +} + +func (d *diagnostic) WithURL(url string) *diagnostic { + d.diagnostic.URL = url + return d +} + +func (d *diagnostic) WithCategory(category string) *diagnostic { + d.diagnostic.Category = category + return d +} + +func (d *diagnostic) WithSimpleReplacement(replacement string) *diagnostic { + message := "Replace with `" + replacement + "`" + if replacement == "" { + message = "Remove code" + } + + suggestedFix := analysis.SuggestedFix{ + Message: message, + TextEdits: []ast.TextEdit{ + { + Range: ast.NewRangeFromPositioned(nil, d.diagnostic.Range), + Replacement: replacement, + }, + }, + } + + d.diagnostic.SuggestedFixes = append(d.diagnostic.SuggestedFixes, suggestedFix) + return d +} + +func (d *diagnostic) Report() { + d.report(d.diagnostic) +}