UNPKG

@slippy-lint/slippy

Version:

A simple but powerful linter for Solidity

157 lines 5.83 kB
import { pathToFileURL } from "node:url"; import micromatch from "micromatch"; import { z } from "zod"; import { findUp } from "./helpers/fs.js"; import { SeveritySchema } from "./rules/types.js"; import { SlippyConfigLoadingError, SlippyConfigNotFoundError, SlippyInvalidConfigError, } from "./errors.js"; import { conditionalUnionType } from "./zod.js"; const RuleConfigSchema = z.custom((val) => { const error = validateRuleConfig(val); return error === undefined; }, { error: (ctx) => { return { message: validateRuleConfig(ctx.input) ?? "Invalid rule configuration", }; }, }); const UserConfigObjectSchema = z.strictObject({ rules: z.record(z.string(), RuleConfigSchema).optional(), files: z.array(z.string()).optional(), ignores: z.array(z.string()).optional(), }); const UserConfigSchema = conditionalUnionType([ [(x) => Array.isArray(x), z.array(UserConfigObjectSchema).nonempty()], [(x) => typeof x === "object" && x !== null, UserConfigObjectSchema], ], "Configuration must be an object or an array of objects"); function validateRuleConfig(config) { const invalidSeverityMessage = `Invalid option: expected severity to be "off", "warn", or "error"`; const levelAsSeverityMessage = `Invalid option: severity can't be specified as a number, use one of "off", "warn", or "error"`; if (typeof config === "string") { const parsedConfig = SeveritySchema.safeParse(config); if (!parsedConfig.success) { return invalidSeverityMessage; } return; } if (!Array.isArray(config)) { if (typeof config === "number" && 0 <= config && config <= 2) { return levelAsSeverityMessage; } return "Invalid option: expected a string or an array"; } if (config.length === 0) { return "Invalid option: expected a non-empty array"; } if (config.length > 2) { return "Invalid option: expected an array with at most two elements"; } if (typeof config[0] !== "string") { if (typeof config[0] === "number" && 0 <= config[0] && config[0] <= 2) { return levelAsSeverityMessage; } return "Invalid option: expected the first element to be a string"; } const severity = config[0]; const parsedSeverity = SeveritySchema.safeParse(severity); if (!parsedSeverity.success) { return invalidSeverityMessage; } } async function loadSlippyConfig(slippyConfigPath) { try { // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access return (await import(pathToFileURL(slippyConfigPath).href)).default; } catch (error) { if (error instanceof Error) { throw new SlippyConfigLoadingError(slippyConfigPath, error.message); } throw error; } } export async function createConfigLoader(configPath) { const userConfig = await loadSlippyConfig(configPath); validateUserConfig(userConfig, configPath); return BasicConfigLoader.create(userConfig); } export class BasicConfigLoader { constructor(config) { this.config = config; } static create(userConfig) { const resolvedConfig = resolveConfig(userConfig); return new BasicConfigLoader(resolvedConfig); } loadConfig(filePath) { const mergedConfig = { rules: {}, }; for (const configObject of this.config) { const slicedFilePath = filePath.startsWith("./") ? filePath.slice(2) : filePath; if (configObject.ignores.length > 0 && micromatch([slicedFilePath], configObject.ignores).length > 0) { continue; } if (configObject.files.length > 0) { if (micromatch([slicedFilePath], configObject.files).length === 0) { continue; } } mergedConfig.rules = { ...mergedConfig.rules, ...configObject.rules, }; } return mergedConfig; } } export function validateUserConfig(userConfig, slippyConfigPath) { if (userConfig === undefined) { throw new SlippyInvalidConfigError(slippyConfigPath, "Configuration must be an object", "Did you forget to export the config?"); } const parsedConfig = UserConfigSchema.safeParse(userConfig); if (parsedConfig.success) { return; } throw new SlippyInvalidConfigError(slippyConfigPath, "\n\n" + z.prettifyError(parsedConfig.error)); } export async function tryFindSlippyConfigPath(cwd) { return findUp("slippy.config.js", cwd); } export async function findSlippyConfigPath(cwd) { const configPath = await tryFindSlippyConfigPath(cwd); if (configPath === undefined) { throw new SlippyConfigNotFoundError(); } return configPath; } function resolveConfig(userConfig) { if (Array.isArray(userConfig)) { return userConfig.map(resolveConfigObject); } return [resolveConfigObject(userConfig)]; } function resolveConfigObject(userConfigObject) { const rules = {}; for (const [ruleName, ruleConfig] of Object.entries(userConfigObject.rules ?? {})) { if (typeof ruleConfig === "string") { rules[ruleName] = [ruleConfig]; } else { rules[ruleName] = ruleConfig; } } if (userConfigObject.files !== undefined && userConfigObject.files.length === 0) { throw new SlippyInvalidConfigError("slippy.config.js", "If a configuration includes a `files` property, it must not be an empty array"); } return { rules, ignores: userConfigObject.ignores ?? [], files: userConfigObject.files ?? [], }; } //# sourceMappingURL=config.js.map