Convert to js#291
Conversation
|
@adams85 now you can review |
|
maybe now I could also be based on AstVisitor, but I don't know atm... I've only rebased what I've programed long time ago |
|
I took a quick glance, so just some random thoughts before going into the details:
Ok, I think this is enough for starters. 😄 |
On second thoughts, we can't really keep the visitor internal as extensions (like JSX) should be supported too. So I think our best bet is to copy the pattern of
If we did this, |
| protected StringBuilder _sb = new StringBuilder(); | ||
| private int _indentionLevel = 0; | ||
|
|
||
| private readonly List<Node> _parentStack = new List<Node>(); |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| public class ToJavascriptConverter |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| public static class ToJavascriptConverterExtension | ||
| { | ||
| public static string ToJavascript(this Node node, bool beautify = false) | ||
| { | ||
| return ToJavascriptConverter.ToJavascript(node, beautify); | ||
| } | ||
| } |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| VisitNodeList(program.Body, appendAtEnd: ";", addLineBreaks: true); | ||
| } | ||
|
|
||
| protected virtual void VisitUnknownNode(Node node) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| return _parentStack[_parentStack.Count - 1 - offset]; | ||
| } | ||
|
|
||
| public virtual void Visit(Node node) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| protected virtual void VisitClassDeclaration(ClassDeclaration classDeclaration) |
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
| Visit(function.Body); | ||
| } | ||
|
|
||
| protected virtual void VisitClassExpression(ClassExpression classExpression) |
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
| { | ||
| Visit(breakStatement.Label); | ||
| } | ||
| Append("break"); |
This comment was marked as duplicate.
This comment was marked as duplicate.
Sorry, something went wrong.
| return _sb.ToString(); | ||
| } | ||
|
|
||
| public bool IsAsync(Node node) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
| } | ||
| } | ||
|
|
||
| protected virtual void VisitImport(Import import) |
This comment was marked as resolved.
This comment was marked as resolved.
Sorry, something went wrong.
|
Thanks @adams85 for going through this with such a detail! Just that we are on same page how to proceed as I know @jogibear9988 might have his hands full with esprima-next too. Some alternatives at least:
It's a great feature so I'd hope we can agree on some plan to make sure we get it into main, whether "perfect" or incrementally improving. |
|
Thanks for the great review. For me one critical point is, the visitor should be public, and the methods virtual, cause i wanted to overwrite the logic wich some ast nodes are written as javascript, cause we use TemplateLiteralStrings in our component framework, with html and css code, wich I also like to parse and minify. I think if someone works on it, I'll be fine, if not, I'll look when I've time and will do. |
|
Since I've already got into this and I happen to have some free time now, I could as well make an attempt on finishing the feature. If it's okay with you, I'd just need write permissions to the PR.
You mean you want to subclass it in another assembly? If so, maybe we can handle that using an |
As I'm just "a contributor with some merge capabilities", I'm afraid I can't give the permissions. But if @jogibear9988 is OK with it, maybe you can branch from this PR into another PR adding fixes on top. I have no idea how the attribution of changes will flow after that 😬 |
don't know how to do this. I've added you to my fork, will this help?
Don't like this approach. Why should esprima-net have an internals-visible-to my assembly? Why not make it public? Think the best way for people to modify the JS output of some parts would be to override the visitor for the node they wanted to change. |
For me this would be full okay. You also can close, create a new one... as you like. But I also added him to my fork |
Maybe it's because I'm not added as a maintainer here? |
|
@adams85 I've added you to my fork, but you need to accept the invite |
|
I could not add you here, I'm also not a maintainer here. |
|
Ok, it works now. Thanks! |
Generally, it's a good idea to minimize the API surface area so we can make modification more freely without breaking consumer code. However, you have a good point here, so the visitor will remain public then. BTW, what about this issue? Can |
|
if you look here: https://web.archive.org/web/20210314002546/https://developer.mozilla.org/en-US/docs/Mozilla/Projects/SpiderMonkey/Parser_API name of identifier is string and not nullable |
|
Thanks for the clarification. I'll fix this too. |
|
Wow @jogibear9988, what a beautiful piece of work. I've found myself having to use js-beautify/prettifier in my processing pipeline. Both introduce significant performance issues and are not super reliable. This will be epic. I'll try to take a spin on your branch over the weekend and see if it works as-is on my workloads. Fingers crossed! |
|
@CharlieEriksen It's a nice work indeed but please not that this is still a WIP. There are known cases where the AST is transformed incorrectly (e.g. operator precedence handling is pretty unreliable currently). The public API is going to change too. Hopefully, in a few days we'll have a maturer version. |
|
This was a harder nut to crack than it looked at first sight (or even at second) but finally I proudly present the improved AST to JS conversion! 🎉 (Truth be told, there was so many changes, fixes and improvements that I virtually ended up with a complete rewrite...) @jogibear9988 Please re-review it, especially the public API, which is mainly defined in the
I also adjusted your existing test cases and added another set of tests. The complete test suite used for testing the parser was reused: the fixture files get parsed, converted back to text, then re-parsed, finally the two resulting ASTs are compared (only structure and node types for now). So we can say that the feature is pretty comprehensively tested, however getting parentheses rules right is hard af so I'm still not 100% sure that every corner case and quirk is covered. But I think it should be ok for an initial version. @CharlieEriksen Now you may give it a go, the foundations are there for both minimizing and prettifying. |
lahma
left a comment
There was a problem hiding this comment.
Wow, you really went the extra mile with this implementation, seems like powerful stuff. I left some comments, mostly about code style as I don't have much other to give in the JS writing area.
|
|
||
| partial class AstToJavascriptConverter | ||
| { | ||
| #region Statements |
There was a problem hiding this comment.
nit: I don't think we use regions
| protected TokenFlags LastTokenFlags { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; } | ||
| protected bool WhiteSpaceWrittenSinceLastToken { [MethodImpl(MethodImplOptions.AggressiveInlining)] get; [MethodImpl(MethodImplOptions.AggressiveInlining)] private set; } | ||
|
|
||
| #region White-space |
There was a problem hiding this comment.
nit: regions are not preferred in codebase
|
|
||
| public JavascriptTextWriter(TextWriter writer, Options options) | ||
| { | ||
| _writer = writer ?? throw new ArgumentNullException(nameof(writer)); |
There was a problem hiding this comment.
should prefer EsprimaExceptionHelper for consistency
There was a problem hiding this comment.
FYI, EsprimaExceptionHelper isn't used consistently throughout the code base.
As a matter of fact, I fail to see why it exists at all (when throw expressions are a thing since C# 7.0). What do we gain from e.g. Description = description ?? ThrowArgumentNullException<string>(nameof(description)); over Description = description ?? throw new ArgumentNullException(nameof(description));? I mean it's not even shorter and you need to type out the generic parameter too. Plus it breaks static analysis. (Not even DoesNotReturn solves this completely as the compiler still complains when it's used in a switch case branch.)
So how about getting rid of EsprimaExceptionHelper instead? I'm willing to do that if you sign off on it.
There was a problem hiding this comment.
It's for JIT mostly, JIT cannot inline methods that have throw in them (call stack would be wrong).
There was a problem hiding this comment.
Wow, I learned something today again! Thanks for this valuable info. I'll revise throw usage and change it to the helper method where inlining justifies it.
There was a problem hiding this comment.
Yeah, method must of course be quite small to be inline candidate (that's why some usages would be more for consistency). Also I think that the return of T for some throw helpers might not work when wanting the "throw helper pattern", they might need to be void, but not an issue for this PR, just a mental note.
There was a problem hiding this comment.
I annotated all my methods with [MethodImpl(MethodImplOptions.AggressiveInlining)] which I think it's beneficial to inline. Don't really care whether JIT inlines the rest or not, so in those cases I just use plain throws. As I see, this pattern is followed in other parts of the code (e.g. in JavascriptParser). So, are we good or do you insist on using the throw helpers everywhere?
There was a problem hiding this comment.
No need, we can re-evaluate if needed later.
|
|
||
| if (options is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(options)); |
There was a problem hiding this comment.
should prefer EsprimaExceptionHelper for consistency
| { | ||
| if (writerFactory is null) | ||
| { | ||
| throw new ArgumentNullException(nameof(writerFactory)); |
There was a problem hiding this comment.
should prefer `EsprimaExceptionHelper`` for consistency
| case TokenType.Template: | ||
| break; | ||
| default: | ||
| throw new InvalidOperationException(); |
There was a problem hiding this comment.
should prefer EsprimaExceptionHelper for consistency, argument out of range?
There was a problem hiding this comment.
It can't really be an argument out of range because we don't switch on an argument here but a class field. Maybe NotImplementedException is something to consider.
|
|
||
| if (!string.IsNullOrWhiteSpace(indent)) | ||
| { | ||
| throw new ArgumentException("Indent must be null or white-space.", nameof(indent)); |
There was a problem hiding this comment.
should prefer EsprimaExceptionHelper for consistency
| /// </summary> | ||
| public class KnRJavascriptTextWriter : JavascriptTextWriter | ||
| { | ||
| public new record class Options : JavascriptTextWriter.Options |
There was a problem hiding this comment.
the new here feels bad, should this just be KnRJavascriptTextWriterOptions? In deal world could just pass suitable Options instance to JavascriptTextWriter that configures KnR style, but seems that it wouldn't be that easy to implement.
There was a problem hiding this comment.
I myself would have gone with KnRJavascriptTextWriterOptions but I wanted to keep it consistent with existing code: AstJon has had this nested option class thing, I just followed the pattern. So I suggest leaving it as is (or we should change it everywhere but that would mean unnecessary BCs).
In deal world could just pass suitable Options instance to JavascriptTextWriter that configures KnR style, but seems that it wouldn't be that easy to implement.
We need to keep the design extensible (for other code styles or AST extensions), so we have to use the strategy design pattern here in one form or the other. The current design does that and I can't really think of a simpler one. If we had a single writer class which accepted multiple types of options (it must be multiple types since the possible options for all possible code styles are not known in advance), then the writer would need to make decisions based on the received option type and eventually it would implement the same strategy pattern we have now, just internally.
|
|
||
| if (!string.IsNullOrWhiteSpace(extendedOptions.Indent)) | ||
| { | ||
| throw new ArgumentException("Indent must be null or white-space.", nameof(extendedOptions)); |
| } | ||
| } | ||
|
|
||
| #region Arrays |
|
Awesome work folks! Just for background on my testing, here's how my pipeline works in part:
I've run it through a part of my normal test set so far. I ran into one file that failed step 5: It seems to generate the following:
It seems like it's not putting the conditional expression in parentheses, which means that the |
…Converter with it [BC]
…on of issues + implement missing bits
lahma
left a comment
There was a problem hiding this comment.
Changes look good to me, and we can always fine-tune later if necessary.
|
Great work again folks, great to see work iterated and improved based on feedback cycle 🚀 |
|
I've published the latest main on NuGet (including this PR) as version 3.0.0-beta-3. |


rebase of the branch #154