Skip to content
Open
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
1 change: 1 addition & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

## Next
- Add `mops info <pkg>` command to show detailed package metadata from the registry
- Add `[lint.extra]` config for applying additional lint rules to specific files via glob patterns

## 2.8.1

Expand Down
174 changes: 132 additions & 42 deletions cli/commands/lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,62 @@ export interface LintOptions {
extraArgs: string[];
}

function buildCommonArgs(
options: Partial<LintOptions>,
config: Config,
): string[] {
const args: string[] = [];
if (options.verbose) {
args.push("--verbose");
}
if (options.fix) {
args.push("--fix");
}
if (config.lint?.args) {
if (typeof config.lint.args === "string") {
cliError(
`[lint] config 'args' should be an array of strings in mops.toml config file`,
);
}
args.push(...config.lint.args);
}
if (options.extraArgs && options.extraArgs.length > 0) {
args.push(...options.extraArgs);
}
return args;
}

async function runLintoko(
lintokoBinPath: string,
rootDir: string,
args: string[],
options: Partial<LintOptions>,
label: string,
): Promise<boolean> {
try {
if (options.verbose) {
console.log(
chalk.blue("lint"),
chalk.gray(`Running lintoko (${label}):`),
);
console.log(chalk.gray(lintokoBinPath));
console.log(chalk.gray(JSON.stringify(args)));
}

const result = await execa(lintokoBinPath, args, {
cwd: rootDir,
stdio: "inherit",
reject: false,
});

return result.exitCode === 0;
} catch (err: any) {
cliError(
`Error while running lintoko${err?.message ? `\n${err.message}` : ""}`,
);
}
}

export async function lint(
filter: string | undefined,
options: Partial<LintOptions>,
Expand Down Expand Up @@ -131,59 +187,93 @@ export async function lint(
}
}

let args: string[] = [];
if (options.verbose) {
args.push("--verbose");
}
if (options.fix) {
args.push("--fix");
}
const commonArgs = buildCommonArgs(options, config);

// --- base run ---
const baseArgs: string[] = [...commonArgs];
const rules =
options.rules !== undefined
? options.rules
: await collectLintRules(config, rootDir);
rules.forEach((rule) => args.push("--rules", rule));
rules.forEach((rule) => baseArgs.push("--rules", rule));
baseArgs.push(...filesToLint);

if (config.lint?.args) {
if (typeof config.lint.args === "string") {
cliError(
`[lint] config 'args' should be an array of strings in mops.toml config file`,
);
}
args.push(...config.lint.args);
}
let failed = !(await runLintoko(
lintokoBinPath,
rootDir,
baseArgs,
options,
"base",
));

if (options.extraArgs && options.extraArgs.length > 0) {
args.push(...options.extraArgs);
}
// --- extra runs ---
const extraEntries = config.lint?.extra;
if (extraEntries) {
const isFiltered = filter || (options.files && options.files.length > 0);
const baseFileSet = isFiltered
? new Set(filesToLint.map((f) => path.resolve(rootDir, f)))
: undefined;

args.push(...filesToLint);
for (const [globPattern, ruleDirs] of Object.entries(extraEntries)) {
if (!Array.isArray(ruleDirs) || ruleDirs.length === 0) {
console.warn(
chalk.yellow(
`[lint.extra] skipping '${globPattern}': value must be a non-empty array of rule directories`,
),
);
continue;
}

try {
if (options.verbose) {
console.log(chalk.blue("lint"), chalk.gray("Running lintoko:"));
console.log(chalk.gray(lintokoBinPath));
console.log(chalk.gray(JSON.stringify(args)));
}
for (const dir of ruleDirs) {
if (!existsSync(path.join(rootDir, dir))) {
cliError(
`[lint.extra] rule directory '${dir}' not found (referenced by glob '${globPattern}')`,
);
}
}

const result = await execa(lintokoBinPath, args, {
cwd: rootDir,
stdio: "inherit",
reject: false,
});
let matchedFiles = globSync(path.join(rootDir, globPattern), {
...MOTOKO_GLOB_CONFIG,
cwd: rootDir,
});

if (result.exitCode !== 0) {
cliError(`Lint failed with exit code ${result.exitCode}`);
}
if (baseFileSet) {
matchedFiles = matchedFiles.filter((f) =>
baseFileSet.has(path.resolve(rootDir, f)),
);
}

if (options.fix) {
console.log(chalk.green("✓ Lint fixes applied"));
} else {
console.log(chalk.green("✓ Lint succeeded"));
if (matchedFiles.length === 0) {
console.warn(
chalk.yellow(
`[lint.extra] no files matched glob '${globPattern}', skipping`,
),
);
continue;
}

const extraArgs: string[] = [...commonArgs];
for (const dir of ruleDirs) {
extraArgs.push("--rules", dir);
}
extraArgs.push(...matchedFiles);

const passed = await runLintoko(
lintokoBinPath,
rootDir,
extraArgs,
options,
`extra: ${globPattern}`,
);
failed ||= !passed;
}
} catch (err: any) {
cliError(
`Error while running lintoko${err?.message ? `\n${err.message}` : ""}`,
);
}

if (failed) {
cliError("Lint failed");
} else if (options.fix) {
console.log(chalk.green("✓ Lint fixes applied"));
} else {
console.log(chalk.green("✓ Lint succeeded"));
}
}
Loading
Loading