Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: config file #12

Merged
merged 10 commits into from
Jan 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions example-natspec-smells.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
/**
* For full explanation of each supported config, make sure to check the Config type below
*/

/** @type {import('@defi-wonderland/natspec-smells').Config} */
module.exports = {
include: 'solidity',
exclude: ['tests', 'scripts'],
};
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"license": "MIT",
"author": "Wonderland",
"main": "lib/main.js",
"types": "lib/main.d.ts",
"types": "lib/types.d.ts",
"bin": "./lib/main.js",
"scripts": {
"build": "tsc",
Expand Down
1 change: 1 addition & 0 deletions src/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { Config } from './types/config';
43 changes: 24 additions & 19 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
#!/usr/bin/env node
import yargs from 'yargs';
import { hideBin } from 'yargs/helpers';
import { Config } from './types/config.t';
import { getProjectCompiledSources } from './utils';
import { globSync } from 'fast-glob';
import { getProjectCompiledSources } from './utils';
import { Processor } from './processor';
import { Config } from './types/config';
import { Validator } from './validator';

(async () => {
const config: Config = getArguments();
const ignoredPaths = config.ignore.map((path: string) => globSync(path, { cwd: config.root })).flat();
const sourceUnits = await getProjectCompiledSources(config.root, config.contracts, ignoredPaths);

const excludedPaths = config.exclude.map((path) => globSync(path, { cwd: config.root })).flat();
const sourceUnits = await getProjectCompiledSources(config.root, config.include, excludedPaths);
if (!sourceUnits.length) return console.error('No solidity files found in the specified directory');

const processor = new Processor(config);
const validator = new Validator(config);
const processor = new Processor(validator);
const warnings = processor.processSources(sourceUnits);

warnings.forEach(({ location, messages }) => {
Expand All @@ -28,32 +31,34 @@ function getArguments(): Config {
return yargs(hideBin(process.argv))
.strict()
.options({
root: {
include: {
type: 'string',
description: 'The target root directory',
default: './',
description: 'Glob pattern of files to process.',
required: true,
},
contracts: {
exclude: {
type: 'array',
description: 'Glob patterns of files to exclude.',
default: [],
string: true,
},
root: {
type: 'string',
description: 'The directory containing your Solidity contracts',
required: true,
description: 'Root directory of the project.',
default: './',
},
enforceInheritdoc: {
type: 'boolean',
description: 'If set to true, all external and public functions must have @inheritdoc',
description: 'If set to true, all external and public functions must have @inheritdoc.',
default: true,
},
constructorNatspec: {
type: 'boolean',
description: 'True if constructor natspec is mandatory',
description: 'True if constructor natspec is mandatory.',
default: false,
},
ignore: {
describe: 'Glob pattern of files and directories to exclude from processing',
default: [],
type: 'array',
string: true,
},
})
.config()
.default('config', 'natspec-smells.config')
.parseSync();
}
23 changes: 4 additions & 19 deletions src/processor.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,15 @@
import fs from 'fs';
import { Config } from './types/config.t';
import { Validator } from './validator';
import { SourceUnit, FunctionDefinition, ContractDefinition } from 'solc-typed-ast';
import { NodeToProcess } from './types/solc-typed-ast.t';
import { parseNodeNatspec } from './utils';
import { NodeToProcess } from './types/solc-typed-ast.d';
import { getLineNumberFromSrc, parseNodeNatspec } from './utils';

interface IWarning {
location: string;
messages: string[];
}

export class Processor {
config: Config;
validator: Validator;

constructor(config: Config) {
this.config = config;
this.validator = new Validator(config);
}
constructor(private validator: Validator) {}

processSources(sourceUnits: SourceUnit[]): IWarning[] {
return sourceUnits.flatMap((sourceUnit) =>
Expand Down Expand Up @@ -60,14 +52,7 @@ export class Processor {
formatLocation(node: NodeToProcess, sourceUnit: SourceUnit, contract: ContractDefinition): 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;
const line = this.getLineNumberFromSrc(sourceUnit.absolutePath, node.src);
const line = getLineNumberFromSrc(sourceUnit.absolutePath, node.src);
return `${sourceUnit.absolutePath}:${line}\n${contract.name}:${nodeName}`;
}

private getLineNumberFromSrc(filePath: string, src: string) {
const [start] = src.split(':').map(Number);
const fileContent = fs.readFileSync(filePath, 'utf8');
const lines = fileContent.substring(0, start).split('\n');
return lines.length; // Line number
}
}
7 changes: 7 additions & 0 deletions src/types/config.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
export interface Config {
include: string; // Required: Glob pattern of files to process.
exclude: string[]; // Optional: Glob patterns of files to exclude.
root: string; // Optional: The target root directory.
enforceInheritdoc: boolean; // Optional: If set to true, all external and public functions must have @inheritdoc.
constructorNatspec: boolean; // Optional: True if constructor natspec is mandatory.
}
7 changes: 0 additions & 7 deletions src/types/config.t.ts

This file was deleted.

File renamed without changes.
File renamed without changes.
57 changes: 34 additions & 23 deletions src/utils.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,28 @@
import fs from 'fs/promises';
import path from 'path';
import { ASTKind, ASTReader, SourceUnit, compileSol } from 'solc-typed-ast';
import { Natspec, NatspecDefinition } from './types/natspec.t';
import { NodeToProcess } from './types/solc-typed-ast.t';
import { Natspec, NatspecDefinition } from './types/natspec.d';
import { NodeToProcess } from './types/solc-typed-ast.d';

export async function getProjectCompiledSources(rootPath: string, contractsPath: string, ignoredPaths: string[]): Promise<SourceUnit[]> {
export async function getSolidityFiles(dir: string): Promise<string[]> {
let files = await fs.readdir(dir, { withFileTypes: true });
let solidityFiles: string[] = [];

for (const file of files) {
const res = path.resolve(dir, file.name);
if (file.isDirectory()) {
solidityFiles = solidityFiles.concat(await getSolidityFiles(res));
} else if (file.isFile() && file.name.endsWith('.sol')) {
solidityFiles.push(res);
}
}

return solidityFiles;
}

export async function getProjectCompiledSources(rootPath: string, includedPath: string, excludedPaths: string[]): Promise<SourceUnit[]> {
// Fetch Solidity files from the specified directory
const solidityFiles: string[] = await getSolidityFiles(contractsPath);
const solidityFiles: string[] = await getSolidityFiles(includedPath);
const remappings: string[] = await getRemappings(rootPath);

const compiledFiles = await compileSol(solidityFiles, 'auto', {
Expand All @@ -19,12 +35,17 @@ export async function getProjectCompiledSources(rootPath: string, contractsPath:
new ASTReader()
.read(compiledFiles.data, ASTKind.Any, compiledFiles.files)
// avoid processing files that are not in the specified directory, e.g. node modules or other imported files
.filter((sourceUnit) => isFileInDirectory(contractsPath, sourceUnit.absolutePath))
// avoid processing files from ignored directories
.filter((sourceUnit) => !ignoredPaths.some((ignoredPath) => ignoredPath === sourceUnit.absolutePath))
.filter((sourceUnit) => isFileInDirectory(includedPath, sourceUnit.absolutePath))
// avoid processing files from excluded directories
.filter((sourceUnit) => !excludedPaths.some((excludedPath) => excludedPath === sourceUnit.absolutePath))
);
}

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];
}

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 @@ -34,22 +55,6 @@ export function isFileInDirectory(directory: string, filePath: string): boolean
return absoluteFilePath.startsWith(absoluteDirectoryPath);
}

export async function getSolidityFiles(dir: string): Promise<string[]> {
let files = await fs.readdir(dir, { withFileTypes: true });
let solidityFiles: string[] = [];

for (const file of files) {
const res = path.resolve(dir, file.name);
if (file.isDirectory()) {
solidityFiles = solidityFiles.concat(await getSolidityFiles(res));
} else if (file.isFile() && file.name.endsWith('.sol')) {
solidityFiles.push(res);
}
}

return solidityFiles;
}

export async function getRemappings(rootPath: string): Promise<string[]> {
try {
const filePath = path.join(rootPath, 'remappings.txt');
Expand Down Expand Up @@ -110,3 +115,9 @@ export function parseNodeNatspec(node: NodeToProcess): Natspec {

return result;
}

export async function getLineNumberFromSrc(fileContent: string, src: string) {
const [start] = src.split(':').map(Number);
const lines = fileContent.substring(0, start).split('\n');
return lines.length; // Line number
}
6 changes: 3 additions & 3 deletions src/validator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Natspec } from '../src/types/natspec.t';
import { Config } from './types/config.t';
import { NodeToProcess } from './types/solc-typed-ast.t';
import { Config } from './types/config';
import { Natspec } from './types/natspec';
import { NodeToProcess } from './types/solc-typed-ast';
import {
EnumDefinition,
ErrorDefinition,
Expand Down
4 changes: 2 additions & 2 deletions test/mocks.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Natspec } from '../src/types/natspec.t';
import { Natspec } from '../src/types/natspec.d';
import { SourceUnit, ContractDefinition, FunctionDefinition } from 'solc-typed-ast';
import { NodeToProcess } from '../src/types/solc-typed-ast.t';
import { NodeToProcess } from '../src/types/solc-typed-ast.d';

export function mockNatspec(mockNatspec: Partial<Natspec>): Natspec {
const natspec: Natspec = {
Expand Down
26 changes: 16 additions & 10 deletions test/processor.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { Config } from '../src/types/config';
import { ContractDefinition, FunctionDefinition, UserDefinedType, UsingForDirective } from 'solc-typed-ast';
import { getFileCompiledSource } from './utils';
import { Processor } from '../src/processor';
import { Config } from '../src/types/config.t';
import { Validator } from '../src/validator';

describe('Processor', () => {
const config: Config = {
root: '.',
contracts: './sample-data',
enforceInheritdoc: false,
constructorNatspec: false,
ignore: [],
};

const processor: Processor = new Processor(config);
let processor: Processor;

beforeAll(() => {
const config: Config = {
root: '.',
include: './sample-data',
exclude: [],
enforceInheritdoc: false,
constructorNatspec: false,
};

const validator = new Validator(config);
processor = new Processor(validator);
});

// TODO: Fix these tests
// describe('formatLocation', () => {
Expand Down
9 changes: 5 additions & 4 deletions test/validator.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import { Validator } from '../src/validator';
import { Config } from '../src/types/config.t';
import { getFileCompiledSource } from './utils';
import { NodeToProcess } from '../src/types/solc-typed-ast.t';
import { NodeToProcess } from '../src/types/solc-typed-ast.d';
import { ContractDefinition } from 'solc-typed-ast';
import { Config } from '../src/types/config';

describe('Validator', () => {
let contract: ContractDefinition;
let node: NodeToProcess;

const config: Config = {
root: '.',
contracts: './sample-data',
include: './sample-data',
exclude: [],
enforceInheritdoc: false,
constructorNatspec: false,
ignore: [],
};

const validator: Validator = new Validator(config);
Expand Down