From 54bbe71838b6e3e857b943d9ce5bc95d809dc148 Mon Sep 17 00:00:00 2001 From: Travis Arnold Date: Wed, 10 Jul 2024 01:32:24 -0700 Subject: [PATCH] mark const object properties as readonly --- .../src/types/getTypeDocumentation.test.ts | 6 ++--- .../utils/src/types/getTypeDocumentation.ts | 20 +++++++++++--- packages/utils/src/types/processType.test.ts | 19 ++++++------- packages/utils/src/types/processType.ts | 27 +++++++++++++++++-- 4 files changed, 55 insertions(+), 17 deletions(-) diff --git a/packages/utils/src/types/getTypeDocumentation.test.ts b/packages/utils/src/types/getTypeDocumentation.test.ts index 5e05e9f..99b7886 100644 --- a/packages/utils/src/types/getTypeDocumentation.test.ts +++ b/packages/utils/src/types/getTypeDocumentation.test.ts @@ -2110,7 +2110,7 @@ describe('getTypeDocumentation', () => { { "defaultValue": undefined, "isOptional": false, - "isReadonly": false, + "isReadonly": true, "kind": "String", "name": "primary", "type": ""#ff0000"", @@ -2118,7 +2118,7 @@ describe('getTypeDocumentation', () => { { "defaultValue": undefined, "isOptional": false, - "isReadonly": false, + "isReadonly": true, "kind": "String", "name": "secondary", "type": ""#00ff00"", @@ -2126,7 +2126,7 @@ describe('getTypeDocumentation', () => { { "defaultValue": undefined, "isOptional": false, - "isReadonly": false, + "isReadonly": true, "kind": "String", "name": "tertiary", "type": ""#0000ff"", diff --git a/packages/utils/src/types/getTypeDocumentation.ts b/packages/utils/src/types/getTypeDocumentation.ts index 854ea56..8f76f93 100644 --- a/packages/utils/src/types/getTypeDocumentation.ts +++ b/packages/utils/src/types/getTypeDocumentation.ts @@ -6,7 +6,12 @@ import type { ClassDeclaration, VariableDeclaration, } from 'ts-morph' -import { Node, TypeFormatFlags } from 'ts-morph' +import { + Node, + SyntaxKind, + TypeFormatFlags, + VariableDeclarationKind, +} from 'ts-morph' import { getJsDocMetadata } from '../js-docs' import { @@ -132,6 +137,12 @@ export function getTypeDocumentation( } if (Node.isVariableDeclaration(declaration)) { + const variableStatement = declaration.getFirstAncestorByKind( + SyntaxKind.VariableStatement + ) + const isConst = variableStatement + ? variableStatement.getDeclarationKind() === VariableDeclarationKind.Const + : false const initializer = declaration.getInitializer() if ( @@ -143,7 +154,8 @@ export function getTypeDocumentation( const processedType = processType( initializer.getType(), declaration, - filter + filter, + isConst ) if (!processedType) { @@ -164,10 +176,12 @@ export function getTypeDocumentation( } if (Node.isAsExpression(initializer)) { + const typeNode = initializer.getTypeNode() const processedType = processType( initializer.getType(), declaration, - filter + filter, + typeNode?.getText() === 'const' ) if (processedType) { diff --git a/packages/utils/src/types/processType.test.ts b/packages/utils/src/types/processType.test.ts index b65453a..3153ec2 100644 --- a/packages/utils/src/types/processType.test.ts +++ b/packages/utils/src/types/processType.test.ts @@ -719,26 +719,27 @@ describe('processProperties', () => { { overwrite: true } ) const variableDeclaration = sourceFile.getVariableDeclarationOrThrow('a') - const processedProperties = processType(variableDeclaration.getType()) - - // TODO: const properties should be marked as readonly + const processedProperties = processType( + variableDeclaration.getType(), + variableDeclaration + ) expect(processedProperties).toMatchInlineSnapshot(` { "kind": "Object", - "name": undefined, + "name": "a", "properties": [ { "defaultValue": undefined, "isOptional": false, - "isReadonly": false, + "isReadonly": true, "kind": "Object", "name": "e", "properties": [ { "defaultValue": undefined, "isOptional": false, - "isReadonly": false, + "isReadonly": true, "kind": "Number", "name": "f", "type": "number", @@ -749,7 +750,7 @@ describe('processProperties', () => { { "defaultValue": undefined, "isOptional": false, - "isReadonly": false, + "isReadonly": true, "kind": "String", "name": "g", "type": "string", @@ -757,7 +758,7 @@ describe('processProperties', () => { { "defaultValue": undefined, "isOptional": false, - "isReadonly": false, + "isReadonly": true, "kind": "Number", "name": "b", "type": "1", @@ -765,7 +766,7 @@ describe('processProperties', () => { { "defaultValue": undefined, "isOptional": false, - "isReadonly": false, + "isReadonly": true, "kind": "String", "name": "c", "type": ""string"", diff --git a/packages/utils/src/types/processType.ts b/packages/utils/src/types/processType.ts index ee267ef..19a1351 100644 --- a/packages/utils/src/types/processType.ts +++ b/packages/utils/src/types/processType.ts @@ -3,6 +3,7 @@ import { Type, TypeFormatFlags, SyntaxKind, + VariableDeclarationKind, type ClassDeclaration, type FunctionDeclaration, type ParameterDeclaration, @@ -235,6 +236,7 @@ export function processType( type: Type, enclosingNode?: Node, filter: SymbolFilter = defaultFilter, + isConst: boolean = false, references: Set = new Set(), isRootType: boolean = true, defaultValues?: Record | unknown @@ -247,6 +249,15 @@ export function processType( symbol = apparentType.getAliasSymbol() || apparentType.getSymbol() } + if (isConst === false && Node.isVariableDeclaration(enclosingNode)) { + const variableStatement = enclosingNode.getFirstAncestorByKind( + SyntaxKind.VariableStatement + ) + isConst = variableStatement + ? variableStatement.getDeclarationKind() === VariableDeclarationKind.Const + : false + } + const symbolMetadata = getSymbolMetadata(symbol, enclosingNode) const symbolDeclaration = symbol?.getDeclarations().at(0) const declaration = symbolDeclaration || enclosingNode @@ -282,7 +293,7 @@ export function processType( if (isUtilityType) { const processedTypeArguments = aliasTypeArguments .map((type) => - processType(type, declaration, filter, references, false) + processType(type, declaration, filter, isConst, references, false) ) .filter(Boolean) as ProcessedType[] @@ -379,6 +390,7 @@ export function processType( elementType, declaration, filter, + isConst, references, false ) @@ -425,6 +437,7 @@ export function processType( unionType, declaration, filter, + isConst, references, false, defaultValues @@ -464,6 +477,7 @@ export function processType( intersectionType, declaration, filter, + isConst, references, false, defaultValues @@ -508,6 +522,7 @@ export function processType( type, declaration, filter, + isConst, references, false ) @@ -573,6 +588,7 @@ export function processType( type, declaration, filter, + isConst, references, false, defaultValues @@ -585,6 +601,7 @@ export function processType( type, declaration, filter, + isConst, references, false, defaultValues @@ -621,6 +638,7 @@ export function processType( apparentType, declaration, filter, + isConst, references, false, defaultValues @@ -686,6 +704,7 @@ export function processSignature( parameter.getTypeAtLocation(signatureDeclaration), enclosingNode, filter, + false, references, isRootType, defaultValue @@ -762,6 +781,7 @@ export function processTypeProperties( type: Type, enclosingNode?: Node, filter: SymbolFilter = defaultFilter, + isConst: boolean = false, references: Set = new Set(), isRootType: boolean = true, defaultValues?: Record | unknown @@ -797,6 +817,7 @@ export function processTypeProperties( propertyType, declaration, filter, + isConst, references, isRootType, defaultValue @@ -818,7 +839,7 @@ export function processTypeProperties( name, defaultValue, isOptional, - isReadonly, + isReadonly: isConst || isReadonly, } satisfies PropertyTypes } } else { @@ -835,6 +856,7 @@ function processTypeTupleElements( type: Type, enclosingNode?: Node, filter?: SymbolFilter, + isConst: boolean = false, references: Set = new Set(), isRootType: boolean = true ) { @@ -853,6 +875,7 @@ function processTypeTupleElements( tupleElementType, enclosingNode, filter, + isConst, references, isRootType )