diff --git a/remix-astwalker/package.json b/remix-astwalker/package.json index edf2023220..0be47b4927 100644 --- a/remix-astwalker/package.json +++ b/remix-astwalker/package.json @@ -32,6 +32,7 @@ ] }, "dependencies": { + "remix-lib": "^0.4.6", "@types/tape": "^4.2.33", "nyc": "^13.3.0", "tape": "^4.10.1", diff --git a/remix-astwalker/src/@types/remix-lib/index.d.ts b/remix-astwalker/src/@types/remix-lib/index.d.ts new file mode 100644 index 0000000000..3cba5328ea --- /dev/null +++ b/remix-astwalker/src/@types/remix-lib/index.d.ts @@ -0,0 +1,7 @@ +// Type definitiosn for the things we need from remix-lib + +declare module "remix-lib" { + export module util { + export function findLowerBound(target: number, array: Array): number; + } +} diff --git a/remix-astwalker/src/sourceMappings.ts b/remix-astwalker/src/sourceMappings.ts index 058b2a66ed..7da08121d3 100644 --- a/remix-astwalker/src/sourceMappings.ts +++ b/remix-astwalker/src/sourceMappings.ts @@ -1,30 +1,60 @@ import { isAstNode, AstWalker } from './astWalker'; -import { AstNode, Location } from "./types"; +import { AstNode, LineColPosition, LineColRange, Location } from "./types"; +import { util } from "remix-lib"; export declare interface SourceMappings { new(): SourceMappings; } /** - * Break out fields of an AST's "src" attribute string (s:l:f) - * into its "start", "length", and "file index" components. + * Turn an character offset into a "LineColPosition". * - * @param {AstNode} astNode - the object to convert. + * @param offset The character offset to convert. + */ +export function lineColPositionFromOffset(offset: number, lineBreaks: Array): LineColPosition { + let line: number = util.findLowerBound(offset, lineBreaks); + if (lineBreaks[line] !== offset) { + line += 1; + } + const beginColumn = line === 0 ? 0 : (lineBreaks[line - 1] + 1); + return { + line: line + 1, + character: (offset - beginColumn) + 1 + } +} + +/** + * Turn a solc AST's "src" attribute string (s:l:f) + * into a Location + * + * @param astNode The object to convert. */ export function sourceLocationFromAstNode(astNode: AstNode): Location | null { if (isAstNode(astNode) && astNode.src) { - var split = astNode.src.split(':') - return { - start: parseInt(split[0], 10), - length: parseInt(split[1], 10), - file: parseInt(split[2], 10) - } + return sourceLocationFromSrc(astNode.src) } return null; } /** - * Routines for retrieving AST object(s) using some criteria, usually + * Break out fields of solc AST's "src" attribute string (s:l:f) + * into its "start", "length", and "file index" components + * and return that as a Location + * + * @param src A solc "src" field. + * @returns {Location} + */ +export function sourceLocationFromSrc(src: string): Location { + const split = src.split(':') + return { + start: parseInt(split[0], 10), + length: parseInt(split[1], 10), + file: parseInt(split[2], 10) + } +} + +/** + * Routines for retrieving solc AST object(s) using some criteria, usually * includng "src' information. */ export class SourceMappings { @@ -45,11 +75,10 @@ export class SourceMappings { }; /** - * get a list of nodes that are at the given @arg position + * Get a list of nodes that are at the given "position". * - * @param {String} astNodeType - type of node to return or null - * @param {Int} position - character offset - * @return {Object} ast object given by the compiler + * @param astNodeType Type of node to return or null. + * @param position Character offset where AST node should be located. */ nodesAtPosition(astNodeType: string | null, position: Location, ast: AstNode): Array { const astWalker = new AstWalker() @@ -70,6 +99,12 @@ export class SourceMappings { return found; } + /** + * Retrieve the first "astNodeType" that includes the source map at arg instIndex, or "null" if none found. + * + * @param astNodeType nodeType that a found ASTNode must be. Use "null" if any ASTNode can match. + * @param sourceLocation "src" location that the AST node must match. + */ findNodeAtSourceLocation(astNodeType: string | undefined, sourceLocation: Location, ast: AstNode | null): AstNode | null { const astWalker = new AstWalker() let found = null; @@ -90,4 +125,25 @@ export class SourceMappings { astWalker.walkFull(ast, callback); return found; } + + /** + * Retrieve the line/column range position for the given source-mapping string. + * + * @param src Solc "src" object containing attributes {source} and {length}. + */ + srcToLineColumnRange(src: string): LineColRange { + const sourceLocation = sourceLocationFromSrc(src); + if (sourceLocation.start >= 0 && sourceLocation.length >= 0) { + return { + start: lineColPositionFromOffset(sourceLocation.start, this.lineBreaks), + end: lineColPositionFromOffset(sourceLocation.start + sourceLocation.length, this.lineBreaks) + } + } else { + return { + start: null, + end: null + } + } + } + } diff --git a/remix-astwalker/src/types.ts b/remix-astwalker/src/types.ts index 440ba4d9bb..2e9f77e23e 100644 --- a/remix-astwalker/src/types.ts +++ b/remix-astwalker/src/types.ts @@ -1,9 +1,27 @@ +// FIXME: should this be renamed to indicate its offset/length orientation? +// Add "reaadonly property"? export interface Location { start: number; length: number; file: number; // Would it be clearer to call this a file index? } +// This is intended to be compatibile with VScode's Position. +// However it is pretty common with other things too. +// Note: File index is missing here +export interface LineColPosition { + readonly line: number; + readonly character: number; +} + +// This is intended to be compatibile with vscode's Range +// However it is pretty common with other things too. +// Note: File index is missing here +export interface LineColRange { + readonly start: LineColPosition; + readonly end: LineColPosition; +} + export interface Node { ast?: AstNode; legacyAST?: AstNodeLegacy; diff --git a/remix-astwalker/tests/resources/test.sol b/remix-astwalker/tests/resources/test.sol new file mode 100644 index 0000000000..2df646a834 --- /dev/null +++ b/remix-astwalker/tests/resources/test.sol @@ -0,0 +1,17 @@ +contract test { + int x; + + int y; + + function set(int _x) returns (int _r) + { + x = _x; + y = 10; + _r = x; + } + + function get() returns (uint x, uint y) + { + + } +} diff --git a/remix-astwalker/tests/sourceMappings.ts b/remix-astwalker/tests/sourceMappings.ts index 808d2cfd0b..1a2bcbdd31 100644 --- a/remix-astwalker/tests/sourceMappings.ts +++ b/remix-astwalker/tests/sourceMappings.ts @@ -1,12 +1,72 @@ import tape from "tape"; -import { AstNode, isAstNode, SourceMappings, sourceLocationFromAstNode } from "../src"; +import { + AstNode, isAstNode, + LineColPosition, lineColPositionFromOffset, + LineColRange, Location, + SourceMappings, sourceLocationFromAstNode, + sourceLocationFromSrc +} from "../src"; import node from "./resources/newAST"; tape("SourceMappings", (t: tape.Test) => { const source = node.source; const srcMappings = new SourceMappings(source); + t.test("SourceMappings conversions", (st: tape.Test) => { + st.plan(9); + const loc = { + start: 32, + length: 6, + file: 0 + }; + + const ast = node.ast; + + st.deepEqual(lineColPositionFromOffset(0, srcMappings.lineBreaks), + { line: 1, character: 1 }, + "lineColPositionFromOffset degenerate case"); + st.deepEqual(lineColPositionFromOffset(200, srcMappings.lineBreaks), + { line: 17, character: 1 }, + "lineColPositionFromOffset conversion"); + + /* Typescript will keep us from calling sourceLocationFromAstNode + with the wrong type. However, for non-typescript uses, we add + this test which casts to an AST to check that there is a + run-time check in walkFull. + */ + st.notOk(sourceLocationFromAstNode(null), + "sourceLocationFromAstNode rejects an invalid astNode"); + + st.deepEqual(sourceLocationFromAstNode(ast.nodes[0]), + { start: 0, length: 31, file: 0 }, + "sourceLocationFromAstNode extracts a location"); + st.deepEqual(sourceLocationFromSrc("32:6:0"), loc, + "sourceLocationFromSrc conversion"); + const startLC = { line: 6, character: 6 }; + st.deepEqual(srcMappings.srcToLineColumnRange("45:96:0"), + { + start: startLC, + end: { line: 11, character: 6 } + }, "srcToLineColumnRange end of line"); + st.deepEqual(srcMappings.srcToLineColumnRange("45:97:0"), + { + start: startLC, + end: { line: 12, character: 1 } + }, "srcToLineColumnRange beginning of next line"); + st.deepEqual(srcMappings.srcToLineColumnRange("45:98:0"), + { + start: startLC, + end: { line: 13, character: 1 } + }, "srcToLineColumnRange skip over empty line"); + st.deepEqual(srcMappings.srcToLineColumnRange("-1:0:0"), + { + start: null, + end: null + }, "srcToLineColumnRange invalid range"); + st.end(); + }); + t.test("SourceMappings constructor", (st: tape.Test) => { - st.plan(2) + st.plan(2); st.equal(srcMappings.source, source, "sourceMappings object has source-code string"); st.deepEqual(srcMappings.lineBreaks, [15, 26, 27, 38, 39, 81, 87, 103, 119, 135, 141, 142, 186, 192, 193, 199], @@ -14,19 +74,9 @@ tape("SourceMappings", (t: tape.Test) => { st.end(); }); t.test("SourceMappings functions", (st: tape.Test) => { - // st.plan(2) + st.plan(5); const ast = node.ast; - st.deepEqual(sourceLocationFromAstNode(ast.nodes[0]), - { start: 0, length: 31, file: 0 }, - "sourceLocationFromAstNode extracts a location"); - /* Typescript will keep us from calling sourceLocationFromAstNode - with the wrong type. However, for non-typescript uses, we add - this test which casts to an AST to check that there is a - run-time check in walkFull. - */ - st.notOk(sourceLocationFromAstNode(null), - "sourceLocationFromAstNode rejects an invalid astNode"); const loc = { start: 267, length: 20, file: 0 }; let astNode = srcMappings.findNodeAtSourceLocation('ExpressionStatement', loc, ast); st.ok(isAstNode(astNode), "findsNodeAtSourceLocation finds something"); @@ -36,7 +86,6 @@ tape("SourceMappings", (t: tape.Test) => { let astNodes = srcMappings.nodesAtPosition(null, loc, ast); st.equal(astNodes.length, 2, "nodesAtPosition should find more than one astNode"); st.ok(isAstNode(astNodes[0]), "nodesAtPosition returns only AST nodes"); - // console.log(astNodes[0]); astNodes = srcMappings.nodesAtPosition("ExpressionStatement", loc, ast); st.equal(astNodes.length, 1, "nodesAtPosition filtered to a single nodeType"); st.end(); diff --git a/remix-astwalker/tsconfig.json b/remix-astwalker/tsconfig.json index 934d6d2dff..3070bc384a 100644 --- a/remix-astwalker/tsconfig.json +++ b/remix-astwalker/tsconfig.json @@ -1,5 +1,6 @@ { "include": ["src"], + "exclude": ["node_modules", "src/@types" ], "compilerOptions": { /* Basic Options */ "target": "es5", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */ @@ -8,6 +9,7 @@ "declaration": true, /* Generates corresponding '.d.ts' file. */ "sourceMap": true, /* Generates corresponding '.map' file. */ "outDir": "./dist", /* Redirect output structure to the directory. */ + /* Strict Type-Checking Options */ "strict": true, /* Enable all strict type-checking options. */ "noImplicitAny": false, /* Raise error on expressions and declarations with an implied 'any' type. */ @@ -15,7 +17,7 @@ /* Module Resolution Options */ "baseUrl": "./src", /* Base directory to resolve non-absolute module names. */ "paths": { "remix-tests": ["./"] }, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ - "typeRoots": ["node_modules/@types"], /* List of folders to include type definitions from. */ + "typeRoots": ["./@types", "node_modules/@types"], /* List of folders to include type definitions from. */ "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ "types": [ "node"