How to replace a path in AST with just parsed javascript(string)?
Asked Answered
P

2

8

https://astexplorer.net/#/gist/70df1bc56b9ee73d19fc949d2ef829ed/7e14217fd8510f0bf83f3372bf08454b7617bce1

I've found now I'm trying to replace an expression and I don't care whats in it.

in this example I've found the this.state.showMenu && this.handleMouseDown portion in

<a
  onMouseDown={this.state.showMenu && this.handleMouseDown}
>

I need to convert to:

<a
  onMouseDown={this.state.showMenu ? this.handleMouseDown : undefined}
>

how can I do so without explicitly reconstructing the tree? I just want to do something like

path.replaceText("this.state.showMenu ? this.handleMouseDown : undefined")
Phares answered 23/4, 2018 at 21:37 Comment(4)
So you want to create the tree, then change the part of tree where this expression is there and then make source back from the updated tree?Triadelphous
@TarunLalwani yes, basically replace a tree.Phares
Which parser library are you using or you are open to any parser library which does the job?Triadelphous
@TarunLalwani i guess whatever can make the link i posted working.Phares
D
3

Here's a transformer that does what you describe:

export default function transformer(file, api) {
  const j = api.jscodeshift;
  const root = j(file.source)

  root
    .find(j.JSXExpressionContainer)
    .replaceWith(path => {
        return j.jsxExpressionContainer(
            j.conditionalExpression(
                j.identifier(j(path.value.expression.left).toSource()),
                j.identifier(j(path.value.expression.right).toSource()),
                j.identifier('undefined')
            )
        )
    })

  return root.toSource()
}

See it in action here.

You can also just put arbitrary text in the JSXExpressionContainer node:

export default function transformer(file, api) {
  const j = api.jscodeshift;
  const root = j(file.source)

  root
    .find(j.JSXExpressionContainer)
    .replaceWith(path => {
        return j.jsxExpressionContainer(
            j.identifier('whatever you want')
        )
    })

  return root.toSource()
}

See this example.

Finally, you don't even need to return a JSXExpressionContainer.

export default function transformer(file, api) {
  const j = api.jscodeshift;
  const root = j(file.source)

  root
    .find(j.JSXExpressionContainer)
    .replaceWith(path => {
        return j.identifier("this isn't valid JS, but works just fine")
    })

  return root.toSource()
}

See the result here.

Doubleacting answered 3/5, 2018 at 19:45 Comment(4)
is there a way to replace with any AST node?Phares
If you're asking whether you can just stick any text in the JSXExpressionContainer, the answer is yes! Updated with an example. If you mean something else please clarify :)Doubleacting
The function you pass to replaceWith can return whatever it wants (doesn't have to be a JSXExpressionContainer). Also updated the answer with an example.Doubleacting
oh i get what you are saying, clever!Phares
R
2

You can do this with our DMS Software Reengineering Toolkit.

DMS treats HTML pages as native HTML text with embedded scripting sublanguage, which might be ECMAScript, or VBScript, or something else. So the process of building a complete HTML "AST" requires that one first build the pure HTML part, then find all the "onXXXXX" tags and convert them to ASTs in the chosen scripting language. DMS can distinguish AST nodes from different langauges so there's no chance of confusion in understanding the compound AST.

So, first we need to parse the HTML document of interest (code edited for pedagogical reasons):

(local (;; [my_HTML_AST AST:Node]
           (includeunique `DMS/Domains/HTML/Component/ParserComponent.par')
        );;
     (= working_graph (AST:CreateForest))
     (= my_HTML_AST (Parser:ParseFile parser working_graph input_file_full_path))

Then we need to walk over the HTML tree, find the JavaScript text fragments, parse them and splice the parsed ECMASCript tree in to replace the text fragment:

(local (;; (includeunique `DMS/Domains/ECMAScript/Components/ParserComponent.par') );;
       (ScanNodes my_HTML_AST
            (lambda (function boolean AST:Node)
                  (ifthenelse (!! (~= (AST:GetNodeType ?) GrammarConstants:Rule:Attribute) ; not an HTML attribute
                                  (~= (Strings:Prefix (AST:GetLiteralString (AST:GetFirstChild ?)) `on')) ; not at action attribute
                               )&&
                      ~t ; scan deeper into tree
                      (value (local (;; [my_ECMAScript_AST AST:Node]
                                        [ECMASCript_text_stream streams:buffer]
                                    );;
                                 (= ECMAScript_text_stream (InputStream:MakeBufferStream (AST:StringLiteral (AST:GetSecondChild ?))=
                                 (= my_ECMAScript_AST (Parser:ParseStream parser working_graph ECMAScript_text_stream))
                                 (= AST:ReplaceNode ? my_ECMAScript_AST)
                                 (= InputStream:Close my_ECMAScript_text_stream)
                         ~f) ; no need to scan deeper here
                  )ifthenelse
            )lambda
       ) ; at this point, we have a mixed HTML/ECMAScript tree
)local

If the scripting language can be something else, then this code has to change. If your pages are all HTML + ECMAScript, you can wrap the above stuff into a black box and call it "(ParseHTML)" which is what the other answer assumed happened.

Now for the actual work. OP want to replace a pattern found in his HTML with another. Here DMS shines because you can write those patterns, using the syntax of the targeted language, directly as a DMS Rewrite Rule (see this link for details).

source domain ECMAScript;
target domain ECMAScript;
rule OP_special_rewrite()=expression -> expression
     "this.state.showMenu && this.handleMouseDown"
  ->  "this.state.showMenu ? this.handleMouseDown : undefined "

Now you need to apply this rewrite:

(RSL:Apply my_HTML_AST `OP_special_rewrite') ; applies this rule to every node in AST
; only those that match get modified

And finally regenerate text from the AST:

 (PrettyPrinter:PrintStream my_ECMAScript_AST input_file_full_path)

OP's example is pretty simply because he is matching against what amounts to a constant pattern. DMS's rules can be written using all kinds of pattern variables; see above link, and can have arbitrary conditions over the matched pattern and other state information to control whether the rule applies.

Roana answered 3/5, 2018 at 22:33 Comment(7)
wha.. jaw dropped. Anyways thanks for your insights, your argument generally make sense and I agree there are probably other tools are are able to achieve what I'm looking for. However I'm looking for a way to do this via Jscodeshift, a way to just replace AST Node. Should not take this much complexity.Phares
"This much complexity..." well, you can't avoid parsing, you can't avoid searching for the desired places to replace, you can't avoid building and inserting a replacement tree... DMS is the "simplest" mechanism that can do all these things for arbitrary computer languages. You might find an off-the-shelf tool that does it for your specific case, but my guess is such tools will be really rare, because it is a lot of mechanism for someone to implement.Roana
my goal is to reuse Jscodeshift, seems like you are explaining the algorithm.Phares
Explaining, well, yeah... any tool that does this kind of has to do do the parsing as I've described. Perhaps more important is "doing it with pattern-directed rewrites" which I'm fairly sure Jscodeshift doesn't have. And as your ambititions to make more complicated changes grow, you'll begin to appreciate the power of using such rewrites rather than manually coding the replacement as @Doubleacting has done.Roana
Just want to clarify for OP's sake that jscodeshift can do what is described in this answer, and OP doesn't need another tool to solve this problem (even if they want to replace nodes with arbitrary text).Doubleacting
@bgran: I have no doubt it can achieve the same functionality. I'm not so convinced that jscodeshift allows you to use the surface syntax of the targeted language. I don't think people appreciate the convenience of this, and your answer, while motivated by the abstract syntax of ECMAScript, is just that and doesn't show directly-stated patterns. I added this answer because OP said he was willing to look at any tool.Roana
@IraBaxter I truly appreciate a way to reapply what i've learnt in codeshift general purpose wise to other language / domain. But as bgran said, I was looking to use jscodeshift to accomplish this task, which was specifically designed to refactor javascript. But thank you for your input.Phares

© 2022 - 2024 — McMap. All rights reserved.