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
9 changes: 7 additions & 2 deletions src/selfhost/config-lint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,13 @@ function unknownTopLevelWarnings(text: string | null | undefined): string[] {
const parsed = parseTopLevelObject(trimmed);
if (parsed === null) return [];
const keys = Object.keys(parsed).filter((key) => !TOP_LEVEL_FIELD_SET.has(key));
const retiredWarnings = keys.filter((key) => key in RETIRED_FIELD_MIGRATION_WARNINGS).map((key) => RETIRED_FIELD_MIGRATION_WARNINGS[key]!);
const unknown = keys.filter((key) => !(key in RETIRED_FIELD_MIGRATION_WARNINGS)).map(formatFieldName);
// `hasOwnProperty.call`, NOT `key in`: a manifest field named like an Object.prototype member
// (`constructor`, `toString`, `hasOwnProperty`, ...) would otherwise test true for the inherited
// property and resolve to the prototype's function instead of a real retired-field warning string,
// corrupting the string[] result and suppressing the genuine unknown-field warning.
const isRetired = (key: string): boolean => Object.prototype.hasOwnProperty.call(RETIRED_FIELD_MIGRATION_WARNINGS, key);
const retiredWarnings = keys.filter(isRetired).map((key) => RETIRED_FIELD_MIGRATION_WARNINGS[key]!);
const unknown = keys.filter((key) => !isRetired(key)).map(formatFieldName);
return [
...retiredWarnings,
...(unknown.length > 0 ? [`Manifest contains unknown top-level field${unknown.length === 1 ? "" : "s"}: ${unknown.join(", ")}.`] : []),
Expand Down
10 changes: 10 additions & 0 deletions test/unit/selfhost-config-lint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,16 @@ unknownSecretKey: super-secret-value
expect(JSON.stringify(result)).not.toContain("/tmp/private");
});

it("treats a field named like an Object.prototype member as unknown, not retired", () => {
const result = lintManifestText("wantedPaths: [src/]\nconstructor: whatever\n");

expect(result.ok).toBe(false);
expect(result.recognizedFields).toEqual(["wantedPaths"]);
expect(result.warnings).toEqual(["Manifest contains unknown top-level field: constructor."]);
// Every warning must be a real string — a prototype-name collision must never leak a function.
expect(result.warnings.every((warning) => typeof warning === "string")).toBe(true);
});

it("uses singular wording for one unknown top-level field", () => {
const result = lintManifestText("wantedPaths: [src/]\nunknownSecretKey: super-secret-value\n");

Expand Down
Loading