trufflesuite/truffle

View on GitHub
packages/codec/lib/ast/utils.ts

Summary

Maintainability
A
0 mins
Test Coverage
import debugModule from "debug";
const debug = debugModule("codec:ast:utils");

import type * as Abi from "@truffle/abi-utils";
import * as Common from "@truffle/codec/common";

import type { AstNode, AstNodes, Scopes } from "./types";
import BN from "bn.js";
import cloneDeep from "lodash/cloneDeep";

/** @category Definition Reading */
export function typeIdentifier(definition: AstNode): string {
  return definition.typeDescriptions.typeIdentifier;
}

/** @category Definition Reading */
export function typeString(definition: AstNode): string {
  return definition.typeDescriptions.typeString;
}

/**
 * Returns the type string, but with location (if any) stripped off the end
 * @category Definition Reading
 */
export function typeStringWithoutLocation(definition: AstNode): string {
  if (definition.nodeType === "YulTypedName") {
    //for handling Yul variables
    return "bytes32";
  }
  return typeString(definition).replace(
    / (storage|memory|calldata)( slice)?$/,
    ""
  );
}

/**
 * returns basic type class for a variable definition node
 * e.g.:
 *  `t_uint256` becomes `uint`
 *  `t_struct$_Thing_$20_memory_ptr` becomes `struct`
 * @category Definition Reading
 */
export function typeClass(definition: AstNode): string {
  if (definition.nodeType === "YulTypedName") {
    //for handling Yul variables
    return "bytes";
  }
  return typeIdentifier(definition).match(/t_([^$_0-9]+)/)[1];
}

/**
 * similar to typeClass, but includes any numeric qualifiers
 * e.g.:
 * `t_uint256` becomes `uint256`
 * @category Definition Reading
 */
export function typeClassLongForm(definition: AstNode): string {
  return typeIdentifier(definition).match(/t_([^$_]+)/)[1];
}

/**
 * for user-defined types -- structs, enums, contracts
 * often you can get these from referencedDeclaration, but not
 * always
 * @category Definition Reading
 */
export function typeId(definition: AstNode): number {
  debug("definition %O", definition);
  return parseInt(
    typeIdentifier(definition).match(
      /\$(\d+)(_(storage|memory|calldata)(_ptr(_slice)?)?)?$/
    )[1]
  );
}

/**
 * For function types; returns internal or external
 * (not for use on other types! will cause an error!)
 * should only return "internal" or "external"
 * @category Definition Reading
 */
export function visibility(definition: AstNode): Common.Visibility {
  return <Common.Visibility>(
    (definition.typeName
      ? definition.typeName.visibility
      : definition.visibility)
  );
}

/**
 * e.g. uint48 -> 6
 * @return size in bytes for explicit type size, or `null` if not stated
 * @category Definition Reading
 */
export function specifiedSize(definition: AstNode): number {
  if (definition.nodeType === "YulTypedName") {
    return 32; //for handling Yul variables
  }
  let specified = typeIdentifier(definition).match(/t_[a-z]+([0-9]+)/);

  if (!specified) {
    return null;
  }

  let num = parseInt(specified[1]);

  switch (typeClass(definition)) {
    case "int":
    case "uint":
    case "fixed":
    case "ufixed":
      return num / 8;

    case "bytes":
      return num;

    default:
      debug(
        "Unknown type for size specification: %s",
        typeIdentifier(definition)
      );
  }
}

/**
 * for fixed-point types, obviously
 * @category Definition Reading
 */
export function decimalPlaces(definition: AstNode): number {
  return parseInt(
    typeIdentifier(definition).match(/t_[a-z]+[0-9]+x([0-9]+)/)[1]
  );
}

/** @category Definition Reading */
export function isArray(definition: AstNode): boolean {
  return typeIdentifier(definition).match(/^t_array/) != null;
}

/** @category Definition Reading */
export function isDynamicArray(definition: AstNode): boolean {
  return (
    isArray(definition) &&
    //NOTE: we do this by parsing the type identifier, rather than by just
    //checking the length field, because we might be using this on a faked-up
    //definition
    typeIdentifier(definition).match(
      /\$dyn_(storage|memory|calldata)(_ptr(_slice)?)?$/
    ) != null
  );
}

/**
 * length of a statically sized array -- please only use for arrays
 * already verified to be static!
 * @category Definition Reading
 */
export function staticLength(definition: AstNode): number {
  //NOTE: we do this by parsing the type identifier, rather than by just
  //checking the length field, because we might be using this on a faked-up
  //definition
  return parseInt(staticLengthAsString(definition));
}

/**
 * see staticLength for explanation
 * @category Definition Reading
 */
export function staticLengthAsString(definition: AstNode): string {
  return typeIdentifier(definition).match(
    /\$(\d+)_(storage|memory|calldata)(_ptr(_slice)?)?$/
  )[1];
}

/** @category Definition Reading */
export function isStruct(definition: AstNode): boolean {
  return typeIdentifier(definition).match(/^t_struct/) != null;
}

/** @category Definition Reading */
export function isMapping(definition: AstNode): boolean {
  return typeIdentifier(definition).match(/^t_mapping/) != null;
}

/** @category Definition Reading */
export function isEnum(definition: AstNode): boolean {
  return typeIdentifier(definition).match(/^t_enum/) != null;
}

/** @category Definition Reading */
export function isReference(definition: AstNode): boolean {
  return (
    typeIdentifier(definition).match(
      /_(memory|storage|calldata)(_ptr(_slice)?)?$/
    ) != null
  );
}

/**
 * note: only use this on things already verified to be references
 * @category Definition Reading
 */
export function referenceType(definition: AstNode): Common.Location {
  return typeIdentifier(definition).match(
    /_([^_]+)(_ptr(_slice)?)?$/
  )[1] as Common.Location;
}

/**
 * only for contract types, obviously! will yield nonsense otherwise!
 * @category Definition Reading
 */
export function contractKind(definition: AstNode): Common.ContractKind {
  return typeString(definition).split(" ")[0] as Common.ContractKind;
}

/**
 * stack size, in words, of a given type
 * note: this function assumes that UDVTs only ever take up
 * a single word, which is currently true
 * @category Definition Reading
 */
export function stackSize(definition: AstNode): number {
  if (
    typeClass(definition) === "function" &&
    visibility(definition) === "external"
  ) {
    return 2;
  }
  if (isReference(definition) && referenceType(definition) === "calldata") {
    if (
      typeClass(definition) === "string" ||
      typeClass(definition) === "bytes"
    ) {
      return 2;
    }
    if (isDynamicArray(definition)) {
      return 2;
    }
  }
  return 1;
}

/** @category Definition Reading */
export function isSimpleConstant(definition: AstNode): boolean {
  const types = ["stringliteral", "rational"];
  return types.includes(typeClass(definition));
}

/**
 * definition: a storage reference definition
 * location: the location you want it to refer to instead
 * @category Definition Reading
 */
export function spliceLocation(
  definition: AstNode,
  location: Common.Location
): AstNode {
  debug("definition %O", definition);
  return {
    ...definition,

    typeDescriptions: {
      ...definition.typeDescriptions,

      typeIdentifier: definition.typeDescriptions.typeIdentifier.replace(
        /_(storage|memory|calldata)(?=((_slice)?_ptr)?$)/,
        "_" + location
      )
    }
  };
}

/**
 * adds "_ptr" on to the end of type identifiers that might need it; note that
 * this operates on identifiers, not definitions
 * @category Definition Reading
 */
export function regularizeTypeIdentifier(identifier: string): string {
  return identifier.replace(
    /(_(storage|memory|calldata))((_slice)?_ptr)?$/,
    "$1_ptr" //this used to use lookbehind for clarity, but Firefox...
    //(see: https://github.com/trufflesuite/truffle/issues/3068 )
  );
}

/**
 * extract the actual numerical value from a node of type rational.
 * currently assumes result will be integer (currently returns BN)
 * @category Definition Reading
 */
export function rationalValue(definition: AstNode): BN {
  let identifier = typeIdentifier(definition);
  let absoluteValue: string = identifier.match(/_(\d+)_by_1$/)[1];
  let isNegative: boolean = identifier.match(/_minus_/) != null;
  return isNegative ? new BN(absoluteValue).neg() : new BN(absoluteValue);
}

/** @category Definition Reading */
export function baseDefinition(definition: AstNode): AstNode {
  if (definition.typeName && definition.typeName.baseType) {
    return definition.typeName.baseType;
  }

  if (definition.baseType) {
    return definition.baseType;
  }

  //otherwise, we'll have to spoof it up ourselves
  let baseIdentifier =
    typeIdentifier(definition).match(/^t_array\$_(.*)_\$/)[1];
  //greedy match to extract everything from first to last dollar sign

  // HACK - internal types for memory or storage also seem to be pointers
  baseIdentifier = regularizeTypeIdentifier(baseIdentifier);

  // another HACK - we get away with it because we're only using that one property
  let result: AstNode = cloneDeep(definition);
  result.typeDescriptions.typeIdentifier = baseIdentifier;
  return result;

  //WARNING -- these hacks do *not* correctly handle all cases!
  //they do, however, handle the cases we currently need.
}

/**
 * for use for mappings and arrays only!
 * for arrays, fakes up a uint definition
 * @category Definition Reading
 */
export function keyDefinition(definition: AstNode, scopes?: Scopes): AstNode {
  let result: AstNode;
  switch (typeClass(definition)) {
    case "mapping":
      //first: is there a key type already there? if so just use that
      if (definition.keyType) {
        return definition.keyType;
      }
      if (definition.typeName && definition.typeName.keyType) {
        return definition.typeName.keyType;
      }

      //otherwise: is there a referencedDeclaration? if so try using that
      let baseDeclarationId = definition.referencedDeclaration;
      debug("baseDeclarationId %d", baseDeclarationId);
      //if there's a referencedDeclaration, we'll use that
      if (baseDeclarationId !== undefined) {
        let baseDeclaration = scopes[baseDeclarationId].definition;
        return baseDeclaration.keyType || baseDeclaration.typeName.keyType;
      }

      //otherwise, we'll need to perform some hackery, similarly to in baseDefinition;
      //we'll have to spoof it up ourselves
      let keyIdentifier = typeIdentifier(definition).match(
        /^t_mapping\$_(.*?)_\$_/
      )[1];
      //use *non*-greedy match; note that if the key type could include
      //the sequence "_$_", this could cause a problem, but they can't; the only
      //valid key types that include dollar signs at all are user-defined types,
      //which contain both "$_" and "_$" but never "_$_".

      // HACK - internal types for memory or storage also seem to be pointers
      keyIdentifier = regularizeTypeIdentifier(keyIdentifier);

      let keyString = typeString(definition).match(
        /mapping\((.*?) => .*\)( storage)?$/
      )[1];
      //use *non*-greedy match; note that if the key type could include
      //"=>", this could cause a problem, but mappings are not allowed as key
      //types, so this can't come up

      // another HACK - we get away with it because we're only using that one property
      result = cloneDeep(definition);
      result.typeDescriptions = {
        typeIdentifier: keyIdentifier,
        typeString: keyString
      };
      return result;

    case "array":
      //HACK -- again we should get away with it because for a uint256 we don't
      //really need to inspect the other properties
      result = cloneDeep(definition);
      result.typeDescriptions = {
        typeIdentifier: "t_uint256",
        typeString: "uint256"
      };
      return result;
    default:
      debug("unrecognized index access!");
  }
}

/**
 * for use for mappings only!
 * @category Definition Reading
 */
export function valueDefinition(definition: AstNode, scopes?: Scopes): AstNode {
  let result: AstNode;
  //first: is there a value type already there? if so just use that
  if (definition.valueType) {
    return definition.valueType;
  }
  if (definition.typeName && definition.typeName.valueType) {
    return definition.typeName.valueType;
  }

  //otherwise: is there a referencedDeclaration? if so try using that
  let baseDeclarationId = definition.referencedDeclaration;
  debug("baseDeclarationId %d", baseDeclarationId);
  //if there's a referencedDeclaration, we'll use that
  if (baseDeclarationId !== undefined) {
    let baseDeclaration = scopes[baseDeclarationId].definition;
    return baseDeclaration.valueType || baseDeclaration.typeName.valueType;
  }

  //otherwise, we'll need to perform some hackery, similarly to in keyDefinition;
  //we'll have to spoof it up ourselves
  let valueIdentifier = typeIdentifier(definition).match(
    /^t_mapping\$_.*?_\$_(.*)_\$/
  )[1];
  //use *non*-greedy match on the key; note that if the key type could include
  //the sequence "_$_", this could cause a problem, but they can't; the only
  //valid key types that include dollar signs at all are user-defined types,
  //which contain both "$_" and "_$" but never "_$_".

  // HACK - internal types for memory or storage also seem to be pointers
  valueIdentifier = regularizeTypeIdentifier(valueIdentifier);

  let valueString = typeString(definition).match(
    /mapping\(.*? => (.*)\)( storage)?$/
  )[1];
  //use *non*-greedy match; note that if the key type could include
  //"=>", this could cause a problem, but mappings are not allowed as key
  //types, so this can't come up

  // another HACK - we get away with it because we're only using that one property
  result = cloneDeep(definition);
  result.typeDescriptions = {
    typeIdentifier: valueIdentifier,
    typeString: valueString
  };
  return result;
}

/**
 * returns input parameters, then output parameters
 * NOTE: ONLY FOR VARIABLE DECLARATIONS OF FUNCTION TYPE
 * NOT FOR FUNCTION DEFINITIONS
 * @category Definition Reading
 */
export function parameters(definition: AstNode): [AstNode[], AstNode[]] {
  let typeObject = definition.typeName || definition;
  if (typeObject.parameterTypes && typeObject.returnParameterTypes) {
    return [
      typeObject.parameterTypes.parameters,
      typeObject.returnParameterTypes.parameters
    ];
  } else {
    return undefined;
  }
}

/**
 * compatibility function, since pre-0.5.0 functions don't have node.kind
 * returns undefined if you don't put in a function node
 * @category Definition Reading
 */
export function functionKind(node: AstNode): string | undefined {
  if (node.nodeType !== "FunctionDefinition") {
    return undefined;
  }
  if (node.kind !== undefined) {
    //if we're dealing with 0.5.x, we can just read node.kind
    return node.kind;
  }
  //otherwise, we need this little shim
  if (node.isConstructor) {
    return "constructor";
  }
  return node.name === "" ? "fallback" : "function";
}

//this is kind of a weird one, it exposes some Solidity internals.
//for internal functions it'll return "internal".
//for external functions it'll return "external".
//for library functions it'll return "delegatecall".
//and for builtin functions, it'll return an internal name for
//that particular builtin function.
//(there are more possibilities but I'm not going to list them all here)
export function functionClass(node: AstNode): string | undefined {
  const match = typeIdentifier(node).match(/^t_function_([^_]+)_/);
  return match ? match[1] : undefined;
}

/**
 * similar compatibility function for mutability for pre-0.4.16 versions
 * returns undefined if you don't give it a FunctionDefinition or
 * VariableDeclaration
 * @category Definition Reading
 */
export function mutability(node: AstNode): Common.Mutability | undefined {
  node = node.typeName || node;
  if (
    node.nodeType !== "FunctionDefinition" &&
    node.nodeType !== "FunctionTypeName"
  ) {
    return undefined;
  }
  if (node.stateMutability !== undefined) {
    //if we're dealing with 0.4.16 or later, we can just read node.stateMutability
    return node.stateMutability;
  }
  //otherwise, we need this little shim
  if (node.payable) {
    return "payable";
  }
  if (node.constant) {
    //yes, it means "view" even if you're looking at a variable declaration!
    //old Solidity was weird!
    return "view";
  }
  return "nonpayable";
}

/**
 * takes a contract definition and asks, does it have a payable fallback
 * function?
 * @category Definition Reading
 */
export function isContractPayable(definition: AstNode): boolean {
  return definition.nodes.some(
    node =>
      node.nodeType === "FunctionDefinition" &&
      (functionKind(node) === "fallback" || functionKind(node) === "receive") &&
      mutability(node) === "payable"
  );
}

/**
 * the main function. just does some dispatch.
 * returns undefined on bad input
 */
export function definitionToAbi(
  node: AstNode,
  referenceDeclarations: AstNodes
): Abi.Entry | undefined {
  switch (node.nodeType) {
    case "FunctionDefinition":
      if (node.visibility === "public" || node.visibility === "external") {
        return functionDefinitionToAbi(node, referenceDeclarations);
      } else {
        return undefined;
      }
    case "EventDefinition":
      return eventDefinitionToAbi(node, referenceDeclarations);
    case "ErrorDefinition":
      return errorDefinitionToAbi(node, referenceDeclarations);
    case "VariableDeclaration":
      if (node.visibility === "public") {
        return getterDefinitionToAbi(node, referenceDeclarations);
      } else {
        return undefined;
      }
    default:
      return undefined;
  }
}

//note: not for FunctionTypeNames or VariableDeclarations
function functionDefinitionToAbi(
  node: AstNode,
  referenceDeclarations: AstNodes
):
  | Abi.FunctionEntry
  | Abi.ConstructorEntry
  | Abi.FallbackEntry
  | Abi.ReceiveEntry {
  let kind = functionKind(node);
  let stateMutability = mutability(node);
  let payable = stateMutability === "payable";
  let inputs;
  switch (kind) {
    case "function":
      let name = node.name;
      let outputs = parametersToAbi(
        node.returnParameters.parameters,
        referenceDeclarations
      );
      inputs = parametersToAbi(
        node.parameters.parameters,
        referenceDeclarations
      );
      return {
        type: "function",
        name,
        inputs,
        outputs,
        stateMutability
      };
    case "constructor":
      inputs = parametersToAbi(
        node.parameters.parameters,
        referenceDeclarations
      );
      //note: need to coerce because of mutability restrictions
      return <Abi.ConstructorEntry>{
        type: "constructor",
        inputs,
        stateMutability,
        payable
      };
    case "fallback":
      //note: need to coerce because of mutability restrictions
      return <Abi.FallbackEntry>{
        type: "fallback",
        stateMutability,
        payable
      };
    case "receive":
      //note: need to coerce because of mutability restrictions
      return <Abi.ReceiveEntry>{
        type: "receive",
        stateMutability,
        payable
      };
  }
}

interface EventParameterNode extends AstNode {
  indexed: boolean;
}

function eventDefinitionToAbi(
  node: AstNode,
  referenceDeclarations: AstNodes
): Abi.EventEntry {
  let inputs = parametersToAbi<EventParameterNode>(
    node.parameters.parameters as EventParameterNode[],
    referenceDeclarations
  );
  let name = node.name;
  let anonymous = node.anonymous;
  return {
    type: "event",
    inputs,
    name,
    anonymous
  };
}

function errorDefinitionToAbi(
  node: AstNode,
  referenceDeclarations: AstNodes
): Abi.ErrorEntry {
  let inputs = parametersToAbi(
    node.parameters.parameters,
    referenceDeclarations
  );
  let name = node.name;
  return {
    type: "error",
    inputs,
    name
  };
}

type Parameter<N extends AstNode> = "indexed" extends keyof N
  ? Abi.EventParameter
  : Abi.Parameter;

function parametersToAbi<N extends AstNode>(
  nodes: N[],
  referenceDeclarations: AstNodes
): Parameter<N>[] {
  return nodes.map(node => parameterToAbi(node, referenceDeclarations));
}

//NOTE: This function is only for types that could potentially go in the ABI!
//(otherwise it could, say, loop infinitely)
//currently it will only ever be called on those because it's only called from
//definitionToAbi, which filters out any definitions that are not for
//this that *actually* go in the ABI
//if you want to expand it to handle those (by throwing an exception, say),
//you'll need to give it a way to detect circularities
function parameterToAbi<N extends AstNode>(
  node: N,
  referenceDeclarations: AstNodes
): Parameter<N> {
  let name = node.name; //may be the empty string... or even undefined for a base type
  let components: Abi.Parameter[];
  let internalType: string = typeStringWithoutLocation(node);
  //is this an array? if so use separate logic
  if (typeClass(node) === "array") {
    let baseType = node.typeName ? node.typeName.baseType : node.baseType;
    let baseAbi = parameterToAbi(baseType, referenceDeclarations);
    let arraySuffix = isDynamicArray(node) ? `[]` : `[${staticLength(node)}]`;
    const parameter: Abi.Parameter = {
      name,
      type: baseAbi.type + arraySuffix,
      components: baseAbi.components,
      internalType
    };

    if ("indexed" in node) {
      return {
        ...parameter,
        indexed: node.indexed
      } as Parameter<N>;
    } else {
      return parameter as Parameter<N>;
    }
  }
  let abiTypeString = toAbiType(node, referenceDeclarations);
  //otherwise... is it a struct? if so we need to populate components
  if (typeClass(node) === "struct") {
    let id = typeId(node);
    let referenceDeclaration = referenceDeclarations[id];
    if (referenceDeclaration === undefined) {
      let typeToDisplay = typeString(node);
      throw new Common.UnknownUserDefinedTypeError(
        id.toString(),
        typeToDisplay
      );
    }
    components = parametersToAbi(
      referenceDeclaration.members,
      referenceDeclarations
    );
  }

  const parameter: Abi.Parameter = {
    name, //may be empty string but should only be undefined in recursive calls
    type: abiTypeString,
    components, //undefined if not a struct or (multidim) array of structs
    internalType
  };

  if ("indexed" in node) {
    return {
      ...parameter,
      indexed: node.indexed
    } as Parameter<N>;
  } else {
    return parameter as Parameter<N>;
  }
}

//note: this is only meant for non-array types that can go in the ABI
//it returns how that type is notated in the ABI -- just the string,
//to be clear, not components of tuples
//again, NOT FOR ARRAYS
function toAbiType(node: AstNode, referenceDeclarations: AstNodes): string {
  let basicType = typeClassLongForm(node); //get that whole first segment!
  switch (basicType) {
    case "contract":
      return "address";
    case "struct":
      return "tuple"; //the more detailed checking will be handled elsewhere
    case "enum": {
      const referenceId = typeId(node);
      const referenceDeclaration = referenceDeclarations[referenceId];
      if (referenceDeclaration === undefined) {
        const typeToDisplay = typeString(node);
        throw new Common.UnknownUserDefinedTypeError(
          referenceId.toString(),
          typeToDisplay
        );
      }
      const numOptions = referenceDeclaration.members.length;
      const bits = 8 * Math.ceil(Math.log2(numOptions) / 8);
      return `uint${bits}`;
    }
    case "userDefinedValueType": {
      const referenceId = typeId(node);
      const referenceDeclaration = referenceDeclarations[referenceId];
      if (referenceDeclaration === undefined) {
        const typeToDisplay = typeString(node);
        throw new Common.UnknownUserDefinedTypeError(
          referenceId.toString(),
          typeToDisplay
        );
      }
      const underlyingType = referenceDeclaration.underlyingType;
      return toAbiType(underlyingType, referenceDeclarations);
    }
    default:
      return basicType;
    //note that: int/uint/fixed/ufixed/bytes will have their size and such left on;
    //address will have "payable" left off;
    //external functions will be reduced to "function" (and internal functions shouldn't
    //be passed in!)
    //(mappings shouldn't be passed in either obviously)
    //(nor arrays :P )
  }
}

function getterDefinitionToAbi(
  node: AstNode,
  referenceDeclarations: AstNodes
): Abi.FunctionEntry {
  debug("getter node: %O", node);
  let name = node.name;
  let { inputs, outputs } = getterParameters(node, referenceDeclarations);
  let inputsAbi = parametersToAbi(inputs, referenceDeclarations);
  let outputsAbi = parametersToAbi(outputs, referenceDeclarations);
  return {
    type: "function",
    name,
    inputs: inputsAbi,
    outputs: outputsAbi,
    stateMutability: "view"
  };
}

//how getter parameters work:
//INPUT:
//types other than arrays and mappings take no input.
//array getters take uint256 input. mapping getters take input of their key type.
//if arrays, mappings, stacked, then takes multiple inputs, in order from outside
//to in.
//These parameters are unnamed.
//OUTPUT:
//if base type (beneath mappings & arrays) is not a struct, returns that.
//(This return parameter has no name -- it is *not* named for the variable!)
//if it is a struct, returns multiple outputs, one for each member of the struct,
//*except* arrays and mappings.  (And they have names, the names of the members.)
//important note: inner structs within a struct are just returned, not
//partially destructured like the outermost struct!  Yes, this is confusing.

export function getterParameters(
  node: AstNode,
  referenceDeclarations: AstNodes
): { inputs: AstNode[]; outputs: AstNode[] } {
  let baseNode: AstNode = node.typeName || node;
  let inputs: AstNode[] = [];
  while (typeClass(baseNode) === "array" || typeClass(baseNode) === "mapping") {
    let keyNode = keyDefinition(baseNode); //note: if baseNode is an array, this spoofs up a uint256 definition
    inputs.push({ ...keyNode, name: "" }); //again, getter input params have no name
    switch (typeClass(baseNode)) {
      case "array":
        baseNode = baseNode.baseType;
        break;
      case "mapping":
        baseNode = baseNode.valueType;
        break;
    }
  }
  //at this point, baseNode should hold the base type
  //now we face the question: is it a struct?
  if (typeClass(baseNode) === "struct") {
    let id = typeId(baseNode);
    let referenceDeclaration = referenceDeclarations[id];
    if (referenceDeclaration === undefined) {
      let typeToDisplay = typeString(baseNode);
      throw new Common.UnknownUserDefinedTypeError(
        id.toString(),
        typeToDisplay
      );
    }
    let outputs = referenceDeclaration.members.filter(
      member => typeClass(member) !== "array" && typeClass(member) !== "mapping"
    );
    return { inputs, outputs }; //no need to wash name!
  } else {
    //only one output; it's just the base node, with its name washed
    return { inputs, outputs: [{ ...baseNode, name: "" }] };
  }
}