[WIP] Add routines for finding AST nodes

Functions from remix-lib:
- sourceLocationFromAstNode
- nodesAtPosition
- getLinebreakPositions
- findNodeAtSourceLocation

Add walkFull that traverses more of the AST nodes. This is non-legacy
only.
pull/7/head
rocky 6 years ago
parent 7cbc24e0d3
commit 40168bd26d
  1. 2
      .gitignore
  2. 46
      remix-astwalker/src/astWalker.ts
  3. 1
      remix-astwalker/src/index.ts
  4. 105
      remix-astwalker/src/sourceMappings.ts
  5. 19
      remix-astwalker/src/types.ts
  6. 26
      remix-astwalker/tests/sourceMappings.ts

2
.gitignore vendored

@ -14,3 +14,5 @@ package-lock.json
TODO
soljson.js
lerna-debug.log
*~
/tmp

@ -98,6 +98,52 @@ export class AstWalker extends EventEmitter {
}
}
walkFullInternal(ast: AstNode, callback?: Function | Object) {
function isObject(obj: any): boolean {
return obj != null && obj.constructor.name === "Object"
}
function isAstNode(node: Object): boolean {
return (
isObject(node) &&
'id' in node &&
'nodeType' in node &&
'src' in node
);
}
if (isAstNode(ast) && this.manageCallback(ast, callback)) {
// console.log(`XXX id ${ast.id}, nodeType: ${ast.nodeType}, src: ${ast.src}`);
for (let k of Object.keys(ast)) {
const astItem = ast[k];
if (Array.isArray(astItem)) {
for (let child of astItem) {
if (child) {
this.walkFullInternal(child, callback);
}
}
} else {
this.walkFullInternal(astItem, callback);
}
}
}
}
// Normalizes parameter callback and calls walkFullInternal
walkFull(ast: AstNode, callback?: Function | Object) {
if (callback) {
if (callback instanceof Function) {
callback = Object({ "*": callback });
}
if (!("*" in callback)) {
callback["*"] = function() {
return true;
};
}
}
return this.walkFullInternal(ast, callback);
}
walkAstList(sourcesList: Node, cb?: Function) {
if (cb) {
if (sourcesList.ast) {

@ -1,2 +1,3 @@
export * from './types'
export * from './astWalker'
export * from './sourceMappings'

@ -0,0 +1,105 @@
import { AstWalker } from './astWalker';
import { AstNode, Location } from "./index";
/**
* Break out fields of an AST's "src" attribute string (s:l:f)
* into its "start", "length", and "file index" components.
*
* @param {AstNode} astNode - the object to convert.
*/
export function sourceLocationFromAstNode(astNode: AstNode): Location | null {
if (astNode.src) {
var split = astNode.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
* includng "src' information.
*/
export class SourceMappings {
readonly source: string;
readonly lineBreaks: Array<number>;
constructor(source: string) {
this.source = source;
this.lineBreaks = this.getLinebreakPositions();
};
/**
* get a list of nodes that are at the given @arg offset
*
* @param {String} astNodeType - type of node to return or null
* @param {Int} position - character offset
* @return {Object} ast object given by the compiler
*/
nodesAtPosition(astNodeType: string | null, position: number, ast: AstNode): Array<AstNode> {
const astWalker = new AstWalker()
const callback = {}
let found: Array<AstNode> = [];
/* FIXME: Looking at AST walker code,
I don't understand a need to return a boolean. */
callback['*'] = function(node: AstNode): boolean {
let nodeLocation = sourceLocationFromAstNode(node);
if (nodeLocation &&
nodeLocation.start <= position &&
nodeLocation.start + nodeLocation.length >= position) {
if (!astNodeType || astNodeType === node.name) {
found.push(node)
}
}
return true;
}
astWalker.walk(ast, callback);
return found;
}
/**
* Retrieve line/column position of each source char
*
* @param {String} source - contract source code
* @return {Array} returns an array containing offset of line breaks
*/
getLinebreakPositions(source: string = this.source): Array<number> {
let ret: Array<number> = [];
for (var pos = source.indexOf('\n'); pos >= 0; pos = source.indexOf('\n', pos + 1)) {
ret.push(pos)
}
return ret;
}
findNodeAtSourceLocation(astNodeType: string, sourceLocation: Location, ast: AstNode | null) {
const astWalker = new AstWalker()
const callback = {};
let found = null;
/* FIXME: Looking at AST walker code,
I don't understand a need to return a boolean. */
callback['*'] = function(node: AstNode) {
let nodeLocation = sourceLocationFromAstNode(node);
if (nodeLocation &&
nodeLocation.start <= sourceLocation.start &&
nodeLocation.start + nodeLocation.length >= sourceLocation.start + sourceLocation.length) {
if (astNodeType === node.nodeType) {
found = node;
}
}
return true;
}
astWalker.walkFull(ast, callback);
return found;
}
}
module.exports = {
SourceMappings,
sourceLocationFromAstNode
};

@ -1,3 +1,9 @@
export interface Location {
start: number;
length: number;
file: number; // Would it be clearer to call this a file index?
}
export interface Node {
ast?: AstNode;
legacyAST?: AstNodeLegacy;
@ -6,12 +12,15 @@ export interface Node {
}
export interface AstNode {
/* The following fields are essential, and indicates an that object
is an AST node. */
id: number; // This is unique across all nodes in an AST tree
nodeType: string;
src: string;
absolutePath?: string;
exportedSymbols?: Object;
id: number;
nodeType: string;
nodes?: Array<AstNode>;
src: string;
literals?: Array<string>;
file?: string;
scope?: number;
@ -22,9 +31,9 @@ export interface AstNode {
export interface AstNodeLegacy {
id: number;
name: string;
name: string; // This corresponds to nodeType in current AST
src: string;
children?: Array<AstNodeLegacy>;
children?: Array<AstNodeLegacy>; // This corresponds to nodes in current AST
attributes?: AstNodeAtt;
}

@ -0,0 +1,26 @@
import tape from "tape";
import { SourceMappings, sourceLocationFromAstNode } from "../src/sourceMappings";
import node from "./resources/newAST";
tape("SourceMappings", (t: tape.Test) => {
const source = node.source;
const srcMappings = new SourceMappings(source);
t.test("SourceMappings constructor", (st: tape.Test) => {
st.plan(2);
st.equal(source, srcMappings.source);
st.deepEqual(srcMappings.lineBreaks,
[15, 26, 27, 38, 39, 81, 87, 103, 119, 135, 141, 142, 186, 192, 193, 199]);
st.end();
});
t.test("SourceMappings fns", (st: tape.Test) => {
st.plan(2);
const ast = node.ast;
st.deepEqual(sourceLocationFromAstNode(ast.nodes[0]),
{ start: 0, length: 31, file: 0 });
const loc = { start: 267, length: 20, file: 0 };
const rr = srcMappings.findNodeAtSourceLocation('ExpressionStatement', loc, ast);
st.ok(rr);
st.end();
});
});
Loading…
Cancel
Save