Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
whalemare opened this issue May 18, 2021 · 6 comments · Fixed by #1451 · May be fixed by #1412

Comments

@whalemare
Copy link

whalemare commented May 18, 2021

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 
    }; 
@dsherret dsherret changed the title Writers.object is totally broken Support automatically determining if a string literal should be written for a property key or value in Writers.object May 18, 2021
@dsherret
Copy link
Owner

@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.

@whalemare
Copy link
Author

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: '),',
            },
          })
        },
      },
    ],
  })

@dderevjanik
Copy link

dderevjanik commented Jun 28, 2021

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 dderevjanik/wsdl-tsclient#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 ?

@forivall
Copy link

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
Copy link

forivall commented May 12, 2023

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

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

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);

@dsherret
Copy link
Owner

Re-opened by 055bf21

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

@dsherret dsherret reopened this Sep 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
4 participants