Skip to content

Commit

Permalink
docs: add natspec
Browse files Browse the repository at this point in the history
  • Loading branch information
dristpunk committed Feb 14, 2024
1 parent 17ba2d3 commit 5bfee05
Show file tree
Hide file tree
Showing 5 changed files with 133 additions and 14 deletions.
7 changes: 7 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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()
Expand Down
35 changes: 29 additions & 6 deletions src/processor.ts
Original file line number Diff line number Diff line change
@@ -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<IWarning[]>} - The list of resulting warnings
*/
async processSources(sourceUnits: SourceUnit[]): Promise<IWarning[]> {
const warnings: IWarning[] = [];

Expand Down Expand Up @@ -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,
Expand All @@ -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;
Expand Down
5 changes: 5 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,8 @@ export type NodeToProcess =
| ModifierDefinition
| VariableDeclaration
| StructDefinition;

export interface IWarning {
location: string;
messages: string[];
}
59 changes: 54 additions & 5 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string[]>} - The list of absolute paths
*/
export async function getSolidityFilesAbsolutePaths(files: string[]): Promise<string[]> {
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<SourceUnit[]> {
// Fetch Solidity files from the specified directory
const solidityFiles: string[] = await getSolidityFilesAbsolutePaths(includedPaths);
Expand All @@ -26,11 +37,12 @@ export async function getProjectCompiledSources(rootPath: string, includedPaths:
);
}

export async function getFileCompiledSource(filePath: string): Promise<SourceUnit> {
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;
Expand All @@ -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<string[]>} - The list of remappings
*/
export async function getRemappings(rootPath: string): Promise<string[]> {
// First try the remappings.txt file
try {
Expand All @@ -54,6 +71,11 @@ export async function getRemappings(rootPath: string): Promise<string[]> {
}
}

/**
* Returns the remappings from the remappings.txt file
* @param {string} remappingsPath - The path of the remappings file
* @returns {Promise<string[]>} - The list of remappings
*/
export async function getRemappingsFromFile(remappingsPath: string): Promise<string[]> {
const remappingsContent = await fs.readFile(remappingsPath, 'utf8');

Expand All @@ -64,6 +86,11 @@ export async function getRemappingsFromFile(remappingsPath: string): Promise<str
.map((line) => (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<string[]>} - The list of remappings
*/
export async function getRemappingsFromConfig(foundryConfigPath: string): Promise<string[]> {
const foundryConfigContent = await fs.readFile(foundryConfigPath, 'utf8');
const regex = /\n+remappings[\s|\n]*\=[\s\n]*\[\n*\s*(?<remappings>[a-zA-Z-="'\/_,\n\s]+)/;
Expand All @@ -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: [] };
Expand Down Expand Up @@ -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<string, number>} - The frequency of the elements
*/
export function getElementFrequency(array: any[]) {
return array.reduce((acc, curr) => {
acc[curr] = (acc[curr] || 0) + 1;
Expand Down
41 changes: 38 additions & 3 deletions src/validator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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')) {
Expand Down Expand Up @@ -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[] = [];
Expand All @@ -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[] = [];
Expand All @@ -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);
Expand All @@ -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;

Expand Down

0 comments on commit 5bfee05

Please sign in to comment.