Skip to content

Commit 9debedd

Browse files
committed
Fix color issue
1 parent df2e1e5 commit 9debedd

File tree

2 files changed

+200
-1
lines changed

2 files changed

+200
-1
lines changed

src/analyzer.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -726,6 +726,7 @@ const SK_NamespaceImport = ts.SyntaxKind.NamespaceImport
726726
const SK_NamedExports = ts.SyntaxKind.NamedExports
727727
const SK_ExportKw = ts.SyntaxKind.ExportKeyword
728728
const SK_DefaultKw = ts.SyntaxKind.DefaultKeyword
729+
const SK_TypeLiteral = ts.SyntaxKind.TypeLiteral
729730

730731
function isComponentIdentifier(name: string): boolean {
731732
const code = name.charCodeAt(0)
@@ -829,6 +830,7 @@ function collectSourceElements(
829830

830831
let currentComponent: string | undefined
831832
let currentComponentTracked = false
833+
let typeLiteralDepth = 0
832834

833835
const visit = (node: ts.Node): void => {
834836
const nodeKind = node.kind
@@ -841,6 +843,9 @@ function collectSourceElements(
841843
return
842844
}
843845

846+
const isTypeLiteral = nodeKind === SK_TypeLiteral
847+
if (isTypeLiteral) typeLiteralDepth++
848+
844849
const entry = componentByPos.get(node.pos)
845850
const entered = entry !== undefined && entry.end === node.end
846851

@@ -869,7 +874,7 @@ function collectSourceElements(
869874
if (jsxTag) {
870875
jsxTags.push(jsxTag)
871876
}
872-
} else if (nodeKind === SK_TypeReference) {
877+
} else if (nodeKind === SK_TypeReference && typeLiteralDepth === 0) {
873878
const typeName = (node as ts.TypeReferenceNode).typeName
874879
if (
875880
typeName.kind === SK_Identifier &&
@@ -937,6 +942,8 @@ function collectSourceElements(
937942

938943
ts.forEachChild(node, visit)
939944

945+
if (isTypeLiteral) typeLiteralDepth--
946+
940947
if (entered) {
941948
currentComponent = savedComponent
942949
currentComponentTracked = savedTracked

test/analyzer.test.ts

Lines changed: 192 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { expect, test } from 'bun:test'
77
import { ComponentLensAnalyzer, type ScopeConfig } from '../src/analyzer'
88
import {
99
createDiskSignature,
10+
createOpenSignature,
1011
ImportResolver,
1112
type SourceHost,
1213
} from '../src/resolver'
@@ -1688,6 +1689,197 @@ test('codelens scope tracks source file paths for imports', async () => {
16881689
}
16891690
})
16901691

1692+
test('does not color type references inside inline object types', async () => {
1693+
const project = createProject({
1694+
'Button.tsx': [
1695+
"'use client';",
1696+
'',
1697+
'interface ButtonProps {',
1698+
' label: string;',
1699+
'}',
1700+
'',
1701+
'function Button(props: { children: ReactNode }) {',
1702+
' return <button />;',
1703+
'}',
1704+
'',
1705+
'function IconButton(props: ButtonProps) {',
1706+
' return <button />;',
1707+
'}',
1708+
].join('\n'),
1709+
})
1710+
1711+
try {
1712+
const analyzer = createAnalyzer(project.host)
1713+
const filePath = project.filePath('Button.tsx')
1714+
const source = project.readFile('Button.tsx')
1715+
const scope: ScopeConfig = {
1716+
declaration: false,
1717+
element: false,
1718+
export: false,
1719+
import: false,
1720+
type: true,
1721+
}
1722+
const usages = await analyzer.analyzeDocument(
1723+
filePath,
1724+
source,
1725+
project.signature('Button.tsx'),
1726+
scope,
1727+
)
1728+
1729+
expect(usages.map((u) => u.tagName)).toEqual(['ButtonProps', 'ButtonProps'])
1730+
} finally {
1731+
project[Symbol.dispose]()
1732+
}
1733+
})
1734+
1735+
test('colors named type reference but not inline object type members', async () => {
1736+
const project = createProject({
1737+
'Card.tsx': [
1738+
'interface CardProps {',
1739+
' title: string;',
1740+
'}',
1741+
'',
1742+
'function Card({ title }: CardProps) {',
1743+
' return <div>{title}</div>;',
1744+
'}',
1745+
'',
1746+
'function Badge(props: { icon: ReactElement, label: string }) {',
1747+
' return <span />;',
1748+
'}',
1749+
].join('\n'),
1750+
})
1751+
1752+
try {
1753+
const analyzer = createAnalyzer(project.host)
1754+
const filePath = project.filePath('Card.tsx')
1755+
const source = project.readFile('Card.tsx')
1756+
const scope: ScopeConfig = {
1757+
declaration: false,
1758+
element: false,
1759+
export: false,
1760+
import: false,
1761+
type: true,
1762+
}
1763+
const usages = await analyzer.analyzeDocument(
1764+
filePath,
1765+
source,
1766+
project.signature('Card.tsx'),
1767+
scope,
1768+
)
1769+
1770+
expect(usages.map((u) => u.tagName)).toEqual(['CardProps', 'CardProps'])
1771+
} finally {
1772+
project[Symbol.dispose]()
1773+
}
1774+
})
1775+
1776+
test('findComponentDeclaration returns position for existing component', async () => {
1777+
const project = createProject({
1778+
'Card.tsx': [
1779+
"'use client';",
1780+
'',
1781+
'export function Card() {',
1782+
' return <div />;',
1783+
'}',
1784+
].join('\n'),
1785+
})
1786+
1787+
try {
1788+
const analyzer = createAnalyzer(project.host)
1789+
const filePath = project.filePath('Card.tsx')
1790+
1791+
const result = await analyzer.findComponentDeclaration(filePath, 'Card')
1792+
expect(result).toEqual({ line: 2, character: 16 })
1793+
} finally {
1794+
project[Symbol.dispose]()
1795+
}
1796+
})
1797+
1798+
test('findComponentDeclaration returns undefined for non-existent file', async () => {
1799+
const project = createProject({
1800+
'Card.tsx': ['export function Card() {', ' return <div />;', '}'].join(
1801+
'\n',
1802+
),
1803+
})
1804+
1805+
try {
1806+
const analyzer = createAnalyzer(project.host)
1807+
const result = await analyzer.findComponentDeclaration(
1808+
project.filePath('Missing.tsx'),
1809+
'Card',
1810+
)
1811+
expect(result).toBeUndefined()
1812+
} finally {
1813+
project[Symbol.dispose]()
1814+
}
1815+
})
1816+
1817+
test('findComponentDeclaration returns undefined for unknown component name', async () => {
1818+
const project = createProject({
1819+
'Card.tsx': ['export function Card() {', ' return <div />;', '}'].join(
1820+
'\n',
1821+
),
1822+
})
1823+
1824+
try {
1825+
const analyzer = createAnalyzer(project.host)
1826+
const filePath = project.filePath('Card.tsx')
1827+
1828+
const result = await analyzer.findComponentDeclaration(
1829+
filePath,
1830+
'NonExistent',
1831+
)
1832+
expect(result).toBeUndefined()
1833+
} finally {
1834+
project[Symbol.dispose]()
1835+
}
1836+
})
1837+
1838+
test('findComponentDeclaration returns undefined when signature is unavailable', async () => {
1839+
const project = createProject({
1840+
'Card.tsx': ['export function Card() {', ' return <div />;', '}'].join(
1841+
'\n',
1842+
),
1843+
})
1844+
1845+
try {
1846+
const host: SourceHost = {
1847+
fileExists: project.host.fileExists,
1848+
getSignature: () => undefined,
1849+
readFile: project.host.readFile,
1850+
}
1851+
const analyzer = createAnalyzer(host)
1852+
const result = await analyzer.findComponentDeclaration(
1853+
project.filePath('Card.tsx'),
1854+
'Card',
1855+
)
1856+
expect(result).toBeUndefined()
1857+
} finally {
1858+
project[Symbol.dispose]()
1859+
}
1860+
})
1861+
1862+
test('findComponentDeclaration locates component on first line', async () => {
1863+
const project = createProject({
1864+
'Hero.tsx': ['function Hero() {', ' return <div />;', '}'].join('\n'),
1865+
})
1866+
1867+
try {
1868+
const analyzer = createAnalyzer(project.host)
1869+
const filePath = project.filePath('Hero.tsx')
1870+
1871+
const result = await analyzer.findComponentDeclaration(filePath, 'Hero')
1872+
expect(result).toEqual({ line: 0, character: 9 })
1873+
} finally {
1874+
project[Symbol.dispose]()
1875+
}
1876+
})
1877+
1878+
test('createOpenSignature formats version string', () => {
1879+
expect(createOpenSignature(42)).toBe('open:42')
1880+
expect(createOpenSignature(0)).toBe('open:0')
1881+
})
1882+
16911883
function createAnalyzer(host: SourceHost): ComponentLensAnalyzer {
16921884
const resolver = new ImportResolver(host)
16931885
return new ComponentLensAnalyzer(host, resolver)

0 commit comments

Comments
 (0)