Skip to content

add refactoring: string concatenation to template literals #30565

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

Merged
merged 46 commits into from
Jan 3, 2020

Conversation

bigaru
Copy link
Contributor

@bigaru bigaru commented Mar 24, 2019

Fixes #18267

@ajafff have a look

Copy link
Member

@gabritto gabritto left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems good except for some minor things.
Another thing I've noticed is that this refactor has the same problem as other ones regarding comments that disappear. For example, in the code above both "comment b" occurrences go missing after refactoring:

const a = 2;
const comment1 = `test ${ /*comment b*/ a /*comment e*/ }!`;
const comment2 = "test " + /* comment b*/ a /*comment e*/ + "!";

becomes:

const a = 2;
const comment1 = "test " + a /*comment e*/ + "!";
const comment2 = `test ${a /*comment e*/}!`;

I've had the same problem in this refactor so I had to explicitly handle the copying of comments from old nodes to new ones. I'm writing about that problem and solution and will link to it here when I finish.
Update: wrote about comments here: https://gist.github.com/gabritto/fa489e48e09ce6bc089955abba78d91a#trivia


function getNodeOrParentOfParentheses(file: SourceFile, startPosition: number) {
const node = getTokenAtPosition(file, startPosition);
if (isParenthesizedExpression(node.parent) && isBinaryExpression(node.parent.parent)) return node.parent.parent;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you explain why this is necessary?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the idea behind is that
even if your cursor is inside a parentheses, you can still invoke this refactoring (as long as it's not a string binary expression)

both examples were invoked inside the parentheses:

// before
const foo = "foobar is " + (42 + 6) + " years old"
const fooStr = "foobar is " + (42 + 6 + "years") + " old"

// after
const foo = `foobar is ${42 + 6} years old`
const fooStr = "foobar is " + (`${42 + 6}years`) + " old"

It is not necessary but nice to have

let text = "";

while (index < nodes.length && isStringLiteral(nodes[index])) {
text = text + decodeRawString(nodes[index].getText());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you could cast nodes[index] to StringLiteral and use nodes[index].text instead of using getText. then decodeRawString becomes unnecessary i think.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I first used as StringLiteral.
but last time @ajafff pointed out that Octal escapes are disallowed inside template literal

If I use (nodes[index] as StringLiteral).text then happens that:

// before
const foo = "Unicode \u0023 \u{0023} " + "Hex \x23 " + "Octal \43";

// after
const foo = `Unicode # # Hex # Octal 43`;

As you can see the octal escape is not supported with .text That's why I created a suboptimal function decodeRawString. It seems that the parser has a bug.

What do you think?
Should I switch back to: (nodes[index] as StringLiteral).text ?
and provide a fix for the bug (as a separate PR)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I switched back to (nodes[index] as StringLiteral).text

@RyanCavanaugh
Is there a reason why octal escape sequence is not interpreted from scanner (even when strict mode is off)

// before
const foo = "Unicode \u0023 \u{0023} " + "Hex \x23 " + "Octal \43";

// after
const foo = `Unicode # # Hex # Octal 43`;


function escapeText(content: string) {
return content.replace("`", "\`") // back-tick
.replace("${", "$\\{"); // placeholder alike beginning
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think there's an extra backslash here. maybe it should be .replace("${", "$\{")?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately if I pass as $\{, the backslash will not be preserved.
because the parser optimize this \{ to {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i tested this locally and this is what happens:
image
so it does seem like there's an extra backslash on the resulting template literal.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that "$\\{" changes the external behavior

to explain my dilemma

// before 
const escape = "with ${dollar}"

// after using .replace("${", "$\{")
const escape = `with ${dollar}`

// after using .replace("${", "\${")
const escape = `with ${dollar}`

// after using .replace("${", "$\\{")
const escape = `with $\\{dollar}`

// after using .replace("${", "\\${")
const escape = `with \\${dollar}`

using $\{ or \${

  • the backslash completely disappears after refactoring
  • and therefore, it leaves the template literal in an invalid state

using $\\{ or \\${

  • there is one backslash too much
  • however, the template literal is still in valid state

Both approaches change the external behavior.
As it seems, neither $ nor { can be escaped with single backslash.
However, the backtick ` can be escaped with just a single backslash.

What do you think?
How should I deal with this matter?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the external behavior cannot be preserved with one or two backslash, I've decided to remove escaping of opening placeholder. .replace("${", "$\\{")

@RyanCavanaugh
Copy link
Member

My inclination here is that concat -> template is a good refactor, but it's not clear to me why you'd want the reverse direction.

@DanielRosenwasser thoughts?

@DanielRosenwasser
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 21, 2019

Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at 35a3a5f. You can monitor the build here. It should now contribute to this PR's status checks.

@DanielRosenwasser
Copy link
Member

Okay now @typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 23, 2019

Heya @DanielRosenwasser, I've started to run the tarball bundle task on this PR at 88795e2. You can monitor the build here. It should now contribute to this PR's status checks.

@typescript-bot
Copy link
Collaborator

typescript-bot commented Dec 23, 2019

Hey @DanielRosenwasser, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/58560/artifacts?artifactName=tgz&fileId=E4ED7ECEC258847C2940852925C4BCCCCA6AC9A99B44880962350EAC6E997F6602&fileName=/typescript-3.8.0-insiders.20191223.tgz"
    }
}

and then running npm install.


There is also a playground for this build.

@DanielRosenwasser
Copy link
Member

DanielRosenwasser commented Jan 3, 2020

Hey @bigaru, I spoke with Ryan and at this time we think convert to template expression is clearly useful from users. On the other hand, we didn't have consensus that users wanted to go the other way. I've removed those bits and pushed up a commit to remove that functionality. In the future, we'll be able to revert ad0f006 and ff4fa1f if we change our minds.

@DanielRosenwasser DanielRosenwasser merged commit 024b8c1 into microsoft:master Jan 3, 2020
@rauschma
Copy link

Nit-pick: At the moment, this refactoring seems to be called “Convert to template string”. The more correct name would be “Convert to template literal”.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Add refactoring option to convert string concatenation to template literals
7 participants