diff --git a/src/commands/usages/usages.test.ts b/src/commands/usages/usages.test.ts index d05b26a1..5c420ed2 100644 --- a/src/commands/usages/usages.test.ts +++ b/src/commands/usages/usages.test.ts @@ -1,3 +1,6 @@ +import fs from 'fs' +import os from 'os' +import path from 'path' import { test } from '@oclif/test' import { expect } from 'vitest' import { BASE_URL } from '../../api/common' @@ -135,4 +138,31 @@ describe('usages', () => { expect(ctx.stdout).toMatchSnapshot() }, ) + + const htmlOutputDir = fs.mkdtempSync(path.join(os.tmpdir(), 'dvc-usages-')) + const htmlOutputPath = path.join(htmlOutputDir, 'usages.json') + afterAll(() => fs.rmSync(htmlOutputDir, { recursive: true, force: true })) + + test.stdout() + .command([ + 'usages', + '--include', + 'test-utils/fixtures/usages/sample.html', + '--match-pattern', + 'html=isFeatureEnabled\\("([^"]*)"\\)', + '--format', + 'json', + '--output', + htmlOutputPath, + ]) + .it( + 'sets language from the file extension for custom match patterns', + () => { + const [usage] = JSON.parse( + fs.readFileSync(htmlOutputPath, 'utf-8'), + ) + expect(usage.key).toEqual('html-flag') + expect(usage.references[0].language).toEqual('html') + }, + ) }) diff --git a/src/utils/parsers/BaseParser.ts b/src/utils/parsers/BaseParser.ts index 8fec5d33..5ef89805 100644 --- a/src/utils/parsers/BaseParser.ts +++ b/src/utils/parsers/BaseParser.ts @@ -6,7 +6,7 @@ import { VariableUsageMatch, } from './types' import * as usage from '../../commands/usages/types' -import { LANGUAGE_MAP } from '../parsers' +import { getLanguageForExtension } from '../parsers' type MatchWithRange = { // Variable name @@ -395,7 +395,7 @@ export abstract class BaseParser { }, fileName: file.name, content: bufferedContent, - language: LANGUAGE_MAP[fileExtension], + language: getLanguageForExtension(fileExtension), ...(match.isUnknown ? { isUnknown: true } : {}), }) }) diff --git a/src/utils/parsers/index.ts b/src/utils/parsers/index.ts index a47504ba..ff8e0f73 100644 --- a/src/utils/parsers/index.ts +++ b/src/utils/parsers/index.ts @@ -55,3 +55,16 @@ export const LANGUAGE_MAP: Record = { php: 'php', dart: 'dart', } + +const SAFE_EXTENSION_PATTERN = /^[a-z0-9]{1,16}$/i + +/** + * Map a file extension to a language, falling back to the extension itself + * for custom match patterns. Unsafe extensions fall back to 'unknown'. + */ +export const getLanguageForExtension = (extension: string): string => { + if (Object.hasOwn(LANGUAGE_MAP, extension)) return LANGUAGE_MAP[extension] + return SAFE_EXTENSION_PATTERN.test(extension) + ? extension.toLowerCase() + : 'unknown' +} diff --git a/src/utils/parsers/language.test.ts b/src/utils/parsers/language.test.ts new file mode 100644 index 00000000..2c711f66 --- /dev/null +++ b/src/utils/parsers/language.test.ts @@ -0,0 +1,23 @@ +import { expect } from 'vitest' +import { getLanguageForExtension } from '.' + +describe('getLanguageForExtension', () => { + it('uses the mapped language for built-in extensions', () => { + expect(getLanguageForExtension('ts')).toEqual('typescript') + expect(getLanguageForExtension('kt')).toEqual('kotlin') + }) + + it('falls back to the extension for unmapped alphanumeric extensions', () => { + expect(getLanguageForExtension('html')).toEqual('html') + expect(getLanguageForExtension('Vue')).toEqual('vue') + expect(getLanguageForExtension('constructor')).toEqual('constructor') + }) + + it('falls back to unknown for unsafe or empty extensions', () => { + expect(getLanguageForExtension('')).toEqual( + 'unknown', + ) + expect(getLanguageForExtension('a'.repeat(17))).toEqual('unknown') + expect(getLanguageForExtension('')).toEqual('unknown') + }) +}) diff --git a/test-utils/fixtures/usages/sample.html b/test-utils/fixtures/usages/sample.html new file mode 100644 index 00000000..25a71467 --- /dev/null +++ b/test-utils/fixtures/usages/sample.html @@ -0,0 +1,9 @@ + + + + +