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
13 changes: 13 additions & 0 deletions pkgs/id/web/src/highlight.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,19 @@ describe("detectLanguage", () => {
expect(detectLanguage("")).toBeUndefined();
});

it("returns undefined for prototype-member extensions (#2787)", () => {
// Plain-object lookups on EXT_TO_LANG matched inherited
// Object.prototype members: these returned a function / object
// instead of a language string.
expect(detectLanguage("notes.constructor")).toBeUndefined();
expect(detectLanguage("notes.__proto__")).toBeUndefined();
});

it("returns undefined for prototype-member filenames (#2787)", () => {
expect(detectLanguage("constructor")).toBeUndefined();
expect(detectLanguage("__proto__")).toBeUndefined();
});

it("is case-insensitive for extensions", () => {
expect(detectLanguage("App.JS")).toBe("javascript");
expect(detectLanguage("style.CSS")).toBe("css");
Expand Down
14 changes: 10 additions & 4 deletions pkgs/id/web/src/highlight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ export function detectLanguage(filename: string): string | undefined {
// A known extension wins over a special-name stem:
// "makefile.md" is Markdown documentation, not a Makefile.
const ext = basename.includes(".") ? basename.split(".").pop()?.toLowerCase() : undefined;
if (ext && EXT_TO_LANG[ext]) {
// Own-key check (#2787): a plain lookup also matches inherited
// Object.prototype members, so "notes.constructor" / "notes.__proto__"
// returned the Object constructor / prototype object as a "language".
if (ext && Object.hasOwn(EXT_TO_LANG, ext)) {
return EXT_TO_LANG[ext];
}

Expand All @@ -156,11 +159,11 @@ export function detectLanguage(filename: string): string | undefined {
? basenameLower.slice(0, basenameLower.lastIndexOf("."))
: basenameLower;

if (FILENAME_TO_LANG[nameWithoutExt]) {
if (Object.hasOwn(FILENAME_TO_LANG, nameWithoutExt)) {
return FILENAME_TO_LANG[nameWithoutExt];
}
// Full basename (e.g., "Dockerfile" with no ext)
if (FILENAME_TO_LANG[basenameLower]) {
if (Object.hasOwn(FILENAME_TO_LANG, basenameLower)) {
return FILENAME_TO_LANG[basenameLower];
}

Expand Down Expand Up @@ -277,7 +280,10 @@ async function getHighlighter(): Promise<ShikiHighlighter> {
async function ensureLanguage(highlighter: ShikiHighlighter, lang: string): Promise<void> {
if (loadedLangs.has(lang)) return;

const importFn = LANG_IMPORTS[lang];
// Own-key check (#2787): `lang` can be an authored code-block language
// attribute; a plain lookup returns Object.prototype members for
// "constructor"/"__proto__", which were then called as import functions.
const importFn = Object.hasOwn(LANG_IMPORTS, lang) ? LANG_IMPORTS[lang] : undefined;
if (!importFn) {
// Language not in our static registry — mark as loaded to avoid retries
loadedLangs.add(lang);
Expand Down