diff --git a/src/main.ts b/src/main.ts index b87015e..6cf710c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,6 +7,9 @@ import { Processor } from './processor'; import { Config } from './types'; import { Validator } from './validator'; +/** + * Main function that processes the sources and prints the warnings + */ (async () => { const config: Config = getArguments(); @@ -34,6 +37,10 @@ import { Validator } from './validator'; }); })().catch(console.error); +/** + * Parses the command line arguments and returns the configuration + * @returns {Config} + */ function getArguments(): Config { return yargs(hideBin(process.argv)) .strict() diff --git a/src/processor.ts b/src/processor.ts index 5d91a02..51ee62f 100644 --- a/src/processor.ts +++ b/src/processor.ts @@ -1,17 +1,21 @@ import fs from 'fs/promises'; import { Validator } from './validator'; import { SourceUnit, FunctionDefinition, ContractDefinition } from 'solc-typed-ast'; -import { NodeToProcess } from './types'; +import { NodeToProcess, IWarning } from './types'; import { getLineNumberFromSrc, parseNodeNatspec } from './utils'; -interface IWarning { - location: string; - messages: string[]; -} - +/** + * Processor class that processes the source files + */ export class Processor { constructor(private validator: Validator) {} + /** + * Goes through all functions, modifiers, state variables, structs, enums, errors and events + * of the source files and validates their natspec + * @param {SourceUnit[]} sourceUnits - The list of source files + * @returns {Promise} - The list of resulting warnings + */ async processSources(sourceUnits: SourceUnit[]): Promise { const warnings: IWarning[] = []; @@ -41,6 +45,12 @@ export class Processor { return warnings; } + /** + * Selects the nodes that are eligible for natspec validation: + * Enums, Errors, Events, Functions, Modifiers, State Variables, Structs + * @param {ContractDefinition} contract - The contract source + * @returns {NodeToProcess[]} - The list of nodes to process + */ selectEligibleNodes(contract: ContractDefinition): NodeToProcess[] { return [ ...contract.vEnums, @@ -53,11 +63,24 @@ export class Processor { ]; } + /** + * Validates the natspec of the node + * @param {NodeToProcess} node - The node to process + * @returns {string[]} - The list of warning messages + */ validateNatspec(node: NodeToProcess): string[] { const nodeNatspec = parseNodeNatspec(node); return this.validator.validate(node, nodeNatspec); } + /** + * Generates a warning location string + * @param {string} filePath - Path of the file with the warning + * @param {string} fileContent - The content of the file + * @param {string} contractName - The name of the contract + * @param {NodeToProcess} node - The node with the warning + * @returns {string} - The formatted location + */ formatLocation(filePath: string, fileContent: string, contractName: string, node: NodeToProcess): string { // the constructor function definition does not have a name, but it has kind: 'constructor' const nodeName = node instanceof FunctionDefinition ? node.name || node.kind : node.name; diff --git a/src/types.ts b/src/types.ts index d4d4e2a..ae47197 100644 --- a/src/types.ts +++ b/src/types.ts @@ -49,3 +49,8 @@ export type NodeToProcess = | ModifierDefinition | VariableDeclaration | StructDefinition; + +export interface IWarning { + location: string; + messages: string[]; +} diff --git a/src/utils.ts b/src/utils.ts index b7dd460..d1ec19d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -3,10 +3,21 @@ import path from 'path'; import { ASTKind, ASTReader, SourceUnit, compileSol, FunctionDefinition } from 'solc-typed-ast'; import { Natspec, NatspecDefinition, NodeToProcess } from './types'; +/** + * Returns the absolute paths of the Solidity files + * @param {string[]} files - The list of files paths + * @returns {Promise} - The list of absolute paths + */ export async function getSolidityFilesAbsolutePaths(files: string[]): Promise { return files.filter((file) => file.endsWith('.sol')).map((file) => path.resolve(file)); } +/** + * Returns the list of source units of the compiled Solidity files + * @param {string} rootPath - The root path of the project + * @param {string[]} includedPaths - The list of included paths + * @returns + */ export async function getProjectCompiledSources(rootPath: string, includedPaths: string[]): Promise { // Fetch Solidity files from the specified directory const solidityFiles: string[] = await getSolidityFilesAbsolutePaths(includedPaths); @@ -26,11 +37,12 @@ export async function getProjectCompiledSources(rootPath: string, includedPaths: ); } -export async function getFileCompiledSource(filePath: string): Promise { - const compiledFile = await compileSol(filePath, 'auto'); - return new ASTReader().read(compiledFile.data, ASTKind.Any, compiledFile.files)[0]; -} - +/** + * Checks if the file path is in the specified directory + * @param {string} directory - The directory path + * @param {string} filePath - The file path + * @returns + */ export function isFileInDirectory(directory: string, filePath: string): boolean { // Convert both paths to absolute and normalize them const absoluteDirectoryPath = path.resolve(directory) + path.sep; @@ -40,6 +52,11 @@ export function isFileInDirectory(directory: string, filePath: string): boolean return absoluteFilePath.startsWith(absoluteDirectoryPath); } +/** + * Returns the remappings from the remappings.txt file or foundry.toml + * @param {string} rootPath - The root path of the project + * @returns {Promise} - The list of remappings + */ export async function getRemappings(rootPath: string): Promise { // First try the remappings.txt file try { @@ -54,6 +71,11 @@ export async function getRemappings(rootPath: string): Promise { } } +/** + * Returns the remappings from the remappings.txt file + * @param {string} remappingsPath - The path of the remappings file + * @returns {Promise} - The list of remappings + */ export async function getRemappingsFromFile(remappingsPath: string): Promise { const remappingsContent = await fs.readFile(remappingsPath, 'utf8'); @@ -64,6 +86,11 @@ export async function getRemappingsFromFile(remappingsPath: string): Promise (line.slice(-1) === '/' ? line : line + '/')); } +/** + * Returns the remappings from the foundry.toml file + * @param {string} foundryConfigPath - The path of the foundry.toml file + * @returns {Promise} - The list of remappings + */ export async function getRemappingsFromConfig(foundryConfigPath: string): Promise { const foundryConfigContent = await fs.readFile(foundryConfigPath, 'utf8'); const regex = /\n+remappings[\s|\n]*\=[\s\n]*\[\n*\s*(?[a-zA-Z-="'\/_,\n\s]+)/; @@ -79,6 +106,11 @@ export async function getRemappingsFromConfig(foundryConfigPath: string): Promis } } +/** + * Parses the natspec of the node + * @param {NodeToProcess} node - The node to process + * @returns {Natspec} - The parsed natspec + */ export function parseNodeNatspec(node: NodeToProcess): Natspec { if (!node.documentation) { return { tags: [], params: [], returns: [] }; @@ -125,16 +157,33 @@ export function parseNodeNatspec(node: NodeToProcess): Natspec { return result; } +/** + * Returns the line number from the source code + * @param {string} fileContent - The content of the file + * @param {string} src - The node src location (e.g. "10:1:0") + * @returns {number} - The line number of the node + */ export function getLineNumberFromSrc(fileContent: string, src: string): number { const [start] = src.split(':').map(Number); const lines = fileContent.substring(0, start).split('\n'); return lines.length; // Line number } +/** + * Checks if the node matches the function kind + * @param {NodeToProcess} node - The node to process + * @param {string} kind - The function kind + * @returns {boolean} - True if the node matches the function kind + */ export function matchesFunctionKind(node: NodeToProcess, kind: string): boolean { return node instanceof FunctionDefinition && node.kind === kind; } +/** + * Returns the frequency of the elements in the array + * @param {any[]} array - The array of elements + * @returns {Record} - The frequency of the elements + */ export function getElementFrequency(array: any[]) { return array.reduce((acc, curr) => { acc[curr] = (acc[curr] || 0) + 1; diff --git a/src/validator.ts b/src/validator.ts index aee263c..f007548 100644 --- a/src/validator.ts +++ b/src/validator.ts @@ -11,13 +11,25 @@ import { ContractDefinition, } from 'solc-typed-ast'; +/** + * Validator class that validates the natspec of the nodes + */ export class Validator { config: Config; + /** + * @param {Config} config - The configuration object + */ constructor(config: Config) { this.config = config; } + /** + * Validates the natspec of the node + * @param {NodeToProcess} node - The node to validate (Enum, Function etc.) + * @param {Natspec} natspec - Parsed natspec of the node + * @returns {string[]} - The list of alerts + */ validate(node: NodeToProcess, natspec: Natspec): string[] { // Ignore fallback and receive if (matchesFunctionKind(node, 'receive') || matchesFunctionKind(node, 'fallback')) { @@ -63,7 +75,13 @@ export class Validator { return alerts; } - // All defined parameters should have natspec + /** + * Validates the natspec for parameters. + * All defined parameters should have natspec. + * @param {ErrorDefinition | FunctionDefinition | ModifierDefinition} node - The node to validate + * @param {string[]} natspecParams - The list of parameters from the natspec + * @returns {string[]} - The list of alerts + */ private validateParameters(node: ErrorDefinition | FunctionDefinition | ModifierDefinition, natspecParams: (string | undefined)[]): string[] { let definedParameters = node.vParameters.vParameters.map((p) => p.name); let alerts: string[] = []; @@ -79,7 +97,13 @@ export class Validator { return alerts; } - // All members of a struct should have natspec + /** + * Validates the natspec for members of a struct. + * All members of a struct should have natspec. + * @param {StructDefinition} node - The struct node + * @param {string[]} natspecParams - The list of parameters from the natspec + * @returns {string[]} - The list of alerts + */ private validateMembers(node: StructDefinition, natspecParams: (string | undefined)[]): string[] { let members = node.vMembers.map((p) => p.name); let alerts: string[] = []; @@ -96,7 +120,13 @@ export class Validator { return alerts; } - // All returned parameters should have natspec + /** + * Validates the natspec for return parameters. + * All returned parameters should have natspec + * @param {FunctionDefinition} node + * @param {(string | undefined)[]} natspecReturns + * @returns {string[]} - The list of alerts + */ private validateReturnParameters(node: FunctionDefinition, natspecReturns: (string | undefined)[]): string[] { let alerts: string[] = []; let functionReturns = node.vReturnParameters.vParameters.map((p) => p.name); @@ -115,6 +145,11 @@ export class Validator { return alerts; } + /** + * Checks if the node requires inheritdoc + * @param {NodeToProcess} node - The node to process + * @returns {boolean} - True if the node requires inheritdoc + */ private requiresInheritdoc(node: NodeToProcess): boolean { let _requiresInheritdoc: boolean = false;