Skip to content

Commit

Permalink
feat: implement better config logic (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
0xkenj1 authored Apr 10, 2024
1 parent 6f2c28f commit 9e63add
Show file tree
Hide file tree
Showing 20 changed files with 1,326 additions and 1,298 deletions.
2 changes: 1 addition & 1 deletion .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"plugins": ["eslint-plugin-jsdoc"],
"extends": ["plugin:eslint-plugin-jsdoc/recommended"],
"ignorePatterns": ["dist", ".eslintrc.cjs"],
"ignorePatterns": ["dist", ".eslintrc.cjs", "test"],
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 9,
Expand Down
1 change: 1 addition & 0 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
module.exports = {
preset: 'ts-jest',
testEnvironment: 'node',
collectCoverage: true,
};
7 changes: 4 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,18 @@
"**/*": "prettier --write --ignore-unknown"
},
"dependencies": {
"@sinclair/typebox": "0.32.14",
"ajv": "8.12.0",
"fast-glob": "3.3.2",
"lodash": "4.17.21",
"solc-typed-ast": "18.1.2",
"yargs": "17.7.2"
"yargs": "17.7.2",
"yup": "1.4.0"
},
"devDependencies": {
"@commitlint/cli": "17.6.5",
"@commitlint/config-conventional": "17.6.5",
"@faker-js/faker": "8.3.1",
"@types/jest": "29.5.11",
"@types/lodash": "4.17.0",
"@types/node": "20.10.7",
"@typescript-eslint/parser": "6.2.0",
"eslint": "8.56.0",
Expand Down
84 changes: 84 additions & 0 deletions src/config/handlers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import yargs from 'yargs';
import fs from 'fs';
import { hideBin } from 'yargs/helpers';
import _ from 'lodash';
import { defaultConfig } from '../constants';
import { configSchema } from './schemas';
import { Config } from '../types';

/**
* Gets the config from the CLI or the config file
* Prioritizes the config file over the CLI
* @param {string} configPath - The expected config path
* @returns {Config} - The config
*/
export function getConfig(configPath: string): Config {
const fileConfig = getFileConfig(configPath);
const argConfig = getArgsConfig();
// Merge default config with file config and arg config
const inputConfig = _.merge(_.merge(_.cloneDeep(defaultConfig), fileConfig), argConfig);

return configSchema.validateSync(inputConfig);
}

/**
* Retrieves the configuration from a file.
* If the file does not exist or is invalid, an empty object is returned.
* @param {string} configPath - The path to the configuration file.
* @returns {Partial<Config>} - The configuration object parsed from the file, or an empty object if the file is invalid or does not exist.
* @throws {Error} - If the config file is invalid.
*/
export function getFileConfig(configPath: string): Partial<Config> {
try {
if (!fs.existsSync(configPath)) {
return {};
}
const fileContent = fs.readFileSync(configPath, 'utf8');
return JSON.parse(fileContent) as Partial<Config>;
} catch (e) {
throw Error(`Invalid config file: ${e}`);
}
}

/**
* Retrieves the configuration from the command-line arguments.
* Parses the command-line arguments using yargs and returns a partial configuration object.
* @returns {Partial<Config>} - The configuration object parsed from the command-line arguments.
*/
export function getArgsConfig(): Partial<Config> {
const argv = yargs(hideBin(process.argv))
.options({
include: {
type: 'string',
description: 'Glob pattern of files to process.',
},
exclude: {
type: 'string',
description: 'Glob pattern of files to exclude.',
},
root: {
type: 'string',
description: 'Root directory of the project.',
},
inheritdoc: {
type: 'boolean',
description: 'If set to true, all external and public functions must have @inheritdoc.',
},
constructorNatspec: {
type: 'boolean',
description: 'If set to true, all contracts must have a natspec for the constructor.',
},
})
.parseSync();

const { $0, _, ...config } = argv;

// Remove kebab case items from config
Object.keys(config).forEach((key) => {
if (key.includes('-')) {
delete config[key];
}
});

return config;
}
2 changes: 2 additions & 0 deletions src/config/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './schemas';
export * from './handlers';
38 changes: 38 additions & 0 deletions src/config/schemas.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { object, string, boolean, date, InferType } from 'yup';

export const tagSchema = object({
tags: object({
dev: boolean().required().strict(),
notice: boolean().required().strict(),
param: boolean().required().strict(),
}),
});

export const functionSchema = object({
tags: object({
dev: boolean().required().strict(),
notice: boolean().required().strict(),
param: boolean().required().strict(),
return: boolean().required().strict(),
}),
});

export const functionConfigSchema = object({
internal: functionSchema,
external: functionSchema,
public: functionSchema,
private: functionSchema,
});

export const configSchema = object({
include: string().required().strict(),
exclude: string().strict().optional(),
root: string().required().strict(),
functions: functionConfigSchema,
events: tagSchema,
errors: tagSchema,
modifiers: tagSchema,
structs: tagSchema,
inheritdoc: boolean().required().strict(),
constructorNatspec: boolean().required().strict(),
});
53 changes: 40 additions & 13 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,43 @@
import { Functions } from './types';
import { Config } from './types';

export const defaultFunctions: Functions = {
internal: { tags: { dev: false, notice: true, return: true, param: true } },
external: { tags: { dev: false, notice: true, return: true, param: true } },
public: { tags: { dev: false, notice: true, return: true, param: true } },
private: { tags: { dev: false, notice: true, return: true, param: true } },
} as const;

export const defaultTags = {
tags: {
dev: false,
notice: true,
param: true,
export const defaultConfig: Readonly<Config> = {
include: './**/*.sol',
exclude: undefined,
root: './',
functions: {
internal: { tags: { dev: false, notice: true, return: true, param: true } },
external: { tags: { dev: false, notice: true, return: true, param: true } },
public: { tags: { dev: false, notice: true, return: true, param: true } },
private: { tags: { dev: false, notice: true, return: true, param: true } },
},
modifiers: {
tags: {
dev: false,
notice: true,
param: true,
},
},
structs: {
tags: {
dev: false,
notice: true,
param: true,
},
},
events: {
tags: {
dev: false,
notice: true,
param: true,
},
},
errors: {
tags: {
dev: false,
notice: true,
param: true,
},
},
inheritdoc: true,
constructorNatspec: false,
} as const;
61 changes: 4 additions & 57 deletions src/main.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,22 @@
#!/usr/bin/env node
import path from 'path';
import yargs from 'yargs';
import fs from 'fs';
import { hideBin } from 'yargs/helpers';
import { glob } from 'fast-glob';
import { getProjectCompiledSources, processConfig } from './utils';
import { getProjectCompiledSources } from './utils';
import { Processor } from './processor';
import { Config } from './types';
import { Validator } from './validator';
import { defaultFunctions, defaultTags } from './constants';
import { getConfig } from './config';

/**
* Main function that processes the sources and prints the warnings
*/
(async () => {
// Requires the config is in the root of the users directory
const configPath = path.join(process.cwd(), './natspec-smells.config.json');
const config: Config = await getConfig(configPath);
const config: Config = getConfig(configPath);

// TODO: Add configuration logic to the linter
const excludedPaths = config.exclude === '' ? [] : await glob(config.exclude, { cwd: config.root });
const excludedPaths = !config.exclude ? [] : await glob(config.exclude, { cwd: config.root });
const includedPaths = await glob(config.include, { cwd: config.root, ignore: excludedPaths });

const sourceUnits = await getProjectCompiledSources(config.root, includedPaths);
Expand All @@ -42,53 +39,3 @@ import { defaultFunctions, defaultTags } from './constants';
console.warn();
});
})().catch(console.error);

/**
* Gets the config from the CLI or the config file
* @dev Prioritizes the config file over the CLI
* @param {string} configPath - The expected config path
* @returns {Config} - The config
*/
async function getConfig(configPath: string): Promise<Config> {
if (fs.existsSync(configPath)) {
return await processConfig(configPath);
}

const config: Partial<Config> = yargs(hideBin(process.argv))
.options({
include: {
type: 'string',
description: 'Glob pattern of files to process.',
required: true,
},
exclude: {
type: 'string',
description: 'Glob pattern of files to exclude.',
default: '',
},
root: {
type: 'string',
description: 'Root directory of the project.',
default: './',
},
inheritdoc: {
type: 'boolean',
description: 'If set to true, all external and public functions must have @inheritdoc.',
default: true,
},
constructorNatspec: {
type: 'boolean',
description: 'If set to true, all contracts must have a natspec for the constructor.',
default: false,
},
})
.parseSync();

config.functions = defaultFunctions;
config.modifiers = defaultTags;
config.errors = defaultTags;
config.events = defaultTags;
config.structs = defaultTags;

return config as Config;
}
3 changes: 1 addition & 2 deletions src/processor.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import fs from 'fs/promises';
import { SourceUnit, FunctionDefinition, ContractDefinition } from 'solc-typed-ast';
import { Validator } from './validator';
import { NodeToProcess } from './types';
import { getLineNumberFromSrc, parseNodeNatspec } from './utils';
import { SourceUnit, FunctionDefinition, ContractDefinition } from 'solc-typed-ast';

export interface IWarning {
location: string;
messages: string[];
Expand Down
Loading

0 comments on commit 9e63add

Please sign in to comment.