Skip to content

Commit

Permalink
feat: un-aliased object types (#30)
Browse files Browse the repository at this point in the history
* unaliased support for objects

* index type and naming

* flip conditional

---------

Co-authored-by: Myles Murphy <[email protected]>
  • Loading branch information
mylesmmurphy and Myles Murphy authored Aug 11, 2024
1 parent 7419a0f commit 80bdfcf
Show file tree
Hide file tree
Showing 3 changed files with 86 additions and 27 deletions.
1 change: 0 additions & 1 deletion packages/typescript-plugin/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ function init (modules: { typescript: typeof ts }): ts.server.PluginModule {
const ts = modules.typescript

function create (info: ts.server.PluginCreateInfo): ts.LanguageService {
// Log a message
info.project.projectService.logger.info('Prettify LSP is starting')

// Set up decorator object
Expand Down
87 changes: 70 additions & 17 deletions packages/typescript-plugin/src/type-tree/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,9 +19,6 @@ let options: PrettifyOptions = {
unwrapPromises: true
}

// Tracks the properties processed so far
let propertiesCount = 0

/**
* Get TypeInfo at a position in a source file
*/
Expand All @@ -36,7 +33,6 @@ export function getTypeInfoAtPosition (
typescript = typescriptContext
checker = typeChecker
options = prettifyOptions
propertiesCount = 0

const node = getDescendantAtRange(typescript, sourceFile, [position, position])
if (!node || node === sourceFile || !node.parent) return undefined
Expand Down Expand Up @@ -76,7 +72,7 @@ function getTypeTree (type: ts.Type, depth: number, visited: Set<ts.Type>): Type
const typeName = checker.typeToString(type, undefined, typescript.TypeFormatFlags.NoTruncation)
const apparentType = checker.getApparentType(type)

if (depth >= options.maxDepth || isPrimitiveType(type) || options.skippedTypeNames.includes(typeName)) return {
if (isPrimitiveType(type) || options.skippedTypeNames.includes(typeName)) return {
kind: 'basic',
typeName
}
Expand Down Expand Up @@ -166,24 +162,59 @@ function getTypeTree (type: ts.Type, depth: number, visited: Set<ts.Type>): Type
}

if (apparentType.isClassOrInterface() || (apparentType.flags & typescript.TypeFlags.Object)) {
if (propertiesCount >= options.maxProperties) return { kind: 'basic', typeName }

// Resolve how many properties to show based on the maxProperties option
const remainingProperties = options.maxProperties - propertiesCount
const depthMaxProps = depth >= 1 ? options.maxSubProperties : options.maxProperties
const allowedPropertiesCount = Math.min(depthMaxProps, remainingProperties)

let typeProperties = apparentType.getProperties()
if (options.hidePrivateProperties) {
typeProperties = typeProperties.filter((symbol) => isPublicProperty(symbol))
}

const publicProperties = typeProperties.slice(0, allowedPropertiesCount)
const excessProperties = Math.max(0, typeProperties.length - depthMaxProps)
typeProperties = typeProperties.slice(0, depthMaxProps)

const stringIndexType = type.getStringIndexType()
const stringIndexIdentifierName = getIndexIdentifierName(type, 'string')

const numberIndexType = type.getNumberIndexType()
const numberIndexIdentifierName = getIndexIdentifierName(type, 'number')

if (depth >= options.maxDepth) {
// If we've reached the max depth and has a type alias, return it as a basic type
// Otherwise, return an object with the properties count
// If it has index signatures, add it to the properties as "..."
if (!typeName.includes('{')) return {
kind: 'basic',
typeName
}

const indexProperties: TypeProperty[] = []

if (stringIndexType) {
indexProperties.push({
name: `[${stringIndexIdentifierName}: string]`,
readonly: isReadOnly(stringIndexType.symbol),
type: { kind: 'basic', typeName: '...' }
})
}

propertiesCount += publicProperties.length
const excessProperties = Math.max(typeProperties.length - publicProperties.length, 0)
if (numberIndexType) {
indexProperties.push({
name: `[${numberIndexIdentifierName}: number]`,
readonly: isReadOnly(numberIndexType.symbol),
type: { kind: 'basic', typeName: '...' }
})
}

const properties: TypeProperty[] = publicProperties.map(symbol => {
return {
kind: 'object',
typeName,
properties: indexProperties,
excessProperties: typeProperties.length // Return all properties as excess to avoid deeper nesting
}
}

const properties: TypeProperty[] = typeProperties.map(symbol => {
const symbolType = checker.getTypeOfSymbol(symbol)
return {
name: symbol.getName(),
Expand All @@ -192,19 +223,17 @@ function getTypeTree (type: ts.Type, depth: number, visited: Set<ts.Type>): Type
}
})

const stringIndexType = type.getStringIndexType()
if (stringIndexType) {
properties.push({
name: '[key: string]',
name: `[${stringIndexIdentifierName}: string]`,
readonly: isReadOnly(stringIndexType.symbol),
type: getTypeTree(stringIndexType, depth + 1, new Set(visited))
})
}

const numberIndexType = type.getNumberIndexType()
if (numberIndexType) {
properties.push({
name: '[key: number]',
name: `[${numberIndexIdentifierName}: number]`,
readonly: isReadOnly(numberIndexType.symbol),
type: getTypeTree(numberIndexType, depth + 1, new Set(visited))
})
Expand Down Expand Up @@ -339,3 +368,27 @@ function isReadOnly (symbol: ts.Symbol | undefined): boolean {
) &&
declaration.modifiers?.some(modifier => modifier.kind === typescript.SyntaxKind.ReadonlyKeyword)))
}

function hasMembers (declaration: ts.Declaration): declaration is ts.InterfaceDeclaration | ts.ClassDeclaration | ts.TypeLiteralNode {
return typescript.isInterfaceDeclaration(declaration) || typescript.isClassDeclaration(declaration) || typescript.isTypeLiteralNode(declaration)
}

function getIndexIdentifierName (type: ts.Type | undefined, signature: 'string' | 'number'): string {
const declarations = type?.getSymbol()?.getDeclarations()?.filter(hasMembers) ?? []
const members = declarations.flatMap(declaration => declaration.members as ts.NodeArray<ts.Node>)
if (!members.length) return 'key'

const indexSignatures = members.filter(typescript.isIndexSignatureDeclaration)

for (const indexSignature of indexSignatures) {
const parameter = indexSignature.parameters[0]
if (!parameter) continue

const signatureKind = parameter.getChildren()?.[2]?.getText()
if (signatureKind !== signature) continue

return parameter?.name?.getText() ?? 'key'
}

return 'key'
}
25 changes: 16 additions & 9 deletions packages/vscode-extension/src/stringify-type-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ export function stringifyTypeTree (typeTree: TypeTree, anonymousFunction = true)

if (typeTree.kind === 'intersection') {
const nonObjectTypeStrings = typeTree.types.filter(t => t.kind !== 'object').map(t => stringifyTypeTree(t))

const objectTypes = typeTree.types.filter((t): t is Extract<TypeTree, { kind: 'object' }> => t.kind === 'object')

if (objectTypes.length === 0) {
return nonObjectTypeStrings.join(' & ')
}

const objectTypesProperties = objectTypes.flatMap(t => t.properties)
const objectTypeExcessProperties = objectTypes.reduce((acc, t) => acc + t.excessProperties, 0)
const mergedObjectTypeString = stringifyTypeTree({
Expand Down Expand Up @@ -202,18 +206,21 @@ export function prettyPrintTypeString (typeStringInput: string, indentation = 2)
}
}

// Remove empty braces newlines and empty newlines
result = result
.replace(/{\s*\n*\s*}/g, '{}')
.replace(/^\s*[\r\n]/gm, '')
.replace(/{\s*\n*\s*}/g, '{}') // Remove empty braces newlines
.replace(/^\s*[\r\n]/gm, '') // Remove empty newlines
.replace(/{\s*\.\.\.\s*([0-9]+)\s*more\s*}/g, '{ ... $1 more }') // Replace only excess properties into one line

return result
}

/**
* Sanitizes a string by removing leading words, whitespace, newlines, and semicolons
*/
export function sanitizeString (str: string): string {
// Remove the leading word, ex: type, const, interface
str = str.replace(/^[a-z]+\s/, '')

// Remove all whitespace, newlines, and semicolons
return str.replace(/\s/g, '').replace(/\n/g, '').replace(/;/g, '')
return str
.replace(/^[a-z]+\s/, '') // Remove the leading word, ex: type, const, interface
.replace(/\s/g, '') // Remove all whitespace
.replace(/\n/g, '') // Remove all newlines
.replace(/;/g, '') // Remove all semicolons
}

0 comments on commit 80bdfcf

Please sign in to comment.