trufflesuite/truffle

View on GitHub
packages/resolver/lib/sources/vyper.ts

Summary

Maintainability
B
6 hrs
Test Coverage
import debugModule from "debug";
const debug = debugModule("resolver:sources:vyper");
import path from "path";
import type { ContractObject } from "@truffle/contract-schema/spec";
import type { ResolverSource, SourceResolution } from "../source";

export class Vyper implements ResolverSource {
  contractsDirectory: string;
  wrappedSources: ResolverSource[];
  //because this ResolverSource has to do an actual resolution just to
  //do a resolveDependencyPath, I'm giving it a cache to prevent redoing
  //the work of resolution later
  cache: {
    [filePath: string]: SourceResolution;
  };

  constructor(wrappedSources: ResolverSource[], contractsDirectory: string) {
    this.wrappedSources = wrappedSources;
    this.cache = {};
    this.contractsDirectory = contractsDirectory;
  }

  require(): ContractObject | null {
    //out of scope for this resolver source
    return null;
  }

  async resolve(
    importModule: string,
    importedFrom: string,
    options: { compiler?: { name: string } } = {}
  ) {
    const { compiler } = options;
    if (!compiler || compiler.name !== "vyper") {
      //this resolver source for use by Vyper only!
      debug("not Vyper, go away!");
      return { body: undefined, filePath: undefined };
    }

    importedFrom = importedFrom || "";

    debug("importModule: %s", importModule);
    debug("importedFrom: %s", importedFrom);

    //attempt to just resolve as if it's a file path rather than Vyper module
    //(we have to do this rather than just leaving it to the other, unwrapped,
    //resolver sources, because of resolveDependencyPath... yes, that results
    //in checking those sources twice on failure :-/ )
    for (const source of this.wrappedSources) {
      const directlyResolvedSource = await source.resolve(
        importModule,
        importedFrom
      );
      if (directlyResolvedSource.body !== undefined) {
        debug("found directly");
        return directlyResolvedSource;
      }
    }
    //so if we've made it here, it's time for some Vyper module processing...
    debug("running Vyper import processing!");

    //only attempt this if what we have looks like a Vyper module
    if (!importModule.match(/^[\w.]+$/)) {
      debug("clearly not a Vyper module");
      return { body: undefined, filePath: undefined };
    }

    const importPath = moduleToPath(importModule); //note: no file extension yet
    debug("importPath: %s", importPath);
    const explicitlyRelative = importModule[0] === "."; //note we check importModule,
    //not importPath, to make the check simpler (can just check if begins with "."
    //rather than "./" or "../")
    debug("explicitlyRelative: %o", explicitlyRelative);

    const possiblePathsMinusExtension: string[] = [];
    //first: check in local directory
    possiblePathsMinusExtension.push(
      path.join(path.dirname(importedFrom), importPath)
    );
    if (!explicitlyRelative) {
      //next: check in contracts dir, if not explicitly relative
      possiblePathsMinusExtension.push(
        path.join(this.contractsDirectory, importPath)
      );
      //finally: check wherever the resolver says to check
      possiblePathsMinusExtension.push(importPath);
    }
    const possibleExtensions = [".json", ".vy"]; //Vyper only expects these two
    //note: this puts all JSON before all Vyper, which is how we want it
    //(we do not want to try Vyper from any sources until JSON from all sources
    //has been checked)
    const possiblePaths = [].concat(
      ...possibleExtensions.map(extension =>
        possiblePathsMinusExtension.map(path => path + extension)
      )
    );

    debug("possiblePaths: %O", possiblePaths);

    for (const possiblePath of possiblePaths) {
      debug("possiblePath: %s", possiblePath);
      let resolvedSource;
      if (possiblePath in this.cache) {
        resolvedSource = this.cache[possiblePath];
      } else {
        for (const source of this.wrappedSources) {
          debug("source: %o", source);
          resolvedSource = await source.resolve(possiblePath, importedFrom);
          if (resolvedSource.body !== undefined) {
            debug("found via this source");
            break;
          }
        }
        this.cache[possiblePath] = resolvedSource; //yes, even failures are cached!
      }

      if (resolvedSource.body !== undefined) {
        debug("found");
        return resolvedSource;
      }
      debug("not found");
    }

    //if not found, return nothing
    return { body: undefined, filePath: undefined };
  }

  async resolveDependencyPath(importPath: string, dependencyPath: string) {
    //unfortunately, for this sort of source to resolve a dependency path,
    //it's going to need to do a resolve :-/
    debug("importPath: %s", importPath);
    const resolved = await this.resolve(
      dependencyPath,
      importPath,
      { compiler: { name: "vyper" } } //HACK
    );
    if (resolved) {
      return resolved.filePath;
    } else {
      return null;
    }
  }
}

function moduleToPath(moduleName: string): string {
  //first: get initial dot count by matching against regular expression for
  //initial dots, then taking captured group (note: regular expression
  //will always match so don't have to worry about null here) and taking
  //length
  const initialDotCount = moduleName.match(/^(\.*)/)[1].length;
  //then: change rest of dots to slashes
  const withoutInitialDots = moduleName.slice(initialDotCount);
  const pathWithoutDots = withoutInitialDots.replace(/\./g, path.sep);
  let initialDotPath;
  //then: interpret initial dots
  switch (initialDotCount) {
    case 0:
      initialDotPath = "";
      break;
    case 1:
      initialDotPath = "./";
      break;
    default:
      initialDotPath = "../".repeat(initialDotCount - 1);
      break;
  }
  //finally: combine
  return initialDotPath + pathWithoutDots;
}