Skip to content
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
88 changes: 88 additions & 0 deletions __tests__/configure.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,94 @@ describe("configuration validation", () => {
);
});

test("rejects an unknown configuration field", () => {
const configPath = path.join(tmpDir, ".lintmdrc");
writeFileSync(configPath, JSON.stringify({ extensons: [".md"] }), "utf8");

const error = captureCliError(() => getLintConfig(configPath));

expect(error.code).toBe("CONFIG_INVALID");
expect(error.detail).toContain('Unknown configuration field "extensons".');
});

test("rejects unknown fields without fuzzy suggestions", () => {
const configPath = path.join(tmpDir, ".lintmdrc");
writeFileSync(
configPath,
JSON.stringify({ excludeFile: ["dist/**"] }),
"utf8"
);

const error = captureCliError(() => getLintConfig(configPath));

expect(error.detail).toContain(
'Unknown configuration field "excludeFile".'
);
expect(error.detail).not.toContain("Did you mean");
});

test("reports all unknown fields", () => {
const configPath = path.join(tmpDir, ".lintmdrc");
writeFileSync(
configPath,
JSON.stringify({ threads: 2, extensionsX: [], extra: true }),
"utf8"
);

const error = captureCliError(() => getLintConfig(configPath));

expect(error.detail).toBe(
[
'Unknown configuration field "threads".',
'Unknown configuration field "extensionsX".',
'Unknown configuration field "extra".',
].join("\n")
);
});

test("aggregates unknown-field and type errors", () => {
const configPath = path.join(tmpDir, ".lintmdrc");
writeFileSync(
configPath,
JSON.stringify({ extensons: [".md"], excludeFiles: "node_modules" }),
"utf8"
);

const error = captureCliError(() => getLintConfig(configPath));

expect(error.detail).toBe(
[
'"excludeFiles" must be an array of strings.',
'Unknown configuration field "extensons".',
].join("\n")
);
});

test("sanitizes control characters in unknown field names", () => {
const configPath = path.join(tmpDir, ".lintmdrc");
writeFileSync(configPath, '{"evil\\u001B[31mfield": 1}', "utf8");

const error = captureCliError(() => getLintConfig(configPath));

expect(error.detail).toContain('Unknown configuration field "evil');
expect(error.detail).not.toContain("\u001B");
});

test("does not reject fields inside rules", () => {
const configPath = path.join(tmpDir, ".lintmdrc");
writeFileSync(
configPath,
JSON.stringify({
rules: { "some-future-core-rule": { option: true } },
}),
"utf8"
);

expect(getLintConfig(configPath)).toMatchObject({
rules: { "some-future-core-rule": { option: true } },
});
});

test("accepts a well-shaped configuration and keeps defaults for absent fields", () => {
const configPath = path.join(tmpDir, ".lintmdrc");
writeFileSync(
Expand Down
19 changes: 19 additions & 0 deletions src/utils/configure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ import * as path from "path";
import { CliError } from "../cli/cli-error";
import type { CLIConfig, ThreadCount } from "../types";
import { parseSize } from "./parse-size";
import { sanitizeTerminalText } from "./sanitize-terminal";

const KNOWN_CONFIG_FIELDS: ReadonlySet<string> = new Set([
"excludeFiles",
"extensions",
"rules",
]);

const collectStringArrayErrors = (field: string, value: unknown): string[] => {
if (!Array.isArray(value)) {
Expand Down Expand Up @@ -53,6 +60,18 @@ export const validateConfigShape = (
errors.push('"rules" must be an object.');
}

// Unknown root fields are rejected, not ignored: a typo like "extensons"
// would otherwise silently fall back to defaults. Field names come from
// user JSON, so sanitize before embedding in terminal output.
for (const field of Object.keys(config)) {
if (KNOWN_CONFIG_FIELDS.has(field)) continue;
errors.push(
`Unknown configuration field ${JSON.stringify(
sanitizeTerminalText(field)
)}.`
);
}

if (errors.length > 0) {
throw new CliError(
"CONFIG_INVALID",
Expand Down