o1-labs / zkapp-cli

CLI to create a zkApp (zero-knowledge app) for Mina Protocol
https://docs.minaprotocol.com/zkapps/how-to-write-a-zkapp
Apache License 2.0
115 stars 43 forks source link

Improved `SmartContract` classes inheritance lookup used during the zkApps deployment procedure. #640

Closed shimkiv closed 3 months ago

shimkiv commented 3 months ago

Closes #636


Includes dependencies and tests upgrade/update.


Description (a bit outdated after the last commit)

This PR introduces the mechanism to identify classes extending or implementing the o1js SmartContract class in a given JavaScript file that are scanned using pattern ./build/**/*.js. The core method findIfClassExtendsOrImplementsSmartContract(entryFilePath) performs this operation. Below is a step-by-step breakdown of how this method works, with explanations of the intermediate methods it invokes, using an example:

./build/src/SomeZkApp.js

import { someOtherLib } from 'someOtherLib';
import { SmartContract } from 'o1js';

export class TestZkApp extends SmartContract {}
export class AnotherTestZkApp extends TestZkApp {}
export class SomethingElse extends Parent {}

findIfClassExtendsOrImplementsSmartContract(entryFilePath)

This method identifies all classes in the provided file that extend or implement the SmartContract class (Note: the o1js belonging is yet to be fixed). It is being invoked with the file path to process like this:

const result = findIfClassExtendsOrImplementsSmartContract('./build/src/SomeZkApp.js');

It returns:

[
  { "className": "TestZkApp", "filePath": "./build/scr/SomeZkApp.js" },
  { "className": "AnotherTestZkApp", "filePath": "./build/src/SomeZkApp.js" }
]

This data then used by zkapp-cli deploy method to proceed (if only 1 SC is available) or prompt user to select which SC to deploy.

Steps:

  1. Build class hierarchy: Calls buildClassHierarchy(entryFilePath) to generate a map of class names to class information.
  2. Resolve imports: Calls resolveImports(entryFilePath) to map local import names to their resolved file paths.
  3. Check inheritance: Iterates over the classes found in the class hierarchy and checks if they extend or implement SmartContract by calling checkClassInheritance().

Intermediate methods involved

  1. buildClassHierarchy(filePath):

    • Purpose: To parse the file and create a map of class declarations.
    • Reason: Essential for understanding the structure of classes within the file, including their inheritance and implemented interfaces.

    This function parses the file to build a hierarchy of class declarations.

    const classesMap = buildClassHierarchy('./build/src/SomeZkApp.js');

    Returns:

    {
      "TestZkApp": {
        "extends": "SmartContract",
        "implements": [],
        "filePath": "./build/src/SomeZkApp.js"
      },
      "AnotherTestZkApp": {
        "extends": "TestZkApp",
        "implements": [],
        "filePath": "./build/src/SomeZkApp.js"
      },
      "SomethingElse": {
        "extends": "Parent",
        "implements": [],
        "filePath": "./build/src/SomeZkApp.js"
      }
    }
  2. resolveImports(filePath):

    • Purpose: To resolve import statements to their respective file paths.
    • Reason: Necessary for identifying classes that might extend SmartContract indirectly through imports from other files.

    This function parses the file to resolve import statements to their respective file paths.

    const importMappings = resolveImports('./build/src/SomeZkApp.js');

    Returns:

    {
      "someDep": "./node_modules/someDep/index.js"
    }
  3. resolveModulePath(moduleName, basePath):

    • Purpose: To resolve the path of a module based on its name and the base path of the current file.
    • Reason: Critical for correctly resolving and locating the files referenced in import statements, especially for node modules and relative paths.
  4. checkClassInheritance(className, targetClass, classesMap, visitedClasses, importMappings):

    • Purpose: To recursively check if a given class (or its ancestors) extends or implements the target class.
    • Reason: Ensures comprehensive detection of inheritance chains, even when they span multiple files.

    This recursive function checks if a given class (or its ancestors) extends or implements the target class.

    const result = checkClassInheritance(
      "AnotherTestZkApp",
      "SmartContract",
      classesMap,
      new Set(),
      importMappings
    );

    Returns:

    true

    Steps

    1. Avoid infinite loops: Checks if the class has already been visited to prevent infinite loops.
    2. Extend class hierarchy: If the class is not found in the current hierarchy, it tries to extend the hierarchy using the resolved imports.
    3. Direct check: Checks if the class directly extends or implements the SmartContract class.
    4. Recursive check: Recursively checks the parent class and implemented interfaces.

    Other code segments

    if (!classesMap[className]) {
        let resolvedPath = importMappings[className];
        if (!resolvedPath) return false;
    
        Object.assign(classesMap, buildClassHierarchy(resolvedPath));
    }
    • Purpose:
      • This block handles cases where the class being checked (className) is not found in the current class hierarchy map (classesMap).
      • It attempts to resolve the file path of the class from the import mappings (importMappings) and then extends the class hierarchy by parsing the resolved file.
    • Reason:
      • This code ensures that even if the class hierarchy is split across multiple files, all relevant classes are considered.
      • It allows the method to dynamically build a comprehensive class hierarchy that includes classes defined across multiple files.