Add more position conversion functions

- type LineColPosition,
- type LineColRange,
- function lineColPositionFromOffset
- function srcToLineColumnRange

And associated tests for these
pull/5370/head
rocky 6 years ago
parent 3cd9cd2996
commit 724942140f
  1. 1
      remix-astwalker/package.json
  2. 7
      remix-astwalker/src/@types/remix-lib/index.d.ts
  3. 80
      remix-astwalker/src/sourceMappings.ts
  4. 18
      remix-astwalker/src/types.ts
  5. 17
      remix-astwalker/tests/resources/test.sol
  6. 77
      remix-astwalker/tests/sourceMappings.ts
  7. 4
      remix-astwalker/tsconfig.json

@ -32,6 +32,7 @@
]
},
"dependencies": {
"remix-lib": "^0.4.6",
"@types/tape": "^4.2.33",
"nyc": "^13.3.0",
"tape": "^4.10.1",

@ -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>): number;
}
}

@ -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<number>): LineColPosition {
let line: number = util.findLowerBound(offset, lineBreaks);
if (lineBreaks[line] !== offset) {
line += 1;
}
const beginColumn = line === 0 ? 0 : (lineBreaks[line - 1] + 1);
return <LineColPosition>{
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 sourceLocationFromSrc(astNode.src)
}
return null;
}
/**
* 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 <Location>{
start: parseInt(split[0], 10),
length: parseInt(split[1], 10),
file: parseInt(split[2], 10)
}
}
return null;
}
/**
* Routines for retrieving AST object(s) using some criteria, usually
* 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<AstNode> {
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 <LineColRange>{
start: lineColPositionFromOffset(sourceLocation.start, this.lineBreaks),
end: lineColPositionFromOffset(sourceLocation.start + sourceLocation.length, this.lineBreaks)
}
} else {
return <LineColRange>{
start: null,
end: null
}
}
}
}

@ -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;

@ -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)
{
}
}

@ -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 = <Location>{
start: 32,
length: 6,
file: 0
};
const ast = node.ast;
st.deepEqual(lineColPositionFromOffset(0, srcMappings.lineBreaks),
<LineColPosition>{ line: 1, character: 1 },
"lineColPositionFromOffset degenerate case");
st.deepEqual(lineColPositionFromOffset(200, srcMappings.lineBreaks),
<LineColPosition>{ 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(<AstNode>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 = <LineColPosition>{ line: 6, character: 6 };
st.deepEqual(srcMappings.srcToLineColumnRange("45:96:0"),
<LineColRange>{
start: startLC,
end: <LineColPosition>{ line: 11, character: 6 }
}, "srcToLineColumnRange end of line");
st.deepEqual(srcMappings.srcToLineColumnRange("45:97:0"),
<LineColRange>{
start: startLC,
end: <LineColPosition>{ line: 12, character: 1 }
}, "srcToLineColumnRange beginning of next line");
st.deepEqual(srcMappings.srcToLineColumnRange("45:98:0"),
<LineColRange>{
start: startLC,
end: <LineColPosition>{ line: 13, character: 1 }
}, "srcToLineColumnRange skip over empty line");
st.deepEqual(srcMappings.srcToLineColumnRange("-1:0:0"),
<LineColRange>{
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(<AstNode>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();

@ -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"

Loading…
Cancel
Save