@slippy-lint/slippy
Version:
A simple but powerful linter for Solidity
157 lines • 5.83 kB
JavaScript
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