diff --git a/packages/client/ui-primitives/src/FileTypeIcon.tsx b/packages/client/ui-primitives/src/FileTypeIcon.tsx index 1fbcee6750..fc2e3c2667 100644 --- a/packages/client/ui-primitives/src/FileTypeIcon.tsx +++ b/packages/client/ui-primitives/src/FileTypeIcon.tsx @@ -44,7 +44,9 @@ export type FileTypeIconProps = IconProps & ( } ) -const EXTENSION_TYPES: Readonly> = { +// Maps, not objects: a basename or extension equal to an `Object.prototype` key +// (`constructor`, `__proto__`) must miss instead of resolving the inherited member. +const EXTENSION_TYPES: ReadonlyMap = new Map(Object.entries({ scss: 'code', sass: 'code', less: 'code', @@ -101,13 +103,13 @@ const EXTENSION_TYPES: Readonly> = { ots: 'excel', fods: 'excel', numbers: 'excel', -} +})) -const NAME_TYPES: Readonly> = { +const NAME_TYPES: ReadonlyMap = new Map(Object.entries({ changelog: 'markdown', contributing: 'markdown', readme: 'markdown', -} +})) function basename(path: string): string { return path.slice(Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\')) + 1) @@ -140,8 +142,8 @@ export function classifyFileType( const name = basename(path).toLowerCase() const extension = fileExtension(name).toLowerCase() return classifyCodeFileType(name, extension, context) - ?? NAME_TYPES[name] - ?? EXTENSION_TYPES[extension] + ?? NAME_TYPES.get(name) + ?? EXTENSION_TYPES.get(extension) ?? 'other' } diff --git a/packages/client/ui-primitives/src/code-file-types.ts b/packages/client/ui-primitives/src/code-file-types.ts index 9b5ce75459..5bb1a5a628 100644 --- a/packages/client/ui-primitives/src/code-file-types.ts +++ b/packages/client/ui-primitives/src/code-file-types.ts @@ -61,7 +61,9 @@ export interface FileTypeProjectContext { const CODE_FILE_TYPE_SET = new Set(CODE_FILE_TYPES) -const FILE_NAME_TYPES: Readonly> = { +// Maps, not objects: a basename or extension equal to an `Object.prototype` key +// (`constructor`, `__proto__`) must miss instead of resolving the inherited member. +const FILE_NAME_TYPES: ReadonlyMap = new Map(Object.entries({ '.bash_profile': 'shell', '.bashrc': 'shell', '.env': 'env', @@ -90,7 +92,7 @@ const FILE_NAME_TYPES: Readonly> = { 'package.json': 'node', 'podfile': 'ruby', 'rakefile': 'ruby', -} +})) const FILE_NAME_PREFIX_TYPES: readonly (readonly [string, CodeFileType])[] = [ ['dockerfile.', 'docker'], @@ -109,7 +111,7 @@ const FILE_NAME_SUFFIX_TYPES: readonly (readonly [string, CodeFileType])[] = [ ['.dockerfile', 'docker'], ] -const EXTENSION_TYPES: Readonly> = { +const EXTENSION_TYPES: ReadonlyMap = new Map(Object.entries({ 'bash': 'shell', 'c': 'c', 'c++': 'cpp', @@ -217,7 +219,7 @@ const EXTENSION_TYPES: Readonly> = { 'yml': 'yaml', 'zig': 'zig', 'zsh': 'shell', -} +})) const LINK_CODE_EXTENSIONS = new Set([ 'ts', @@ -322,7 +324,7 @@ export function classifyCodeFileType( extension: string, context?: FileTypeProjectContext, ): CodeFileType | null { - const exact = FILE_NAME_TYPES[name] + const exact = FILE_NAME_TYPES.get(name) if (exact !== undefined) return exact for (const [prefix, type] of FILE_NAME_PREFIX_TYPES) { if (name.startsWith(prefix)) return type @@ -334,5 +336,5 @@ export function classifyCodeFileType( const pubspec = Object.entries(context.files).find(([path]) => basename(path).toLowerCase() === 'pubspec.yaml')?.[1] if (pubspec?.includes('flutter:') === true) return 'flutter' } - return EXTENSION_TYPES[extension] ?? null + return EXTENSION_TYPES.get(extension) ?? null } diff --git a/packages/client/ui-primitives/tests/file-type-icon.client.spec.tsx b/packages/client/ui-primitives/tests/file-type-icon.client.spec.tsx index b39b22f0f2..e9c8367d59 100644 --- a/packages/client/ui-primitives/tests/file-type-icon.client.spec.tsx +++ b/packages/client/ui-primitives/tests/file-type-icon.client.spec.tsx @@ -4,6 +4,7 @@ import { afterEach, describe, expect, it } from 'vitest' import { classifyFileType, fileExtension, FileTypeIcon, type FileTypeKind, } from '@deepseek-ai/dsh-client-ui-primitives' +import { classifyCodeFileType } from '../src/code-file-types.ts' import css from '../src/FileTypeIcon.module.css' afterEach(cleanup) @@ -58,6 +59,30 @@ describe('classifyFileType', () => { }) }) +describe('Object.prototype key names', () => { + const keys = ['constructor', '__proto__', 'toString', 'hasOwnProperty'] + + it.each(keys.flatMap(key => [ + [key, 'other'], + [`src/${key.toUpperCase()}`, 'other'], + [`archive.${key}`, 'other'], + [`${key}.md`, 'markdown'], + [`${key}.ts`, 'typescript'], + ] as [string, Exclude][]))('%s → %s', (path, type) => { + expect(classifyFileType(path)).toBe(type) + }) + + it.each(keys)('%s misses the code-file name and extension tables', (key) => { + expect(classifyCodeFileType(key, key)).toBeNull() + expect(classifyCodeFileType(key, 'ts')).toBe('typescript') + }) + + it.each(keys.flatMap(key => [key, `notes.${key}`]))('%s renders the generic file glyph', (path) => { + const { container } = render() + expect(container.querySelector('svg')!.classList.contains(css.other!)).toBe(true) + }) +}) + describe('FileTypeIcon', () => { const types: FileTypeKind[] = [ 'code', 'excel', 'folder', 'html', 'image', 'markdown', 'other', 'pdf', 'ppt', 'video', 'word',