UNPKG

@tevm/ts-plugin

Version:
833 lines (743 loc) 22.5 kB
import { access, mkdir, writeFile } from 'node:fs/promises' import { tmpdir } from 'node:os' import type { FileAccessObject } from '@tevm/base-bundler' import { bundler } from '@tevm/base-bundler' import { createCache } from '@tevm/bundler-cache' import type { ResolvedCompilerConfig } from '@tevm/config' import type { Node } from 'solidity-ast/node.js' import { findAll } from 'solidity-ast/utils.js' import typescript from 'typescript/lib/tsserverlibrary.js' import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest' import { convertSolcAstToTsDefinitionInfo, findContractDefinitionFileNameFromTevmNode, findNode } from '../utils' import { getDefinitionServiceDecorator } from './getDefinitionAtPosition.js' // For mocking findAll const mockGenerator = function* (): Generator<Node> { yield { name: 'some text' } as unknown as Node } // Mock the bundler, util functions, and findAll vi.mock('@tevm/base-bundler', () => ({ bundler: vi.fn(), })) vi.mock('../utils', () => ({ findNode: vi.fn(), convertSolcAstToTsDefinitionInfo: vi.fn(), findContractDefinitionFileNameFromTevmNode: vi.fn(), })) vi.mock('solidity-ast/utils.js', () => ({ findAll: vi.fn(), })) const fao: FileAccessObject = { existsSync: vi.fn() as any, readFileSync: vi.fn() as any, readFile: vi.fn() as any, writeFileSync: vi.fn() as any, statSync: vi.fn() as any, stat: vi.fn() as any, mkdirSync: vi.fn() as any, mkdir, writeFile, exists: async (path: string) => { try { await access(path) return true } catch (_e) { return false } }, } const mockLogger = { error: vi.fn(), info: vi.fn(), warn: vi.fn(), log: vi.fn(), } const mockLanguageService = { getDefinitionAtPosition: vi.fn(() => { return [ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ] }), getDefinitionAndBoundSpan: vi.fn(() => { return { definitions: [ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ], textSpan: { start: 0, length: 0, }, } }), getProgram: vi.fn(() => ({ getSourceFile: vi.fn(() => typescript.createSourceFile( 'someFile.ts', 'const x = "foo"', typescript.ScriptTarget.ESNext, true, typescript.ScriptKind.TS, ), ), })), } as unknown as typescript.LanguageService // Full bundler mock object type type MockBundler = { name: string config: ResolvedCompilerConfig include?: string[] exclude?: string[] resolveDts: Mock<any> resolveDtsSync: Mock<any> resolveTsModule: Mock<any> resolveTsModuleSync: Mock<any> resolveCjsModule: Mock<any> resolveCjsModuleSync: Mock<any> resolveEsmModule: Mock<any> resolveEsmModuleSync: Mock<any> } describe('getDefinitionServiceDecorator', () => { beforeEach(() => { vi.clearAllMocks() // Mock the bundler function with full bundler interface const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: {}, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) // Mock utils functions vi.mocked(findNode).mockReturnValue({ getText: vi.fn().mockReturnValue('some text'), getStart: vi.fn().mockReturnValue(10), getEnd: vi.fn().mockReturnValue(20), } as any) vi.mocked(convertSolcAstToTsDefinitionInfo).mockReturnValue({ fileName: 'converted.sol', textSpan: { start: 5, length: 10 }, } as any) vi.mocked(findContractDefinitionFileNameFromTevmNode).mockReturnValue('/bar/Contract.sol') }) afterEach(() => { vi.clearAllMocks() }) it('should decorate getDefinitionAtPosition properly', () => { const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) expect(definitions).toEqual([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ]) }) it('should decorate getDefinitionAndBoundSpan properly', () => { const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const result = decoratedService.getDefinitionAndBoundSpan('someFile.ts', 42) expect(result).toEqual({ definitions: [ { fileName: 'someFile.ts', textSpan: { length: 0, start: 0, }, }, ], textSpan: { length: 0, start: 0, }, }) }) it('should return original definitions if getProgram returns null', () => { const mockLSWithNullProgram = { ...mockLanguageService, getProgram: vi.fn(() => null), } as unknown as typescript.LanguageService const decoratedService = getDefinitionServiceDecorator( mockLSWithNullProgram, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) expect(definitions).toEqual([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ]) }) it('should return original definitions if sourceFile is null', () => { const mockLSWithNullSourceFile = { ...mockLanguageService, getProgram: vi.fn(() => ({ getSourceFile: vi.fn(() => null), })), } as unknown as typescript.LanguageService const decoratedService = getDefinitionServiceDecorator( mockLSWithNullSourceFile, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) expect(definitions).toEqual([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ]) }) it('should return original definitions if findNode returns null', () => { vi.mocked(findNode).mockReturnValue(null) const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) expect(definitions).toEqual([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ]) }) it('should return original definitions if ContractPath is null', () => { vi.mocked(findContractDefinitionFileNameFromTevmNode).mockReturnValue(null) const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) expect(definitions).toEqual([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ]) }) it('should handle original service returning null for getDefinitionAtPosition', () => { const mockLSReturningNull = { ...mockLanguageService, getDefinitionAtPosition: vi.fn(() => null), } as unknown as typescript.LanguageService const decoratedService = getDefinitionServiceDecorator( mockLSReturningNull, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) // Test with a Solidity definition vi.mocked(findAll).mockImplementation((type) => { if (type === 'FunctionDefinition') { return mockGenerator() } // Empty generator for other types return (function* () {})() }) const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: { file1: {} }, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) // Should still return converted Solidity definitions expect(definitions).toBeDefined() expect(definitions?.length).toBeGreaterThan(0) expect(mockLSReturningNull.getDefinitionAtPosition).toHaveBeenCalledWith('someFile.ts', 42) }) it('should log an error if resolveDtsSync cannot resolve ASTs', () => { // Setup the bundler mock to return null asts const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: null }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) expect(definitions).toEqual([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ]) expect(mockLogger.error).toHaveBeenCalledWith( '@tevm/ts-plugin: getDefinitionAtPositionDecorator was unable to resolve asts for /bar/Contract.sol', ) }) it('should log an error if unable to find definitions in ASTs', () => { // Setup the bundler mock to return asts but empty findAll results const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: { file1: {} }, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) // Mock findAll to return generator that yields nothing vi.mocked(findAll).mockReturnValue((function* () {})()) const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) expect(definitions).toEqual([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ]) expect(mockLogger.error).toHaveBeenCalledWith('@tevm/ts-plugin: unable to find definitions /bar/Contract.sol') }) it('should handle node.getText() not matching function/event names', () => { // Setup the bundler mock to return multiple asts const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: { file1: {}, }, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) // Mock findAll to return nodes with different names vi.mocked(findAll).mockImplementation((_type) => { return (function* () { yield { name: 'different_name' } as unknown as Node })() }) // Mock node.getText() to return something that won't match vi.mocked(findNode).mockReturnValue({ getText: vi.fn().mockReturnValue('non_matching_name'), getStart: vi.fn().mockReturnValue(10), getEnd: vi.fn().mockReturnValue(20), } as any) const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) // Should log the error and fall back to original definitions expect(mockLogger.error).toHaveBeenCalledWith('@tevm/ts-plugin: unable to find definitions /bar/Contract.sol') expect(definitions).toEqual([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0, }, }, ]) }) it('should handle multiple ASTs and find function definitions', () => { // Setup the bundler mock to return multiple asts const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: { file1: {}, file2: {}, }, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) // Mock findAll to return function definitions vi.mocked(findAll).mockImplementation((type) => { if (type === 'FunctionDefinition') { return mockGenerator() } // Empty generator for other types return (function* () {})() }) // Create a service and manually mock its method const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) // Just test that the function was called the right number of times // and with the right parameters // 1. Original should call service.getDefinitionAtPosition decoratedService.getDefinitionAtPosition('someFile.ts', 42) // 2. convertSolcAstToTsDefinitionInfo should be called twice (once for each AST) expect(vi.mocked(findAll)).toHaveBeenCalledWith('FunctionDefinition', expect.anything()) expect(vi.mocked(findAll)).toHaveBeenCalledTimes(4) // EventDefinition + FunctionDefinition for each AST }) it('should handle multiple ASTs and find event definitions', () => { // Setup the bundler mock to return multiple asts const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: { file1: {}, file2: {}, }, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) // Mock findAll to return event definitions vi.mocked(findAll).mockImplementation((type) => { if (type === 'EventDefinition') { return mockGenerator() } // Empty generator for other types return (function* () {})() }) // Create a decorated service with a mock language service // that returns a new array on each call so we can add to it const mockLS = { ...mockLanguageService, getDefinitionAtPosition: vi.fn().mockReturnValue([ { fileName: 'someFile.ts', textSpan: { start: 0, length: 0 }, }, ]), } as unknown as typescript.LanguageService const decoratedService = getDefinitionServiceDecorator( mockLS, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) // Just test that the function was called the right number of times // and with the right parameters // 1. Original should call service.getDefinitionAtPosition decoratedService.getDefinitionAtPosition('someFile.ts', 42) // 2. Check that findAll was called with EventDefinition expect(vi.mocked(findAll)).toHaveBeenCalledWith('EventDefinition', expect.anything()) }) it('should handle getDefinitionAndBoundSpan with no definitions', () => { // Setup a mock language service that returns null for getDefinitionAtPosition const mockLSWithNullDef = { ...mockLanguageService, getDefinitionAtPosition: vi.fn(() => null), getDefinitionAndBoundSpan: vi.fn(() => ({ definitions: null, textSpan: { start: 0, length: 0 }, })), } as unknown as typescript.LanguageService const decoratedService = getDefinitionServiceDecorator( mockLSWithNullDef, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) // Call and verify const result = decoratedService.getDefinitionAndBoundSpan('someFile.ts', 42) expect(mockLSWithNullDef.getDefinitionAndBoundSpan).toHaveBeenCalledWith('someFile.ts', 42) expect(result).toBeDefined() }) it('should handle getDefinitionAndBoundSpan with no .sol definitions', () => { // Setup bundler to return null asts const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: null, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) // Setup a mock language service that returns a TS file definition const mockLSWithTSDefs = { ...mockLanguageService, getDefinitionAtPosition: vi.fn(() => [ { fileName: 'regular.ts', textSpan: { start: 0, length: 0 }, }, ]), } as unknown as typescript.LanguageService const decoratedService = getDefinitionServiceDecorator( mockLSWithTSDefs, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) // Call and verify const result = decoratedService.getDefinitionAndBoundSpan('someFile.ts', 42) expect(mockLanguageService.getDefinitionAndBoundSpan).toHaveBeenCalledWith('someFile.ts', 42) expect(result).toBeDefined() }) it('should handle getDefinitionAndBoundSpan with .sol definitions but no node', () => { // Setup bundler to return asts const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: { file1: {} }, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) // Mock findAll for FunctionDefinition vi.mocked(findAll).mockImplementation((type) => { if (type === 'FunctionDefinition') { return mockGenerator() } // Empty generator for other types return (function* () {})() }) // Return null for findNode to test that code path vi.mocked(findNode).mockReturnValue(null) const decoratedService = getDefinitionServiceDecorator( mockLanguageService, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) // Call and verify const result = decoratedService.getDefinitionAndBoundSpan('someFile.ts', 42) if (result) { expect(result.textSpan).toEqual({ start: 0, length: 0 }) } }) it('should combine TypeScript and Solidity definitions when both exist', () => { // Setup mock bundler to return ASTs with matching function name const mockBundlerInstance: MockBundler = { name: 'mock-bundler', config: {} as ResolvedCompilerConfig, resolveDts: vi.fn(), resolveTsModule: vi.fn(), resolveTsModuleSync: vi.fn(), resolveCjsModule: vi.fn(), resolveCjsModuleSync: vi.fn(), resolveEsmModule: vi.fn(), resolveEsmModuleSync: vi.fn(), resolveDtsSync: vi.fn().mockReturnValue({ asts: { file1: {} }, solcInput: {}, }), } vi.mocked(bundler).mockReturnValue(mockBundlerInstance) // Mock utils functions to match the node text vi.mocked(findNode).mockReturnValue({ getText: vi.fn().mockReturnValue('targetFunction'), getStart: vi.fn().mockReturnValue(10), getEnd: vi.fn().mockReturnValue(20), } as any) // Mock findAll to return definitions that match the node text vi.mocked(findAll).mockImplementation((type) => { if (type === 'FunctionDefinition') { return (function* () { yield { name: 'targetFunction' } as unknown as Node })() } return (function* () {})() }) // Original TS definitions const tsDefinition = { fileName: 'someFile.ts', textSpan: { start: 0, length: 0 }, } // Mock service that returns TS definitions const mockLSWithTsDefs = { ...mockLanguageService, getDefinitionAtPosition: vi.fn().mockReturnValue([tsDefinition]), } as unknown as typescript.LanguageService // Setup decorated service const decoratedService = getDefinitionServiceDecorator( mockLSWithTsDefs, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) // Call and verify const definitions = decoratedService.getDefinitionAtPosition('someFile.ts', 42) // Should include both the Solidity definitions and TypeScript definitions expect(definitions).toBeDefined() expect(definitions?.length).toBeGreaterThan(1) const fileNames = definitions?.map((d) => d.fileName) expect(fileNames).toContain('converted.sol') expect(fileNames).toContain('someFile.ts') }) it('should forward other LanguageService methods through proxy', () => { // Setup a mock language service with an extra method const mockGetReferences = vi.fn() const mockLSWithExtraMethod = { ...mockLanguageService, getReferencesAtPosition: mockGetReferences, } as unknown as typescript.LanguageService const decoratedService = getDefinitionServiceDecorator( mockLSWithExtraMethod, {} as any, mockLogger as any, typescript, fao, createCache(tmpdir(), fao, tmpdir()), ) // Call the method through the proxy and verify it was called correctly decoratedService.getReferencesAtPosition?.('someFile.ts', 42) expect(mockGetReferences).toHaveBeenCalledWith('someFile.ts', 42) }) })