From 8146b07f24ca5569be3e74f3730abc782c1e2bc3 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 13 Oct 2026 13:38:41 -0400 Subject: [PATCH 1/3] fix: fall back to file extension for usages language with custom match patterns --- src/commands/usages/usages.test.ts | 27 ++++++++++++++++++++++++++ src/utils/parsers/BaseParser.ts | 2 +- test-utils/fixtures/usages/sample.html | 9 +++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 test-utils/fixtures/usages/sample.html diff --git a/src/commands/usages/usages.test.ts b/src/commands/usages/usages.test.ts index d05b26a1..43c4cf39 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,28 @@ describe('usages', () => { expect(ctx.stdout).toMatchSnapshot() }, ) + + const htmlOutputPath = path.join(os.tmpdir(), 'dvc-usages-html.json') + 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..d77337ac 100644 --- a/src/utils/parsers/BaseParser.ts +++ b/src/utils/parsers/BaseParser.ts @@ -395,7 +395,7 @@ export abstract class BaseParser { }, fileName: file.name, content: bufferedContent, - language: LANGUAGE_MAP[fileExtension], + language: LANGUAGE_MAP[fileExtension] ?? fileExtension, ...(match.isUnknown ? { isUnknown: true } : {}), }) }) 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 @@ + + + + + From f79d39b80eb5f4ae15f2984126a635a2f0b54b1b Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 13 Oct 2026 14:09:03 -0400 Subject: [PATCH 2/3] fix: restrict usages language fallback to alphanumeric extensions --- src/utils/parsers/BaseParser.ts | 4 ++-- src/utils/parsers/index.ts | 13 +++++++++++++ src/utils/parsers/language.test.ts | 22 ++++++++++++++++++++++ 3 files changed, 37 insertions(+), 2 deletions(-) create mode 100644 src/utils/parsers/language.test.ts diff --git a/src/utils/parsers/BaseParser.ts b/src/utils/parsers/BaseParser.ts index d77337ac..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] ?? fileExtension, + language: getLanguageForExtension(fileExtension), ...(match.isUnknown ? { isUnknown: true } : {}), }) }) diff --git a/src/utils/parsers/index.ts b/src/utils/parsers/index.ts index a47504ba..dfba81ed 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 (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..c1240123 --- /dev/null +++ b/src/utils/parsers/language.test.ts @@ -0,0 +1,22 @@ +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') + }) + + 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') + }) +}) From 347592509d1edbc2d7047fbca9db1c207a299336 Mon Sep 17 00:00:00 2001 From: Jonathan Norris Date: Tue, 13 Oct 2026 14:34:13 -0400 Subject: [PATCH 3/3] fix: isolate usages test output and restrict language map lookups to own keys --- src/commands/usages/usages.test.ts | 5 ++++- src/utils/parsers/index.ts | 2 +- src/utils/parsers/language.test.ts | 1 + 3 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/commands/usages/usages.test.ts b/src/commands/usages/usages.test.ts index 43c4cf39..5c420ed2 100644 --- a/src/commands/usages/usages.test.ts +++ b/src/commands/usages/usages.test.ts @@ -139,7 +139,10 @@ describe('usages', () => { }, ) - const htmlOutputPath = path.join(os.tmpdir(), 'dvc-usages-html.json') + 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', diff --git a/src/utils/parsers/index.ts b/src/utils/parsers/index.ts index dfba81ed..ff8e0f73 100644 --- a/src/utils/parsers/index.ts +++ b/src/utils/parsers/index.ts @@ -63,7 +63,7 @@ const SAFE_EXTENSION_PATTERN = /^[a-z0-9]{1,16}$/i * for custom match patterns. Unsafe extensions fall back to 'unknown'. */ export const getLanguageForExtension = (extension: string): string => { - if (LANGUAGE_MAP[extension]) return LANGUAGE_MAP[extension] + 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 index c1240123..2c711f66 100644 --- a/src/utils/parsers/language.test.ts +++ b/src/utils/parsers/language.test.ts @@ -10,6 +10,7 @@ describe('getLanguageForExtension', () => { 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', () => {