From 5882a8b97bd97c0889043e82f816333ddad39a5e Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 24 Oct 2024 11:56:14 -0700 Subject: [PATCH 1/5] Add Crypto identifier import back --- languageserver/go.mod | 2 +- languageserver/integration/integration.go | 1 + languageserver/integration/resolvers.go | 11 + languageserver/server/server.go | 18 + languageserver/test/index.test.ts | 844 +++++++++++----------- 5 files changed, 471 insertions(+), 405 deletions(-) diff --git a/languageserver/go.mod b/languageserver/go.mod index a17d1ea2..f30b9e6a 100644 --- a/languageserver/go.mod +++ b/languageserver/go.mod @@ -21,7 +21,7 @@ require ( ) require ( - github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0 // indirect + github.com/onflow/flow-core-contracts/lib/go/contracts v1.4.0 github.com/onflow/flow-core-contracts/lib/go/templates v1.4.0 // indirect ) diff --git a/languageserver/integration/integration.go b/languageserver/integration/integration.go index e1e1c91b..a7829e0b 100644 --- a/languageserver/integration/integration.go +++ b/languageserver/integration/integration.go @@ -53,6 +53,7 @@ func NewFlowIntegration(s *server.Server, enableFlowClient bool) (*FlowIntegrati server.WithStringImportResolver(resolve.stringImport), server.WithInitializationOptionsHandler(integration.initialize), server.WithExtendedStandardLibraryValues(FVMStandardLibraryValues()...), + server.WithIdentifierImportResolver(resolve.identifierImport), } if enableFlowClient { diff --git a/languageserver/integration/resolvers.go b/languageserver/integration/resolvers.go index 5c1a474e..50773fdd 100644 --- a/languageserver/integration/resolvers.go +++ b/languageserver/integration/resolvers.go @@ -25,9 +25,12 @@ import ( "github.com/onflow/cadence/common" "github.com/onflow/cadence/sema" + "github.com/onflow/cadence/stdlib" "github.com/onflow/flow-go-sdk" "github.com/onflow/flowkit/v2" "github.com/onflow/flowkit/v2/config" + + coreContracts "github.com/onflow/flow-core-contracts/lib/go/contracts" ) type resolvers struct { @@ -66,6 +69,14 @@ func (r *resolvers) addressImport(location common.AddressLocation) (string, erro return string(account.Contracts[location.Name]), nil } +// identifierImport resolves the code for an identifier location. +func (r *resolvers) identifierImport(location common.IdentifierLocation) (string, error) { + if location == stdlib.CryptoContractLocation { + return string(coreContracts.Crypto()), nil + } + return "", nil +} + // addressContractNames returns a slice of all the contract names on the address location. func (r *resolvers) addressContractNames(address common.Address) ([]string, error) { if r.client == nil { diff --git a/languageserver/server/server.go b/languageserver/server/server.go index b2c928e2..be281036 100644 --- a/languageserver/server/server.go +++ b/languageserver/server/server.go @@ -175,6 +175,8 @@ type Server struct { resolveAddressContractNames AddressContractNamesResolver // resolveStringImport is the optional function that is used to resolve string imports resolveStringImport StringImportResolver + // resolveIdentifierImport is the optional function that is used to resolve identifier imports + resolveIdentifierImport func(location common.IdentifierLocation) (string, error) // codeLensProviders are the functions that are used to provide code lenses for a checker codeLensProviders []CodeLensProvider // diagnosticProviders are the functions that are used to provide diagnostics for a checker @@ -192,6 +194,7 @@ type Server struct { standardLibrary *standardLibrary // scriptStandardLibrary is the standard library for scripts scriptStandardLibrary *standardLibrary + // customImportResolvers is a map of custom import resolvers } type Option func(*Server) error @@ -242,6 +245,15 @@ func WithStringImportResolver(resolver StringImportResolver) Option { } } +// WithIdentifierImportResolver returns a server option that sets the given function +// as the function that is used to resolve identifier imports +func WithIdentifierImportResolver(resolver func(location common.IdentifierLocation) (string, error)) Option { + return func(s *Server) error { + s.resolveIdentifierImport = resolver + return nil + } +} + // WithCodeLensProvider returns a server option that adds the given function // as a function that is used to generate code lenses func WithCodeLensProvider(provider CodeLensProvider) Option { @@ -2052,6 +2064,12 @@ func (s *Server) resolveImport(location common.Location) (program *ast.Program, } code, err = s.resolveAddressImport(loc) + case common.IdentifierLocation: + if s.resolveIdentifierImport == nil { + return nil, nil + } + code, err = s.resolveIdentifierImport(loc) + default: return nil, nil } diff --git a/languageserver/test/index.test.ts b/languageserver/test/index.test.ts index 3a9bcb93..cf379028 100644 --- a/languageserver/test/index.test.ts +++ b/languageserver/test/index.test.ts @@ -11,44 +11,45 @@ import { StreamMessageReader, StreamMessageWriter, TextDocumentItem, - Trace, Tracer -} from "vscode-languageserver-protocol" + Trace, + Tracer, +} from "vscode-languageserver-protocol"; -import {execSync, spawn} from 'child_process' -import * as path from "path" +import { execSync, spawn } from "child_process"; +import * as path from "path"; import * as fs from "fs"; beforeAll(() => { - execSync("go build ../cmd/languageserver", {cwd: __dirname}) -}) + execSync("go build ../cmd/languageserver", { cwd: __dirname }); +}); class ConsoleTracer implements Tracer { log(dataObject: any): void; log(message: string, data?: string): void; log(dataObject: any, data?: string): void { - console.log("tracer >", dataObject, data) + console.log("tracer >", dataObject, data); } } -async function withConnection(f: (connection: ProtocolConnection) => Promise, enableFlowClient = false, debug = false): Promise { - - let opts = [`--enable-flow-client=${enableFlowClient}`] - const child = spawn( - path.resolve(__dirname, './languageserver'), - opts - ) - - let stderr = "" - child.stderr.setEncoding('utf8') - child.stderr.on('data', (data) => { - stderr += data - }) - child.on('exit', (code) => { +async function withConnection( + f: (connection: ProtocolConnection) => Promise, + enableFlowClient = false, + debug = false +): Promise { + let opts = [`--enable-flow-client=${enableFlowClient}`]; + const child = spawn(path.resolve(__dirname, "./languageserver"), opts); + + let stderr = ""; + child.stderr.setEncoding("utf8"); + child.stderr.on("data", (data) => { + stderr += data; + }); + child.on("exit", (code) => { if (code !== 0) { - console.error(stderr) + console.error(stderr); } - expect(code).toBe(0) - }) + expect(code).toBe(0); + }); const connection = createProtocolConnection( new StreamMessageReader(child.stdout), @@ -56,139 +57,116 @@ async function withConnection(f: (connection: ProtocolConnection) => Promise {}) + connection.onRequest(RegistrationRequest.type, () => {}); } - await connection.sendRequest(InitializeRequest.type, - { - capabilities: {}, - processId: process.pid, - rootUri: '/', - workspaceFolders: null, - initializationOptions: initOpts - } - ) + await connection.sendRequest(InitializeRequest.type, { + capabilities: {}, + processId: process.pid, + rootUri: "/", + workspaceFolders: null, + initializationOptions: initOpts, + }); // debug option when testing if (debug) { - connection.trace(Trace.Verbose, new ConsoleTracer(), true) - connection.onUnhandledNotification((e) => console.log("unhandled >", e)) - connection.onError(e => console.log("err >", e)) + connection.trace(Trace.Verbose, new ConsoleTracer(), true); + connection.onUnhandledNotification((e) => console.log("unhandled >", e)); + connection.onError((e) => console.log("err >", e)); } try { - await f(connection) + await f(connection); } finally { - await connection.sendNotification(ExitNotification.type) + await connection.sendNotification(ExitNotification.type); } } -async function createTestDocument(connection: ProtocolConnection, code: string): Promise { - const uri = "file:///test.cdc" +async function createTestDocument( + connection: ProtocolConnection, + code: string +): Promise { + const uri = "file:///test.cdc"; await connection.sendNotification(DidOpenTextDocumentNotification.type, { - textDocument: TextDocumentItem.create( - uri, - "cadence", - 1, - code, - ) - }) - - return uri + textDocument: TextDocumentItem.create(uri, "cadence", 1, code), + }); + + return uri; } describe("getEntryPointParameters command", () => { - async function testCode(code: string, expectedParameters: object[]) { return withConnection(async (connection) => { - - const uri = await createTestDocument(connection, code) + const uri = await createTestDocument(connection, code); const result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.getEntryPointParameters", - arguments: [uri] - }) + arguments: [uri], + }); - expect(result).toEqual(expectedParameters) - }) + expect(result).toEqual(expectedParameters); + }); } - test("script", async() => - testCode( - `access(all) fun main(a: Int) {}`, - [{name: 'a', type: 'Int'}] - ) - ) + test("script", async () => + testCode(`access(all) fun main(a: Int) {}`, [{ name: "a", type: "Int" }])); - test("transaction", async() => - testCode( - `transaction(a: Int) {}`, - [{name: 'a', type: 'Int'}] - ) - ) -}) + test("transaction", async () => + testCode(`transaction(a: Int) {}`, [{ name: "a", type: "Int" }])); +}); describe("getContractInitializerParameters command", () => { - async function testCode(code: string, expectedParameters: object[]) { return withConnection(async (connection) => { - - const uri = await createTestDocument(connection, code) + const uri = await createTestDocument(connection, code); const result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.getContractInitializerParameters", - arguments: [uri] - }) + arguments: [uri], + }); - expect(result).toEqual(expectedParameters) - }) + expect(result).toEqual(expectedParameters); + }); } - test("no contract", async() => - testCode( - ``, - [] - ) - ) + test("no contract", async () => testCode(``, [])); - test("one contract, no parameters", async() => - testCode( - ` + test("one contract, no parameters", async () => + testCode( + ` access(all) contract C { init() {} } `, - [], - ) - ) + [] + )); - test("one contract, one parameter", async() => - testCode( - ` + test("one contract, one parameter", async () => + testCode( + ` access(all) contract C { init(a: Int) {} } `, - [{name: 'a', type: 'Int'}], - ) - ) + [{ name: "a", type: "Int" }] + )); - test("many contracts", async() => - testCode( - ` + test("many contracts", async () => + testCode( + ` access(all) contract C1 { init(a: Int) {} @@ -199,86 +177,82 @@ describe("getContractInitializerParameters command", () => { init(b: Int) {} } `, - [] - ) - ) -}) + [] + )); +}); describe("parseEntryPointArguments command", () => { - async function testCode(code: string) { return withConnection(async (connection) => { - - const uri = await createTestDocument(connection, code) + const uri = await createTestDocument(connection, code); const result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.parseEntryPointArguments", - arguments: [uri, ['0x42']] - }) + arguments: [uri, ["0x42"]], + }); - expect(result).toEqual([{value: '0x0000000000000042', type: 'Address'}]) - }) + expect(result).toEqual([ + { value: "0x0000000000000042", type: "Address" }, + ]); + }); } - test("script", async() => - testCode("access(all) fun main(a: Address) {}")) + test("script", async () => testCode("access(all) fun main(a: Address) {}")); - test("transaction", async() => - testCode("transaction(a: Address) {}")) -}) + test("transaction", async () => testCode("transaction(a: Address) {}")); +}); describe("diagnostics", () => { - async function testCode(code: string, errors: string[]) { return withConnection(async (connection) => { + const notificationPromise = new Promise( + (resolve) => { + connection.onNotification( + PublishDiagnosticsNotification.type, + resolve + ); + } + ); - const notificationPromise = new Promise((resolve) => { - connection.onNotification(PublishDiagnosticsNotification.type, resolve) - }) - - const uri = await createTestDocument(connection, code) + const uri = await createTestDocument(connection, code); - const notification = await notificationPromise + const notification = await notificationPromise; - expect(notification.uri).toEqual(uri) - expect(notification.diagnostics).toHaveLength(errors.length) - notification.diagnostics.forEach( - (diagnostic, i) => expect(diagnostic.message).toEqual(errors[i]) - ) - }) + expect(notification.uri).toEqual(uri); + expect(notification.diagnostics).toHaveLength(errors.length); + notification.diagnostics.forEach((diagnostic, i) => + expect(diagnostic.message).toEqual(errors[i]) + ); + }); } - test("script", async() => - testCode( - `access(all) fun main() { let x = X }`, - ["cannot find variable in this scope: `X`. not found in this scope"] - ) - ) + test("script", async () => + testCode(`access(all) fun main() { let x = X }`, [ + "cannot find variable in this scope: `X`. not found in this scope", + ])); - test("script auth account", async() => + test("script auth account", async () => testCode( `access(all) fun main() { let account = getAuthAccount<&Account>(0x01) }`, - [], - ) - ) + [] + )); - test("transaction", async() => - testCode( - `transaction() { execute { let x = X } }`, - ["cannot find variable in this scope: `X`. not found in this scope"] - ) - ) + test("transaction", async () => + testCode(`transaction() { execute { let x = X } }`, [ + "cannot find variable in this scope: `X`. not found in this scope", + ])); - test("transaction auth account", async() => + test("transaction auth account", async () => testCode( `transaction() { execute { let account = getAuthAccount<&Account>(0x01) } }`, - ["cannot find variable in this scope: `getAuthAccount`. not found in this scope"], - ) - ) + [ + "cannot find variable in this scope: `getAuthAccount`. not found in this scope", + ] + )); - test("attachments", async() => - testCode( - ` + test("attachments", async () => + testCode( + ` access(all) resource R {} @@ -292,37 +266,34 @@ describe("diagnostics", () => { destroy r } `, - [] - ) - ) + [] + )); - test("capability controllers", async() => - testCode( - ` + test("capability controllers", async () => + testCode( + ` access(all) fun main() { let get = getAccount(0x1).capabilities.get } `, - [] - ) - ) + [] + )); - test("unused result", async() => - testCode( - ` + test("unused result", async () => + testCode( + ` access(all) fun main() { getAccount(0x1) } `, - ["unused result"] - ) - ) - - test("InternalEVM contract exists", async() => - testCode( - ` + ["unused result"] + )); + + test("InternalEVM contract exists", async () => + testCode( + ` access(all) fun main() { // Checks that the InternalEVM contract exists @@ -330,93 +301,105 @@ describe("diagnostics", () => { log(InternalEVM.run) } `, - [] - ) - ) + [] + )); type TestDoc = { - name: string - code: string - } + name: string; + code: string; + }; type DocNotification = { - name: string - notification: Promise - } + name: string; + notification: Promise; + }; - const fooContractCode = fs.readFileSync('./foo.cdc', 'utf8') + const fooContractCode = fs.readFileSync("./foo.cdc", "utf8"); async function testImports(docs: TestDoc[]): Promise { - return new Promise(resolve => { - + return new Promise((resolve) => { withConnection(async (connection) => { - let docsNotifications: DocNotification[] = [] + let docsNotifications: DocNotification[] = []; for (let doc of docs) { - const notification = new Promise((resolve) => { - connection.onNotification(PublishDiagnosticsNotification.type, (notification) => { - if (notification.uri == `file://${doc.name}.cdc`) { - resolve(notification) - } - }) - }) + const notification = new Promise( + (resolve) => { + connection.onNotification( + PublishDiagnosticsNotification.type, + (notification) => { + if (notification.uri == `file://${doc.name}.cdc`) { + resolve(notification); + } + } + ); + } + ); docsNotifications.push({ name: doc.name, - notification: notification - }) - - await connection.sendNotification(DidOpenTextDocumentNotification.type, { - textDocument: TextDocumentItem.create(`file://${doc.name}.cdc`, "cadence", 1, doc.code) - }) + notification: notification, + }); + + await connection.sendNotification( + DidOpenTextDocumentNotification.type, + { + textDocument: TextDocumentItem.create( + `file://${doc.name}.cdc`, + "cadence", + 1, + doc.code + ), + } + ); } - resolve(docsNotifications) - }, true) - - }) + resolve(docsNotifications); + }, true); + }); } - test("script with import", async() => { - const contractName = "foo" - const scriptName = "script" + test("script with import", async () => { + const contractName = "foo"; + const scriptName = "script"; const scriptCode = ` import Foo from "./foo.cdc" access(all) fun main() { log(Foo.bar) } - ` + `; let docNotifications = await testImports([ { name: contractName, code: fooContractCode }, - { name: scriptName, code: scriptCode } - ]) + { name: scriptName, code: scriptCode }, + ]); - let script = await docNotifications.find(n => n.name == scriptName).notification - expect(script.uri).toEqual(`file://${scriptName}.cdc`) - expect(script.diagnostics).toHaveLength(0) - }) + let script = await docNotifications.find((n) => n.name == scriptName) + .notification; + expect(script.uri).toEqual(`file://${scriptName}.cdc`); + expect(script.diagnostics).toHaveLength(0); + }); - test("script with string import", async() => { - const contractName = "Foo" - const scriptName = "script" + test("script with string import", async () => { + const contractName = "Foo"; + const scriptName = "script"; const scriptCode = ` import "Foo" access(all) fun main() { log(Foo.bar) } - ` + `; let docNotifications = await testImports([ { name: contractName, code: fooContractCode }, - { name: scriptName, code: scriptCode } - ]) + { name: scriptName, code: scriptCode }, + ]); - let script = await docNotifications.find(n => n.name == scriptName).notification - expect(script.uri).toEqual(`file://${scriptName}.cdc`) - expect(script.diagnostics).toHaveLength(0) - }) + let script = await docNotifications.find((n) => n.name == scriptName) + .notification; + expect(script.uri).toEqual(`file://${scriptName}.cdc`); + expect(script.diagnostics).toHaveLength(0); + }); test("script with string import non-deployment contract", async () => { const scriptName = "script"; @@ -430,278 +413,331 @@ describe("diagnostics", () => { ]); let script = await docNotifications.find((n) => n.name == scriptName) - .notification - expect(script.uri).toEqual(`file://${scriptName}.cdc`) - expect(script.diagnostics).toHaveLength(0) - }) - - test("script import failure", async() => { - const contractName = "foo" - const scriptName = "script" + .notification; + expect(script.uri).toEqual(`file://${scriptName}.cdc`); + expect(script.diagnostics).toHaveLength(0); + }); + + test("script import failure", async () => { + const contractName = "foo"; + const scriptName = "script"; const scriptCode = ` import Foo from "./foo.cdc" access(all) fun main() { log(Foo.zoo) } - ` + `; let docNotifications = await testImports([ { name: contractName, code: fooContractCode }, - { name: scriptName, code: scriptCode } - ]) + { name: scriptName, code: scriptCode }, + ]); + + let script = await docNotifications.find((n) => n.name == scriptName) + .notification; + expect(script.uri).toEqual(`file://${scriptName}.cdc`); + expect(script.diagnostics).toHaveLength(1); + expect(script.diagnostics[0].message).toEqual( + "value of type `&Foo` has no member `zoo`. unknown member" + ); + }); + + test("Crypto contract import", async () => { + const scriptName = "script"; + const scriptCode = ` + import Crypto + access(all) fun main() { log(Crypto.KeyList()) } + `; - let script = await docNotifications.find(n => n.name == scriptName).notification - expect(script.uri).toEqual(`file://${scriptName}.cdc`) - expect(script.diagnostics).toHaveLength(1) - expect(script.diagnostics[0].message).toEqual("value of type `&Foo` has no member `zoo`. unknown member") - }) + let docNotifications = await testImports([ + { name: scriptName, code: scriptCode }, + ]); -}) + let script = await docNotifications.find((n) => n.name == scriptName) + .notification; + expect(script.uri).toEqual(`file://${scriptName}.cdc`); + expect(script.diagnostics).toHaveLength(0); + }); +}); describe("script execution", () => { - - test("script executes and result is returned", async() => { + test("script executes and result is returned", async () => { await withConnection(async (connection) => { let result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.executeScript", - arguments: [`file://${__dirname}/script.cdc`, "[]"] - }) + arguments: [`file://${__dirname}/script.cdc`, "[]"], + }); - expect(result).toEqual(`Result: "HELLO WORLD"`) - }, true) - }) - -}) + expect(result).toEqual(`Result: "HELLO WORLD"`); + }, true); + }); +}); async function getAccounts(connection: ProtocolConnection) { return connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.getAccounts", - arguments: [] - }) + arguments: [], + }); } async function switchAccount(connection: ProtocolConnection, name: string) { return connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.switchActiveAccount", - arguments: [name] - }) + arguments: [name], + }); } describe("accounts", () => { - - test("get account list", async() => { + test("get account list", async () => { await withConnection(async (connection) => { - let result = await getAccounts(connection) + let result = await getAccounts(connection); - expect(result.map(r => r.Name).sort()).toEqual([ + expect(result.map((r) => r.Name).sort()).toEqual([ "Alice", "Bob", "Charlie", "Dave", "Eve", "emulator-account [flow.json]", - "moose [flow.json]" - ]) - expect(result.map(r => r.Address)).toEqual([ + "moose [flow.json]", + ]); + expect(result.map((r) => r.Address)).toEqual([ "179b6b1cb6755e31", "f3fcd2c1a78f5eee", "e03daebed8ca0615", "045a1763c93006ca", "120e725050340cab", "f8d6e0586b0a20c7", - "f8d6e0586b0a20c7" - ]) - expect(result.map(r => r.Active)).toEqual([true, false, false, false, false, false, false]) - }, true) - }) - - test("switch active account", async() => { - await withConnection(async connection => { - let result = await switchAccount(connection, "Bob") - expect(result).toEqual("Account switched to Bob") + "f8d6e0586b0a20c7", + ]); + expect(result.map((r) => r.Active)).toEqual([ + true, + false, + false, + false, + false, + false, + false, + ]); + }, true); + }); + + test("switch active account", async () => { + await withConnection(async (connection) => { + let result = await switchAccount(connection, "Bob"); + expect(result).toEqual("Account switched to Bob"); - let active = await getAccounts(connection) + let active = await getAccounts(connection); - expect(active.filter(a => a.Active).pop().Name).toEqual("Bob") - }, true) - }) + expect(active.filter((a) => a.Active).pop().Name).toEqual("Bob"); + }, true); + }); - test("create an account", async() => { - await withConnection(async connection => { + test("create an account", async () => { + await withConnection(async (connection) => { let result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.createAccount", - arguments: [] - }) + arguments: [], + }); - expect(result.Active).toBeFalsy() - expect(result.Name).toBeDefined() + expect(result.Active).toBeFalsy(); + expect(result.Name).toBeDefined(); - let accounts = await getAccounts(connection) + let accounts = await getAccounts(connection); - expect(accounts.filter(a => a.Name == result.Name)).toHaveLength(1) - }, true) - }) -}) + expect(accounts.filter((a) => a.Name == result.Name)).toHaveLength(1); + }, true); + }); +}); describe("transactions", () => { - const resultRegex = /^Transaction SEALED with ID [a-f0-9]{64}\. Events: \[\]$/ + const resultRegex = + /^Transaction SEALED with ID [a-f0-9]{64}\. Events: \[\]$/; - test("send a transaction with no signer", async() => { - await withConnection(async connection => { + test("send a transaction with no signer", async () => { + await withConnection(async (connection) => { let result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.sendTransaction", - arguments: [`file://${__dirname}/transaction-none.cdc`, "[]", []] - }) - - expect(resultRegex.test(result)).toBeTruthy() - }, true) - }) + arguments: [`file://${__dirname}/transaction-none.cdc`, "[]", []], + }); + expect(resultRegex.test(result)).toBeTruthy(); + }, true); + }); - test("send a transaction with single signer", async() => { - await withConnection(async connection => { + test("send a transaction with single signer", async () => { + await withConnection(async (connection) => { let result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.sendTransaction", - arguments: [`file://${__dirname}/transaction.cdc`, "[]", ["Alice"]] - }) + arguments: [`file://${__dirname}/transaction.cdc`, "[]", ["Alice"]], + }); - expect(resultRegex.test(result)).toBeTruthy() - }, true) - }) + expect(resultRegex.test(result)).toBeTruthy(); + }, true); + }); - test("send a transaction with an account from configuration", async() => { - await withConnection(async connection => { + test("send a transaction with an account from configuration", async () => { + await withConnection(async (connection) => { let result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.sendTransaction", - arguments: [`file://${__dirname}/transaction.cdc`, "[]", ["moose [flow.json]"]] - }) - - expect(resultRegex.test(result)).toBeTruthy() - }, true) - }) - - test("send a transaction with multiple signers", async() => { - await withConnection(async connection => { + arguments: [ + `file://${__dirname}/transaction.cdc`, + "[]", + ["moose [flow.json]"], + ], + }); + + expect(resultRegex.test(result)).toBeTruthy(); + }, true); + }); + + test("send a transaction with multiple signers", async () => { + await withConnection(async (connection) => { let result = await connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.sendTransaction", - arguments: [`file://${__dirname}/transaction-multiple.cdc`, "[]", ["Alice", "Bob"]] - }) - - expect(resultRegex.test(result)).toBeTruthy() - }, true) - }) - -}) + arguments: [ + `file://${__dirname}/transaction-multiple.cdc`, + "[]", + ["Alice", "Bob"], + ], + }); + + expect(resultRegex.test(result)).toBeTruthy(); + }, true); + }); +}); describe("contracts", () => { - - async function deploy(connection: ProtocolConnection, signer: string, file: string, name: string) { + async function deploy( + connection: ProtocolConnection, + signer: string, + file: string, + name: string + ) { return connection.sendRequest(ExecuteCommandRequest.type, { command: "cadence.server.flow.deployContract", - arguments: [`file://${__dirname}/${file}.cdc`, name, signer] - }) + arguments: [`file://${__dirname}/${file}.cdc`, name, signer], + }); } - test("deploy a contract", async() => { - await withConnection(async connection => { - let result = await deploy(connection, "", "foo", "Foo") - expect(result).toEqual("Contract Foo has been deployed to account Alice") - - result = await deploy(connection, "Bob", "foo", "Foo") - expect(result).toEqual("Contract Foo has been deployed to account Bob") - }, true) - }) - - test("deploy contract with file import", async() => { - await withConnection(async connection => { - let result = await deploy(connection, "moose [flow.json]", "foo", "Foo") - expect(result).toEqual("Contract Foo has been deployed to account moose [flow.json]") - - result = await deploy(connection, "moose [flow.json]", "bar", "Bar") - expect(result).toEqual("Contract Bar has been deployed to account moose [flow.json]") - }, true) - }) - - test("deploy contract with string imports", async() => { - await withConnection(async connection => { - let result = await deploy(connection, "moose [flow.json]", "foo", "Foo") - expect(result).toEqual("Contract Foo has been deployed to account moose [flow.json]") + test("deploy a contract", async () => { + await withConnection(async (connection) => { + let result = await deploy(connection, "", "foo", "Foo"); + expect(result).toEqual("Contract Foo has been deployed to account Alice"); - result = await deploy(connection, "moose [flow.json]", "zoo", "Zoo") - expect(result).toEqual("Contract Zoo has been deployed to account moose [flow.json]") - }, true) - }) + result = await deploy(connection, "Bob", "foo", "Foo"); + expect(result).toEqual("Contract Foo has been deployed to account Bob"); + }, true); + }); -}) + test("deploy contract with file import", async () => { + await withConnection(async (connection) => { + let result = await deploy(connection, "moose [flow.json]", "foo", "Foo"); + expect(result).toEqual( + "Contract Foo has been deployed to account moose [flow.json]" + ); + + result = await deploy(connection, "moose [flow.json]", "bar", "Bar"); + expect(result).toEqual( + "Contract Bar has been deployed to account moose [flow.json]" + ); + }, true); + }); + + test("deploy contract with string imports", async () => { + await withConnection(async (connection) => { + let result = await deploy(connection, "moose [flow.json]", "foo", "Foo"); + expect(result).toEqual( + "Contract Foo has been deployed to account moose [flow.json]" + ); + + result = await deploy(connection, "moose [flow.json]", "zoo", "Zoo"); + expect(result).toEqual( + "Contract Zoo has been deployed to account moose [flow.json]" + ); + }, true); + }); +}); describe("codelenses", () => { - const codelensRequest = "textDocument/codeLens" + const codelensRequest = "textDocument/codeLens"; - test("contract codelensss", async() => { - await withConnection(async connection => { - let code = fs.readFileSync("./foo.cdc") - let path = `file://${__dirname}/foo.cdc` - let document = TextDocumentItem.create(path, "cadence", 1, code.toString()) + test("contract codelensss", async () => { + await withConnection(async (connection) => { + let code = fs.readFileSync("./foo.cdc"); + let path = `file://${__dirname}/foo.cdc`; + let document = TextDocumentItem.create( + path, + "cadence", + 1, + code.toString() + ); await connection.sendNotification(DidOpenTextDocumentNotification.type, { - textDocument: document - }) + textDocument: document, + }); let codelens = await connection.sendRequest(codelensRequest, { textDocument: document, - }) - - expect(codelens).toHaveLength(1) - let c = codelens[0].command - expect(c.command).toEqual("cadence.server.flow.deployContract") - expect(c.title).toEqual("💡 Deploy contract Foo to Alice") - expect(c.arguments).toEqual([path, "Foo", "Alice"]) - }, true) + }); - }) + expect(codelens).toHaveLength(1); + let c = codelens[0].command; + expect(c.command).toEqual("cadence.server.flow.deployContract"); + expect(c.title).toEqual("💡 Deploy contract Foo to Alice"); + expect(c.arguments).toEqual([path, "Foo", "Alice"]); + }, true); + }); - test("transactions codelenses", async() => { - await withConnection(async connection => { - let code = fs.readFileSync("./transaction.cdc") - let path = `file://${__dirname}/transaction.cdc` - let document = TextDocumentItem.create(path, "cadence", 1, code.toString()) + test("transactions codelenses", async () => { + await withConnection(async (connection) => { + let code = fs.readFileSync("./transaction.cdc"); + let path = `file://${__dirname}/transaction.cdc`; + let document = TextDocumentItem.create( + path, + "cadence", + 1, + code.toString() + ); await connection.sendNotification(DidOpenTextDocumentNotification.type, { - textDocument: document - }) + textDocument: document, + }); let codelens = await connection.sendRequest(codelensRequest, { textDocument: document, - }) - - expect(codelens).toHaveLength(1) - let c = codelens[0].command - expect(c.command).toEqual("cadence.server.flow.sendTransaction") - expect(c.title).toEqual("💡 Send signed by Alice") - expect(c.arguments).toEqual([path, "[]", ["Alice"]]) - }, true) + }); - }) + expect(codelens).toHaveLength(1); + let c = codelens[0].command; + expect(c.command).toEqual("cadence.server.flow.sendTransaction"); + expect(c.title).toEqual("💡 Send signed by Alice"); + expect(c.arguments).toEqual([path, "[]", ["Alice"]]); + }, true); + }); - test("script codelenses", async() => { - await withConnection(async connection => { - let code = fs.readFileSync("./script.cdc") - let path = `file://${__dirname}/script.cdc` - let document = TextDocumentItem.create(path, "cadence", 1, code.toString()) + test("script codelenses", async () => { + await withConnection(async (connection) => { + let code = fs.readFileSync("./script.cdc"); + let path = `file://${__dirname}/script.cdc`; + let document = TextDocumentItem.create( + path, + "cadence", + 1, + code.toString() + ); await connection.sendNotification(DidOpenTextDocumentNotification.type, { - textDocument: document - }) + textDocument: document, + }); let codelens = await connection.sendRequest(codelensRequest, { textDocument: document, - }) - - expect(codelens).toHaveLength(1) - let c = codelens[0].command - expect(c.command).toEqual("cadence.server.flow.executeScript") - expect(c.title).toEqual("💡 Execute script") - expect(c.arguments).toEqual([path, '[]']) - }, true) - - }) - -}) \ No newline at end of file + }); + + expect(codelens).toHaveLength(1); + let c = codelens[0].command; + expect(c.command).toEqual("cadence.server.flow.executeScript"); + expect(c.title).toEqual("💡 Execute script"); + expect(c.arguments).toEqual([path, "[]"]); + }, true); + }); +}); From 0fe5eb317d3eef7030d658b76e45dacb21e6a22f Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 24 Oct 2024 12:01:47 -0700 Subject: [PATCH 2/5] remove comment --- languageserver/server/server.go | 1 - 1 file changed, 1 deletion(-) diff --git a/languageserver/server/server.go b/languageserver/server/server.go index be281036..320821f1 100644 --- a/languageserver/server/server.go +++ b/languageserver/server/server.go @@ -194,7 +194,6 @@ type Server struct { standardLibrary *standardLibrary // scriptStandardLibrary is the standard library for scripts scriptStandardLibrary *standardLibrary - // customImportResolvers is a map of custom import resolvers } type Option func(*Server) error From 7e381c6d83d29f7be4318e803b7111df35737ca8 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 24 Oct 2024 12:04:25 -0700 Subject: [PATCH 3/5] Add error --- languageserver/integration/resolvers.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/languageserver/integration/resolvers.go b/languageserver/integration/resolvers.go index 50773fdd..7dce127d 100644 --- a/languageserver/integration/resolvers.go +++ b/languageserver/integration/resolvers.go @@ -20,6 +20,7 @@ package integration import ( "errors" + "fmt" "path/filepath" "strings" @@ -74,7 +75,7 @@ func (r *resolvers) identifierImport(location common.IdentifierLocation) (string if location == stdlib.CryptoContractLocation { return string(coreContracts.Crypto()), nil } - return "", nil + return "", fmt.Errorf("unknown identifier location: %s", location) } // addressContractNames returns a slice of all the contract names on the address location. From 682ed26c4fc6de2336d920b664a1be1227f7ab72 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 24 Oct 2024 12:53:28 -0700 Subject: [PATCH 4/5] Add Crypto to WASM --- languageserver/cmd/languageserver/main_wasm.go | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/languageserver/cmd/languageserver/main_wasm.go b/languageserver/cmd/languageserver/main_wasm.go index 168f4b3d..e22fc405 100644 --- a/languageserver/cmd/languageserver/main_wasm.go +++ b/languageserver/cmd/languageserver/main_wasm.go @@ -29,8 +29,11 @@ import ( "syscall/js" "github.com/onflow/cadence/common" + "github.com/onflow/cadence/stdlib" "github.com/onflow/cadence-tools/languageserver/server" + + coreContracts "github.com/onflow/flow-core-contracts/lib/go/contracts" ) const globalFunctionNamePrefix = "CADENCE_LANGUAGE_SERVER" @@ -185,6 +188,13 @@ func start(id int) { return res.String(), nil } + identifierImportResolver := func(location common.IdentifierLocation) (code string, err error) { + if location == stdlib.CryptoContractLocation { + return string(coreContracts.Crypto()), nil + } + return "", fmt.Errorf("CLS %d: unknown identifier location: %s", id, location) + } + languageServer, err := server.NewServer() if err != nil { panic(err) @@ -193,6 +203,7 @@ func start(id int) { err = languageServer.SetOptions( server.WithAddressImportResolver(addressImportResolver), server.WithStringImportResolver(stringImportResolver), + server.WithIdentifierImportResolver(identifierImportResolver), ) if err != nil { panic(err) From a6504dad506b3d6cbd9ef7eba2269f5fb93de9a1 Mon Sep 17 00:00:00 2001 From: Jordan Ribbink Date: Thu, 24 Oct 2024 12:55:21 -0700 Subject: [PATCH 5/5] Add test --- .../tests/index.test.ts | 336 +++++++++++------- 1 file changed, 205 insertions(+), 131 deletions(-) diff --git a/npm-packages/cadence-language-server/tests/index.test.ts b/npm-packages/cadence-language-server/tests/index.test.ts index 2476b253..125613f9 100644 --- a/npm-packages/cadence-language-server/tests/index.test.ts +++ b/npm-packages/cadence-language-server/tests/index.test.ts @@ -1,5 +1,5 @@ -import {CadenceLanguageServer} from "../src" -import * as fs from "fs" +import { CadenceLanguageServer } from "../src"; +import * as fs from "fs"; import { createProtocolConnection, DidOpenTextDocumentNotification, @@ -15,14 +15,19 @@ import { MessageWriter, PartialMessageInfo, TextDocumentItem, - ExitNotification -} from "vscode-languageserver-protocol" -import {Callbacks} from "../dist" + ExitNotification, +} from "vscode-languageserver-protocol"; +import { Callbacks } from "../dist"; -const binary = fs.readFileSync(require.resolve('../dist/cadence-language-server.wasm')) -async function withConnection(callbacks: Callbacks = {}, callback: (connection: ProtocolConnection) => Promise) { +const binary = fs.readFileSync( + require.resolve("../dist/cadence-language-server.wasm") +); +async function withConnection( + callbacks: Callbacks = {}, + callback: (connection: ProtocolConnection) => Promise +) { // Start the language server - await CadenceLanguageServer.create(binary, callbacks) + await CadenceLanguageServer.create(binary, callbacks); const logger: Logger = { error(message: string) { @@ -74,156 +79,225 @@ async function withConnection(callbacks: Callbacks = {}, callback: (connection: }, }; - const connection = createProtocolConnection(reader, writer, logger) - connection.listen() + const connection = createProtocolConnection(reader, writer, logger); + connection.listen(); - await connection.sendRequest(InitializeRequest.type, - { - capabilities: {}, - processId: process.pid, - rootUri: '/', - workspaceFolders: null, - initializationOptions: {} - } - ) + await connection.sendRequest(InitializeRequest.type, { + capabilities: {}, + processId: process.pid, + rootUri: "/", + workspaceFolders: null, + initializationOptions: {}, + }); try { - await callback(connection) + await callback(connection); } finally { try { - connection.dispose() + connection.dispose(); } catch {} } } -async function createTestDocument(connection: ProtocolConnection, code: string): Promise { - const uri = "file:///test.cdc" +async function createTestDocument( + connection: ProtocolConnection, + code: string +): Promise { + const uri = "file:///test.cdc"; await connection.sendNotification(DidOpenTextDocumentNotification.type, { - textDocument: TextDocumentItem.create( - uri, - "cadence", - 1, - code, - ) - }) - - return uri + textDocument: TextDocumentItem.create(uri, "cadence", 1, code), + }); + + return uri; } describe("import", () => { test("string import", async () => { - await withConnection({ - getStringCode(location: string) { - if (location === "Test") { - return "access(all) contract Test {}" - } - return undefined + await withConnection( + { + getStringCode(location: string) { + if (location === "Test") { + return "access(all) contract Test {}"; + } + return undefined; + }, + }, + async (connection) => { + const uri = await createTestDocument(connection, 'import "Test"'); + + const notificationPromise = new Promise( + (resolve) => { + connection.onNotification( + PublishDiagnosticsNotification.type, + resolve + ); + } + ); + + const notification = await notificationPromise; + + expect(notification.uri).toEqual(uri); + expect(notification.diagnostics).toEqual([]); } - }, async (connection) => { - const uri = await createTestDocument(connection, "import \"Test\"") - - const notificationPromise = new Promise((resolve) => { - connection.onNotification(PublishDiagnosticsNotification.type, resolve) - }) - - const notification = await notificationPromise - - expect(notification.uri).toEqual(uri) - expect(notification.diagnostics).toEqual([]) - }) - }) + ); + }); test("string import not found", async () => { - await withConnection({ - getStringCode(_: string) { - return undefined - } - }, async (connection) => { - const uri = await createTestDocument(connection, "import \"Test\"") - - const notificationPromise = new Promise((resolve) => { - connection.onNotification(PublishDiagnosticsNotification.type, resolve) - }) - - const notification = await notificationPromise - - expect(notification.uri).toEqual(uri) - expect(notification.diagnostics).toEqual([{ - severity: 1, - range: { - start: { - line: 0, - character: 7 + await withConnection( + { + getStringCode(_: string) { + return undefined; + }, + }, + async (connection) => { + const uri = await createTestDocument(connection, 'import "Test"'); + + const notificationPromise = new Promise( + (resolve) => { + connection.onNotification( + PublishDiagnosticsNotification.type, + resolve + ); + } + ); + + const notification = await notificationPromise; + + expect(notification.uri).toEqual(uri); + expect(notification.diagnostics).toEqual([ + { + severity: 1, + range: { + start: { + line: 0, + character: 7, + }, + end: { + line: 0, + character: 8, + }, + }, + message: "checking of imported program `Test` failed", }, - end: { - line: 0, - character: 8 + ]); + } + ); + }); + + test("address import", async () => { + await withConnection( + { + getAddressCode(address: string) { + if (address === "0000000000000001.Test") { + return "access(all) contract Test {}"; } + return undefined; }, - message: "checking of imported program `Test` failed" - }]) - }) - }) - - test("address import", async () => { - await withConnection({ - getAddressCode(address: string) { - if (address === "0000000000000001.Test") { - return "access(all) contract Test {}" - } - return undefined + }, + async (connection) => { + const uri = await createTestDocument( + connection, + "import Test from 0x01" + ); + + const notificationPromise = new Promise( + (resolve) => { + connection.onNotification( + PublishDiagnosticsNotification.type, + resolve + ); + } + ); + + const notification = await notificationPromise; + + expect(notification.uri).toEqual(uri); + expect(notification.diagnostics).toEqual([]); } - }, async (connection) => { - const uri = await createTestDocument(connection, "import Test from 0x01") - - const notificationPromise = new Promise((resolve) => { - connection.onNotification(PublishDiagnosticsNotification.type, resolve) - }) - - const notification = await notificationPromise - - expect(notification.uri).toEqual(uri) - expect(notification.diagnostics).toEqual([]) - }) - }) + ); + }); test("address import not found", async () => { - await withConnection({ - getAddressCode(_: string) { - return undefined - } - }, async (connection) => { - const uri = await createTestDocument(connection, "import Test from 0x01") - - const notificationPromise = new Promise((resolve) => { - connection.onNotification(PublishDiagnosticsNotification.type, resolve) - }) - - const notification = await notificationPromise - - expect(notification.uri).toEqual(uri) - expect(notification.diagnostics).toEqual([{ - severity: 1, - range: { - start: { - line: 0, - character: 17 + await withConnection( + { + getAddressCode(_: string) { + return undefined; + }, + }, + async (connection) => { + const uri = await createTestDocument( + connection, + "import Test from 0x01" + ); + + const notificationPromise = new Promise( + (resolve) => { + connection.onNotification( + PublishDiagnosticsNotification.type, + resolve + ); + } + ); + + const notification = await notificationPromise; + + expect(notification.uri).toEqual(uri); + expect(notification.diagnostics).toEqual([ + { + severity: 1, + range: { + start: { + line: 0, + character: 17, + }, + end: { + line: 0, + character: 18, + }, + }, + message: + "checking of imported program `0000000000000001.Test` failed", }, - end: { - line: 0, - character: 18 + ]); + } + ); + }); + + test("Crypto import", async () => { + await withConnection({}, async (connection) => { + const uri = await createTestDocument( + connection, + ` + import Crypto + access(all) contract Test { + init() { + log(Crypto.KeyList()) } - }, - message: "checking of imported program `0000000000000001.Test` failed" - }]) - }) - }) -}) + } + ` + ); + + const notificationPromise = new Promise( + (resolve) => { + connection.onNotification( + PublishDiagnosticsNotification.type, + resolve + ); + } + ); + + const notification = await notificationPromise; + + expect(notification.uri).toEqual(uri); + expect(notification.diagnostics).toEqual([]); + }); + }); +}); afterAll(() => { // Kill the WASM instance when the tests are done withConnection({}, async (connection) => { - await connection.sendNotification(ExitNotification.type) - }) -}) \ No newline at end of file + await connection.sendNotification(ExitNotification.type); + }); +});