Skip to content

Commit

Permalink
CORE-458 | Improve the function comment of the Explorer (#5861)
Browse files Browse the repository at this point in the history
CORE-458
  • Loading branch information
kien-ngo committed Jan 5, 2025
1 parent 810f319 commit 4778966
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 52 deletions.
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { Badge } from "@/components/ui/badge";
import { CodeClient } from "@/components/ui/code/code.client";
import { useContractSources } from "contract-ui/hooks/useContractSources";
import { useMemo } from "react";
import { useContractFunctionComment } from "contract-ui/hooks/useContractFunctionComment";
import type { ThirdwebContract } from "thirdweb";

/**
Expand All @@ -11,66 +10,24 @@ export default function ContractFunctionComment({
contract,
functionName,
}: { contract: ThirdwebContract; functionName: string }) {
const sourceQuery = useContractSources(contract);
const comment = useMemo(() => {
if (!sourceQuery.data?.length) {
return null;
}
const file = sourceQuery.data.find((item) =>
item.source.includes(functionName),
);
if (!file) {
return null;
}
return extractFunctionComment(file.source, functionName);
}, [sourceQuery.data, functionName]);
const query = useContractFunctionComment(contract, functionName);

if (sourceQuery.isLoading) {
if (query.isLoading) {
return null;
}
if (!comment) {
if (!query.data) {
return null;
}
return (
<>
<p className="mt-6">
About this function <Badge>Beta</Badge>
</p>
<CodeClient lang="solidity" code={comment} />
<CodeClient
lang="wikitext"
code={query.data}
copyButtonClassName="hidden"
/>
</>
);
}

function extractFunctionComment(
// Tthe whole code from the solidity file containing (possibly) the function
solidityCode: string,
functionName: string,
): string | null {
// Regular expression to match function declarations and their preceding comments
// This regex now captures both single-line (//) and multi-line (/** */) comments
const functionRegex =
/(?:\/\/[^\n]*|\/\*\*[\s\S]*?\*\/)\s*function\s+(\w+)\s*\(/g;

while (true) {
const match = functionRegex.exec(solidityCode);
if (match === null) {
return null;
}
const [fullMatch, name] = match;
if (!fullMatch || !fullMatch.length) {
return null;
}
if (name === functionName) {
// Extract the comment part
const comment = (fullMatch.split("function")[0] || "").trim();
if (!comment) {
return null;
}

if (/^[^a-zA-Z0-9]+$/.test(comment)) {
return null;
}
return comment;
}
}
}
139 changes: 139 additions & 0 deletions apps/dashboard/src/contract-ui/hooks/useContractFunctionComment.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
import { useThirdwebClient } from "@/constants/thirdweb.client";
import { useQuery } from "@tanstack/react-query";
import type { ThirdwebContract } from "thirdweb";
import { getCompilerMetadata } from "thirdweb/contract";
import { download } from "thirdweb/storage";

/**
* Try to extract the description (or comment) about a contract's method from our contract metadata endpoint
*
* An example of a contract that has both userdoc and devdoc:
* https://contract.thirdweb.com/metadata/1/0x303a465B659cBB0ab36eE643eA362c509EEb5213
*/
export function useContractFunctionComment(
contract: ThirdwebContract,
functionName: string,
) {
const client = useThirdwebClient();
return useQuery({
queryKey: [
"contract-function-comment",
contract?.chain.id || "",
contract?.address || "",
functionName,
],
queryFn: async (): Promise<string> => {
const data = await getCompilerMetadata(contract);
let comment = "";
/**
* If the response data contains userdoc and/or devdoc
* we always prioritize using them. parsing the comment using regex should
* always be the last resort
*/
if (data.metadata.output.devdoc?.methods) {
const keys = Object.keys(data.metadata.output.devdoc.methods);
const matchingKey = keys.find(
(rawKey) =>
rawKey.startsWith(functionName) &&
rawKey.split("(")[0] === functionName,
);
const devDocContent = matchingKey
? data.metadata.output.devdoc.methods[matchingKey]?.details
: undefined;
if (devDocContent) {
comment += `@dev-doc: ${devDocContent}\n`;
}
}
if (data.metadata.output.userdoc?.methods) {
const keys = Object.keys(data.metadata.output.userdoc.methods);
const matchingKey = keys.find(
(rawKey) =>
rawKey.startsWith(functionName) &&
rawKey.split("(")[0] === functionName,
);
const userDocContent = matchingKey
? data.metadata.output.userdoc.methods[matchingKey]?.notice
: undefined;
if (userDocContent) {
comment += `@user-doc: ${userDocContent}\n`;
}
}
if (comment) {
return comment;
}
if (!data.metadata.sources) {
return "";
}
const sources = await Promise.all(
Object.entries(data.metadata.sources).map(async ([path, info]) => {
if ("content" in info) {
return {
filename: path,
source: info.content || "Could not find source for this file",
};
}
const urls = info.urls;
const ipfsLink = urls
? urls.find((url) => url.includes("ipfs"))
: undefined;
if (ipfsLink) {
const ipfsHash = ipfsLink.split("ipfs/")[1];
const source = await download({
uri: `ipfs://${ipfsHash}`,
client,
})
.then((r) => r.text())
.catch(() => "Failed to fetch source from IPFS");
return {
filename: path,
source,
};
}
return {
filename: path,
source: "Could not find source for this file",
};
}),
);
const file = sources.find((item) => item.source.includes(functionName));
if (!file) {
return "";
}
return extractFunctionComment(file.source, functionName);
},
});
}

function extractFunctionComment(
// The whole code from the solidity file containing (possibly) the function
solidityCode: string,
functionName: string,
): string {
// Regular expression to match function declarations and their preceding comments
// This regex now captures both single-line (//) and multi-line (/** */) comments
const functionRegex =
/(?:\/\/[^\n]*|\/\*\*[\s\S]*?\*\/)\s*function\s+(\w+)\s*\(/g;

while (true) {
const match = functionRegex.exec(solidityCode);
if (match === null) {
return "";
}
const [fullMatch, name] = match;
if (!fullMatch || !fullMatch.length) {
return "";
}
if (name === functionName) {
// Extract the comment part
const comment = (fullMatch.split("function")[0] || "").trim();
if (!comment) {
return "";
}

if (/^[^a-zA-Z0-9]+$/.test(comment)) {
return "";
}
return comment;
}
}
}
5 changes: 5 additions & 0 deletions packages/thirdweb/src/contract/actions/compiler-metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ export type CompilerMetadata = {
// biome-ignore lint/suspicious/noExplicitAny: TODO: fix later by updating this type to match the specs here: https://docs.soliditylang.org/en/latest/metadata.html
metadata: Record<string, any> & {
sources: Record<string, { content: string } | { urls: string[] }>;
output: {
abi: Abi;
devdoc?: Record<string, Record<string, { details: string }>>;
userdoc?: Record<string, Record<string, { notice: string }>>;
};
};
info: {
title?: string;
Expand Down

0 comments on commit 4778966

Please sign in to comment.