OpenZeppelin / solidity-docgen

Documentation generator for Solidity projects
MIT License
441 stars 116 forks source link

Provide helpers to list inherited functions (and other items) #433

Open segfaultxavi opened 1 year ago

segfaultxavi commented 1 year ago

I can't find a simple way of retrieving all the methods available in a contract, including inherited ones. I'm currently working on a helper method that iterates over all linearizedBaseContracts, retrieves their methods and merges them. But I need to take into account visibility, overrides... it starts to look like a lot of work and I kind of think such a basic thing should be available out of the box. What am I missing?

frangio commented 1 year ago

You're right this should be available out of the box, there is quite a bit of complexity. I've implemented this for OpenZeppelin Contracts so you can reuse that but adding this in your templates/properties.js (or wherever you have your templates):

module.exports.inheritance = function ({ item, build }) {
  if (!isNodeType('ContractDefinition', item)) {
    throw new Error('used inherited-items on non-contract');
  }

  return item.linearizedBaseContracts
    .map(id => build.deref('ContractDefinition', id))
    .filter((c, i) => c.name !== 'Context' || i === 0);
};

module.exports['inherited-functions'] = function ({ item }) {
  const { inheritance } = item;
  const baseFunctions = new Set(inheritance.flatMap(c => c.functions.flatMap(f => f.baseFunctions ?? [])));
  return inheritance.map((contract, i) => ({
    contract,
    functions: contract.functions.filter(f => !baseFunctions.has(f.id) && (f.name !== 'constructor' || i === 0)),
  }));
};

Later use it in the contract.hbs template like {{#each inherited-functions}} ....

segfaultxavi commented 1 year ago

Thanks a lot for your answer! I've been tinkering all day (it's my first serious contact with TypeScript) and this is what I came up with:

export function allItems(this: DocItemWithContext, nodeTypeName: string) {
  if (this.nodeType == 'ContractDefinition') {

    const { deref } = this.__item_context.build;
    const parents = this.linearizedBaseContracts.map(deref('ContractDefinition'));
    let items: (EnumDefinition | ErrorDefinition | EventDefinition | FunctionDefinition | ModifierDefinition
      | StructDefinition | UserDefinedValueTypeDefinition | VariableDeclaration)[] = [];
    parents.forEach(p => {
      p.nodes.forEach(n => {
        // Filter out other types
        if (n.nodeType == 'UsingForDirective' || n.nodeType != nodeTypeName) return;
        // Filter out private fields
        if ((n.nodeType == 'VariableDeclaration' || n.nodeType == 'FunctionDefinition') &&
          (n.visibility != 'public' && n.visibility != 'external')) return;
        if (n.nodeType == 'FunctionDefinition' && n.virtual) return;
        // If this item already exists do not add it again.
        // linearizedBaseContracts returned the children first and then the parents, so if the item
        // already exists it means that it is an override, and we want to keep those (if they had any docs).
        const prev = items.find(i => i.name == n.name);
        const prevDocs = prev && (
          prev.nodeType == 'ErrorDefinition' ||
          prev.nodeType == 'EventDefinition' ||
          prev.nodeType == 'FunctionDefinition') ? prev.documentation : null;
        if (!prev || !prevDocs)
          items.push(n);
      });
    });

    items.sort((a, b) => a.name < b.name ? -1 : 1);
    return items;
  }
}

I've added this to helpers.ts and I use it with {{#each (allItems typeName)}} . I'm sure this stinks, I better study your code!