ts-morph icon indicating copy to clipboard operation
ts-morph copied to clipboard

Support automatically determining if a string literal should be written for a property key or value in Writers.object

Open whalemare opened this issue 4 years ago • 6 comments

Describe the bug

Version: 10.1.0

I want to generate .ts file with variable content, that depend on my existedObject Writers generate incorrect object representation

To Reproduce

import { Project, VariableDeclarationKind, Writers } from "ts-morph";

const project = new Project();
const sourceFile = project.createSourceFile("test.ts", ``);

const existedObject = {
      id: "some-id",
      "quote-key": 2
}

sourceFile.addVariableStatement({
  isExported: true,
  declarationKind: VariableDeclarationKind.Const,
  declarations: [{
    name: 'flavor',
    initializer: Writers.object(existedObject),
  }]
})

console.log(sourceFile.getFullText())

Output:

export const flavor = { 
        id: some-id, 
        quote-key: 2 
    }; 

Expected behavior

export const flavor = { 
        id: "some-id", 
        "quote-key": 2 
    }; 

whalemare avatar May 18 '21 08:05 whalemare

@whalemare I wouldn't consider this totally broken.

Right now, you need to explicitly write them as string literals. For example:

const existedObject = {
      id: `"some-id"`,
      '"quote-key"': 2
}

That said, I believe that the code could automatically determine that these should be string literals without having to do some complex parsing. I've renamed the title for the possible action item here.

dsherret avatar May 18 '21 14:05 dsherret

I write some hacky code that close my needs, maybe it can help somebody.

P.s. for some reasons, I need modify parts with value for specific keys, so this code contains Mappers declaration that help to achieve this

import Writer from 'code-block-writer'
import { match } from 'ts-pattern'

type Wrappers = { [key in string]: { start: string; end: string } }

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export function writeAny(writer: Writer, raw: any, wrappers: Wrappers = {}, needComma = false) {
  match(typeof raw)
    .with('object', () => writeObject(writer, raw, wrappers, true))
    .with('string', () => writer.quote(raw))
    .with('number', () => {
      writer.write(`${raw}`)
    })
    .with('bigint', () => {
      writer.write(`${raw}`)
    })
    .with('boolean', () => {
      writer.write(raw ? 'true' : 'false')
    })
    .with('symbol', () => {
      writer.quote(`${raw}`)
    })
    .otherwise(() => {
      if (raw === null) {
        writer.write('null')
      } else if (raw === undefined) {
        writer.write('undefined')
      }
    })
  if (needComma) {
    writer.write(',')
  }
}

// eslint-disable-next-line @typescript-eslint/ban-types
export const writeObject = (writer: Writer, raw: object, wrappers: Wrappers = {}, needComma: boolean) => {
  if (Array.isArray(raw)) {
    writer.write(JSON.stringify(raw))
  } else {
    writer.write('{')
    Object.keys(raw).forEach((key) => {
      const spec = new RegExp('[^A-Za-z0-9]')
      const hasSpecSymbols = spec.test(key)
      writer.newLineIfLastNot()
      if (hasSpecSymbols) {
        writer.quote(key)
      } else {
        writer.write(key)
      }
      writer.write(`: ${wrappers[key]?.start ? wrappers[key].start : ''}`)
      // eslint-disable-next-line @typescript-eslint/ban-ts-comment
      // @ts-ignore
      writeAny(writer, raw[key], wrappers, needComma)

      if (wrappers[key]?.end) {
        writer.write(wrappers[key].end)
      }
    })
    writer.write('}')
  }
}

Usage:

sourceFile.addVariableStatement({
    isExported: true,
    declarationKind: VariableDeclarationKind.Const,
    declarations: [
      {
        name: 'flavor',
        initializer: (writer) => {
          return writeAny(writer, someObject, {
          // this will patch values with key 'textStyle' by inserting in start _StyleSheet.create(_ and in the end _),_)
            textStyle: {
              start: 'StyleSheet.create(',
              end: '),',
            },
          })
        },
      },
    ],
  })

whalemare avatar May 20 '21 03:05 whalemare

Hello @dsherret ,

I'm facing similar issue with wrong generated property names. Currently I'm using ts-morph to generate runtime client from WSDL (SOAP) and I have to make sure that generated propnames are same as in WSDL file. But some names in WSDL contains chars like -,. which are not correct property name characters... check this issue https://github.com/dderevjanik/wsdl-tsclient/issues/18

Temporally I created function which detects if propname contains weird characters, if so, it'll wrap it to double quotes.

const incorrectPropNameChars = [" ", "-", "."];
function sanitizePropName(propName: string) {
    if (incorrectPropNameChars.some(char => propName.includes(char))) {
        return `"${propName}"`;
    }
    return propName;
}

function createProperty(
    name: string,
    type: string,
    doc: string,
    isArray: boolean,
    optional = true
): PropertySignatureStructure {
    return {
        kind: StructureKind.PropertySignature,
        name: sanitizePropName(name),
        docs: [doc],
        hasQuestionToken: true,
        type: isArray ? `Array<${type}>` : type,
    };
}

Yeah, the solution isn't the best, but it will work most time. Do you have any plans to implement it right into ts-morph ?

dderevjanik avatar Jun 28 '21 17:06 dderevjanik

Yeah, I ran into the same issue, and I was hoping to be able to at least use a writer function to properly quote the value, but PropertyNamedNodeStructure is only strings, not string | WriterFunction. I'll be submitting a PR momentarialy to allow name on PropertyNamedNodeStructure and PropertyNameableNodeStructure to be writers, so the following would work:

export function propertyKey(value: string): string | WriterFunction {
  if (/^\w+$/.test(value)) {
    return value;
  }
  // return JSON.stringify(value); // my current workaround
  return (writer) => writer.quote(value);
}

(and use propertyKey like sanitizePropName above)

forivall avatar May 12 '23 03:05 forivall

Hmm, I found the following for enums - should this be applied to all properties? or allow explicit writers? or both?

https://github.com/dsherret/ts-morph/blob/cea07aa7759ecf5a1e9f90b628334b8bd617c624/packages/ts-morph/src/structurePrinters/enum/EnumMemberStructurePrinter.ts#L29-L34

as, with both this would change to

    // Adds quotes if structure is not a valid variable name
    // AND the string is not enclosed in quotation marks
    if (structure.name instanceof Function)
      structure.name(writer)
    else if (isValidVariableName(structure.name) || StringUtils.isQuoted(structure.name))
      writer.write(structure.name);
    else
      writer.quote(structure.name);

forivall avatar May 12 '23 03:05 forivall

Re-opened by https://github.com/dsherret/ts-morph/commit/055bf2158539d0b32c71141ca7c5e5213b85a611

I completely forgot about computed property names and for some reason there wasn't a test for that.

dsherret avatar Sep 15 '23 14:09 dsherret