diff --git a/js/ast.go b/js/ast.go index 515f996..3e92a8a 100644 --- a/js/ast.go +++ b/js/ast.go @@ -34,9 +34,14 @@ func (ast AST) String() string { // JS writes JavaScript to writer. func (ast AST) JS(w io.Writer) { - for _, item := range ast.List { + for i, item := range ast.List { + if i != 0 { + w.Write([]byte("\n")) + } item.JS(w) - w.Write([]byte("; ")) + if _, ok := item.(*VarDecl); ok { + w.Write([]byte(";")) + } } } @@ -414,6 +419,7 @@ func (n Comment) String() string { // JS writes JavaScript to writer. func (n Comment) JS(w io.Writer) { w.Write(n.Value) + w.Write([]byte("\n")) } // BlockStmt is a block statement. @@ -432,15 +438,21 @@ func (n BlockStmt) String() string { // JS writes JavaScript to writer. func (n BlockStmt) JS(w io.Writer) { - if n.Scope.Parent == nil { - panic("X") + if len(n.List) == 0 { + w.Write([]byte("{}")) + return } - w.Write([]byte("{ ")) + + w.Write([]byte("{")) + wi := NewIndenter(w, 4) for _, item := range n.List { - item.JS(w) - w.Write([]byte("; ")) + wi.Write([]byte("\n")) + item.JS(wi) + if _, ok := item.(*VarDecl); ok { + w.Write([]byte(";")) + } } - w.Write([]byte("}")) + w.Write([]byte("\n}")) } // EmptyStmt is an empty statement. @@ -451,7 +463,9 @@ func (n EmptyStmt) String() string { } // JS writes JavaScript to writer. -func (n EmptyStmt) JS(w io.Writer) {} +func (n EmptyStmt) JS(w io.Writer) { + w.Write([]byte(";")) +} // ExprStmt is an expression statement. type ExprStmt struct { @@ -469,6 +483,7 @@ func (n ExprStmt) String() string { // JS writes JavaScript to writer. func (n ExprStmt) JS(w io.Writer) { n.Value.JS(w) + w.Write([]byte(";")) } // IfStmt is an if statement. @@ -490,25 +505,14 @@ func (n IfStmt) String() string { func (n IfStmt) JS(w io.Writer) { w.Write([]byte("if (")) n.Cond.JS(w) - w.Write([]byte(") ")) - switch n.Body.(type) { - case *BlockStmt: - n.Body.JS(w) - default: - w.Write([]byte("{ ")) - n.Body.JS(w) - w.Write([]byte(" }")) + w.Write([]byte(")")) + if _, ok := n.Body.(*EmptyStmt); !ok { + w.Write([]byte(" ")) } + n.Body.JS(w) if n.Else != nil { - switch n.Else.(type) { - case *BlockStmt: - w.Write([]byte(" else ")) - n.Else.JS(w) - default: - w.Write([]byte(" else { ")) - n.Else.JS(w) - w.Write([]byte(" }")) - } + w.Write([]byte(" else ")) + n.Else.JS(w) } } @@ -524,18 +528,14 @@ func (n DoWhileStmt) String() string { // JS writes JavaScript to writer. func (n DoWhileStmt) JS(w io.Writer) { - w.Write([]byte("do ")) - switch n.Body.(type) { - case *BlockStmt: - n.Body.JS(w) - default: - w.Write([]byte("{ ")) - n.Body.JS(w) - w.Write([]byte(" }")) + w.Write([]byte("do")) + if _, ok := n.Body.(*EmptyStmt); !ok { + w.Write([]byte(" ")) } + n.Body.JS(w) w.Write([]byte(" while (")) n.Cond.JS(w) - w.Write([]byte(")")) + w.Write([]byte(");")) } // WhileStmt is a while iteration statement. @@ -552,10 +552,11 @@ func (n WhileStmt) String() string { func (n WhileStmt) JS(w io.Writer) { w.Write([]byte("while (")) n.Cond.JS(w) - w.Write([]byte(") ")) - if n.Body != nil { - n.Body.JS(w) + w.Write([]byte(")")) + if _, ok := n.Body.(*EmptyStmt); !ok { + w.Write([]byte(" ")) } + n.Body.JS(w) } // ForStmt is a regular for iteration statement. @@ -684,7 +685,6 @@ func (n CaseClause) JS(w io.Writer) { for _, item := range n.List { w.Write([]byte(" ")) item.JS(w) - w.Write([]byte(";")) } } @@ -735,6 +735,7 @@ func (n BranchStmt) JS(w io.Writer) { w.Write([]byte(" ")) w.Write(n.Label) } + w.Write([]byte(";")) } // ReturnStmt is a return statement. @@ -757,6 +758,7 @@ func (n ReturnStmt) JS(w io.Writer) { w.Write([]byte(" ")) n.Value.JS(w) } + w.Write([]byte(";")) } // WithStmt is a with statement. @@ -807,6 +809,7 @@ func (n ThrowStmt) String() string { func (n ThrowStmt) JS(w io.Writer) { w.Write([]byte("throw ")) n.Value.JS(w) + w.Write([]byte(";")) } // TryStmt is a try statement. @@ -861,7 +864,7 @@ func (n DebuggerStmt) String() string { // JS writes JavaScript to writer. func (n DebuggerStmt) JS(w io.Writer) { - w.Write([]byte("debugger")) + w.Write([]byte("debugger;")) } // Alias is a name space import or import/export specifier for import/export statements. @@ -953,6 +956,7 @@ func (n ImportStmt) JS(w io.Writer) { } w.Write([]byte(" ")) w.Write(n.Module) + w.Write([]byte(";")) } // ExportStmt is an export statement. @@ -999,6 +1003,7 @@ func (n ExportStmt) JS(w io.Writer) { } w.Write([]byte(" ")) n.Decl.JS(w) + w.Write([]byte(";")) return } else if len(n.List) == 1 && (len(n.List[0].Name) == 1 && n.List[0].Name[0] == '*' || n.List[0].Name == nil && len(n.List[0].Binding) == 1 && n.List[0].Binding[0] == '*') { w.Write([]byte(" ")) @@ -1034,6 +1039,7 @@ func (n DirectivePrologueStmt) String() string { // JS writes JavaScript to writer. func (n DirectivePrologueStmt) JS(w io.Writer) { w.Write(n.Value) + w.Write([]byte(";")) } func (n Comment) stmtNode() {} @@ -1502,6 +1508,7 @@ func (n ClassElement) JS(w io.Writer) { return } n.Field.JS(w) + w.Write([]byte(";")) } // ClassDecl is a class declaration. @@ -1536,12 +1543,12 @@ func (n ClassDecl) JS(w io.Writer) { w.Write([]byte(" extends ")) n.Extends.JS(w) } - w.Write([]byte(" { ")) + w.Write([]byte(" {")) for _, item := range n.List { + w.Write([]byte(" ")) item.JS(w) - w.Write([]byte("; ")) } - w.Write([]byte("}")) + w.Write([]byte(" }")) } func (n VarDecl) stmtNode() {} diff --git a/js/ast_test.go b/js/ast_test.go index c4bedb0..84d1530 100644 --- a/js/ast_test.go +++ b/js/ast_test.go @@ -2,6 +2,7 @@ package js import ( "io" + "regexp" "testing" "github.com/tdewolff/parse/v2" @@ -13,109 +14,114 @@ func TestJS(t *testing.T) { js string expected string }{ - {"if (true) { x(1, 2, 3); };", "if (true) { x(1, 2, 3); }; "}, - {"if (true) { true; };", "if (true) { true; }; "}, - {"if (true) { true; } else { false; };", "if (true) { true; } else { false; }; "}, - {"if (true) { true; } else { if(true) { true; } else { false; } };", "if (true) { true; } else { if (true) { true; } else { false; }; }; "}, - {"do { continue; } while (true);", "do { continue; } while (true); "}, - {"do { x = 1; } while (true);", "do { x = 1; } while (true); "}, - {"while (true) { true; };", "while (true) { true; }; "}, - {"while (true) { x = 1; };", "while (true) { x = 1; }; "}, - {"for ( ; ; ) { true; };", "for ( ; ; ) { true; }; "}, - {"for (x = 1; ; ) { true; };", "for (x = 1; ; ) { true; }; "}, - {"for (x = 1; x < 2; ) { true; };", "for (x = 1; x < 2; ) { true; }; "}, - {"for (x = 1; x < 2; x++) { true; };", "for (x = 1; x < 2; x++) { true; }; "}, - {"for (x = 1; x < 2; x++) { x = 1; };", "for (x = 1; x < 2; x++) { x = 1; }; "}, - {"for (var x in [1, 2]) { true; };", "for (var x in [1, 2]) { true; }; "}, - {"for (var x in [1, 2]) { x = 1; };", "for (var x in [1, 2]) { x = 1; }; "}, - {"for (const element of [1, 2]) { true; };", "for (const element of [1, 2]) { true; }; "}, - {"for (const element of [1, 2]) { x = 1; };", "for (const element of [1, 2]) { x = 1; }; "}, - {"switch (true) { case true: break; case false: false; };", "switch (true) { case true: break; case false: false; }; "}, - {"switch (true) { case true: x(); break; case false: x(); false; };", "switch (true) { case true: x(); break; case false: x(); false; }; "}, - {"switch (true) { default: false; };", "switch (true) { default: false; }; "}, - {"for (i = 0; i < 3; i++) { continue; }; ", "for (i = 0; i < 3; i++) { continue; }; "}, - {"for (i = 0; i < 3; i++) { x = 1; }; ", "for (i = 0; i < 3; i++) { x = 1; }; "}, - {"return;", "return; "}, - {"return 1;", "return 1; "}, - {"with (true) { true; };", "with (true) { true; }; "}, - {"with (true) { x = 1; };", "with (true) { x = 1; }; "}, - {"loop: for (x = 0; x < 1; x++) { true; };", "loop: for (x = 0; x < 1; x++) { true; }; "}, - {"throw x;", "throw x; "}, - {"try { true; } catch(e) { };", "try { true; } catch(e) { }; "}, - {"try { true; } catch(e) { true; };", "try { true; } catch(e) { true; }; "}, - {"try { true; } catch(e) { x = 1; };", "try { true; } catch(e) { x = 1; }; "}, - {"debugger;", "debugger; "}, - {"import * as name from 'module-name';", "import * as name from 'module-name'; "}, - {"import defaultExport from 'module-name';", "import defaultExport from 'module-name'; "}, - {"import * as name from 'module-name';", "import * as name from 'module-name'; "}, - {"import { export1 } from 'module-name';", "import { export1 } from 'module-name'; "}, - {"import { export1 as alias1 } from 'module-name';", "import { export1 as alias1 } from 'module-name'; "}, - {"import { export1 , export2 } from 'module-name';", "import { export1 , export2 } from 'module-name'; "}, - {"import { foo , bar } from 'module-name/path/to/specific/un-exported/file';", "import { foo , bar } from 'module-name/path/to/specific/un-exported/file'; "}, - {"import defaultExport, * as name from 'module-name';", "import defaultExport , * as name from 'module-name'; "}, - {"import 'module-name';", "import 'module-name'; "}, - {"var promise = import('module-name');", "var promise = import('module-name'); "}, - {"export { myFunction as default };", "export { myFunction as default }; "}, - {"export default k = 12;", "export default k = 12; "}, - {"'use strict';", "'use strict'; "}, - {"let [name1, name2 = 6] = z;", "let [name1, name2 = 6] = z; "}, - {"let {name1, key2: name2} = z;", "let {name1, key2: name2} = z; "}, - {"let [{name: key, ...rest}, ...[c,d=9]] = z;", "let [{name: key, ...rest}, ...[c, d = 9]] = z; "}, - {"var x;", "var x; "}, - {"var x = 1;", "var x = 1; "}, - {"var x, y = [];", "var x, y = []; "}, - {"let x;", "let x; "}, - {"let x = 1;", "let x = 1; "}, - {"const x = 1;", "const x = 1; "}, - {"function xyz (a, b) { };", "function xyz (a, b) { }; "}, - {"function xyz (a, b, ...c) { };", "function xyz (a, b, ...c) { }; "}, - {"function xyz (a, b) { };", "function xyz (a, b) { }; "}, - {"class A { field; static get method () { }; };", "class A { field; static get method () { }; }; "}, - {"class A { field; };", "class A { field; }; "}, - {"class A { field = 5; };", "class A { field = 5; }; "}, - {"class A { field; static get method () { }; };", "class A { field; static get method () { }; }; "}, - {"class B extends A { field; static get method () { }; };", "class B extends A { field; static get method () { }; }; "}, + {"if (true) { x(1, 2, 3); }", "if (true) { x(1, 2, 3); }"}, + {"if (true) { true; }", "if (true) { true; }"}, + {"if (true) { true; } else { false; }", "if (true) { true; } else { false; }"}, + {"if (true) { true; } else { if(true) { true; } else { false; } }", "if (true) { true; } else { if (true) { true; } else { false; } }"}, + {"do { continue; } while (true);", "do { continue; } while (true);"}, + {"do { x = 1; } while (true);", "do { x = 1; } while (true);"}, + {"while (true) { true; }", "while (true) { true; }"}, + {"while (true) { x = 1; }", "while (true) { x = 1; }"}, + {"for ( ; ; ) { true; }", "for ( ; ; ) { true; }"}, + {"for (x = 1; ; ) { true; }", "for (x = 1; ; ) { true; }"}, + {"for (x = 1; x < 2; ) { true; }", "for (x = 1; x < 2; ) { true; }"}, + {"for (x = 1; x < 2; x++) { true; }", "for (x = 1; x < 2; x++) { true; }"}, + {"for (x = 1; x < 2; x++) { x = 1; }", "for (x = 1; x < 2; x++) { x = 1; }"}, + {"for (var x in [1, 2]) { true; }", "for (var x in [1, 2]) { true; }"}, + {"for (var x in [1, 2]) { x = 1; }", "for (var x in [1, 2]) { x = 1; }"}, + {"for (const element of [1, 2]) { true; }", "for (const element of [1, 2]) { true; }"}, + {"for (const element of [1, 2]) { x = 1; }", "for (const element of [1, 2]) { x = 1; }"}, + {"switch (true) { case true: break; case false: false; }", "switch (true) { case true: break; case false: false; }"}, + {"switch (true) { case true: x(); break; case false: x(); false; }", "switch (true) { case true: x(); break; case false: x(); false; }"}, + {"switch (true) { default: false; }", "switch (true) { default: false; }"}, + {"for (i = 0; i < 3; i++) { continue; }", "for (i = 0; i < 3; i++) { continue; }"}, + {"for (i = 0; i < 3; i++) { x = 1; }", "for (i = 0; i < 3; i++) { x = 1; }"}, + {"return;", "return;"}, + {"return 1;", "return 1;"}, + {"with (true) { true; }", "with (true) { true; }"}, + {"with (true) { x = 1; }", "with (true) { x = 1; }"}, + {"loop: for (x = 0; x < 1; x++) { true; }", "loop: for (x = 0; x < 1; x++) { true; }"}, + {"throw x;", "throw x;"}, + {"try { true; } catch(e) { }", "try { true; } catch(e) {}"}, + {"try { true; } catch(e) { true; }", "try { true; } catch(e) { true; }"}, + {"try { true; } catch(e) { x = 1; }", "try { true; } catch(e) { x = 1; }"}, + {"debugger;", "debugger;"}, + {"import * as name from 'module-name';", "import * as name from 'module-name';"}, + {"import defaultExport from 'module-name';", "import defaultExport from 'module-name';"}, + {"import * as name from 'module-name';", "import * as name from 'module-name';"}, + {"import { export1 } from 'module-name';", "import { export1 } from 'module-name';"}, + {"import { export1 as alias1 } from 'module-name';", "import { export1 as alias1 } from 'module-name';"}, + {"import { export1 , export2 } from 'module-name';", "import { export1 , export2 } from 'module-name';"}, + {"import { foo , bar } from 'module-name/path/to/specific/un-exported/file';", "import { foo , bar } from 'module-name/path/to/specific/un-exported/file';"}, + {"import defaultExport, * as name from 'module-name';", "import defaultExport , * as name from 'module-name';"}, + {"import 'module-name';", "import 'module-name';"}, + {"var promise = import('module-name');", "var promise = import('module-name');"}, + {"export { myFunction as default }", "export { myFunction as default }"}, + {"export default k = 12;", "export default k = 12;"}, + {"'use strict';", "'use strict';"}, + {"let [name1, name2 = 6] = z;", "let [name1, name2 = 6] = z;"}, + {"let {name1, key2: name2} = z;", "let {name1, key2: name2} = z;"}, + {"let [{name: key, ...rest}, ...[c,d=9]] = z;", "let [{name: key, ...rest}, ...[c, d = 9]] = z;"}, + {"var x;", "var x;"}, + {"var x = 1;", "var x = 1;"}, + {"var x, y = [];", "var x, y = [];"}, + {"let x;", "let x;"}, + {"let x = 1;", "let x = 1;"}, + {"const x = 1;", "const x = 1;"}, + {"function xyz (a, b) { }", "function xyz (a, b) {}"}, + {"function xyz (a, b, ...c) { }", "function xyz (a, b, ...c) {}"}, + {"function xyz (a, b) { }", "function xyz (a, b) {}"}, + {"class A { field; static get method () { } }", "class A { field; static get method () {} }"}, + {"class A { field; }", "class A { field; }"}, + {"class A { field = 5; }", "class A { field = 5; }"}, + {"class A { field; static get method () { } }", "class A { field; static get method () {} }"}, + {"class B extends A { field; static get method () { } }", "class B extends A { field; static get method () {} }"}, - {"x = 1;", "x = 1; "}, - {"'test';", "'test'; "}, - {"[1, 2, 3];", "[1, 2, 3]; "}, - {`x = {x: "value"};`, `x = {x: "value"}; `}, - {`x = {"x": "value"};`, `x = {x: "value"}; `}, - {`x = {"1a": 2};`, `x = {"1a": 2}; `}, - {`x = {x: "value", y: "value"};`, `x = {x: "value", y: "value"}; `}, - {"x = `value`;", "x = `value`; "}, - {"x = `value${'hi'}`;", "x = `value${'hi'}`; "}, - {"x = (1 + 1) / 1;", "x = (1 + 1) / 1; "}, - {"x = y[1];", "x = y[1]; "}, - {"x = y.z;", "x = y.z; "}, - {"x = new.target;", "x = new.target; "}, - {"x = import.meta;", "x = import.meta; "}, - {"x(1, 2);", "x(1, 2); "}, - {"new x;", "new x(); "}, - {"new x(1);", "new x(1); "}, - {"new Date().getTime();", "new Date().getTime(); "}, - {"x();", "x(); "}, - {"x = y?.z;", "x = y?.z; "}, - {"x = -a;", "x = -a; "}, - {"x = - --a;", "x = - --a; "}, - {"a << b;", "a << b; "}, - {"a && b;", "a && b; "}, - {"a || b;", "a || b; "}, - {"x = function* foo (x) { while (x < 2) { yield x; x++; }; };", "x = function* foo (x) { while (x < 2) { yield x; x++; }; }; "}, - {"(x) => { y(); };", "(x) => { y(); }; "}, - {"(x, y) => { z(); };", "(x, y) => { z(); }; "}, - {"async (x, y) => { z(); };", "async (x, y) => { z(); }; "}, - {"await x;", "await x; "}, - {"export default await x;", "export default await x; "}, - {"export let a = await x;", "export let a = await x; "}, + {"x = 1;", "x = 1;"}, + {"'test';", "'test';"}, + {"[1, 2, 3];", "[1, 2, 3];"}, + {`x = {x: "value"};`, `x = {x: "value"};`}, + {`x = {"x": "value"};`, `x = {x: "value"};`}, + {`x = {"1a": 2};`, `x = {"1a": 2};`}, + {`x = {x: "value", y: "value"};`, `x = {x: "value", y: "value"};`}, + {"x = `value`;", "x = `value`;"}, + {"x = `value${'hi'}`;", "x = `value${'hi'}`;"}, + {"x = (1 + 1) / 1;", "x = (1 + 1) / 1;"}, + {"x = y[1];", "x = y[1];"}, + {"x = y.z;", "x = y.z;"}, + {"x = new.target;", "x = new.target;"}, + {"x = import.meta;", "x = import.meta;"}, + {"x(1, 2);", "x(1, 2);"}, + {"new x;", "new x();"}, + {"new x(1);", "new x(1);"}, + {"new Date().getTime();", "new Date().getTime();"}, + {"x();", "x();"}, + {"x = y?.z;", "x = y?.z;"}, + {"x = -a;", "x = -a;"}, + {"x = - --a;", "x = - --a;"}, + {"a << b;", "a << b;"}, + {"a && b;", "a && b;"}, + {"a || b;", "a || b;"}, + {"x = function* foo (x) { while (x < 2) { yield x; x++; } };", "x = function* foo (x) { while (x < 2) { yield x; x++; } };"}, + {"(x) => { y(); };", "(x) => { y(); };"}, + {"(x, y) => { z(); };", "(x, y) => { z(); };"}, + {"async (x, y) => { z(); };", "async (x, y) => { z(); };"}, + {"await x;", "await x;"}, + {"export default await x;", "export default await x;"}, + {"export let a = await x;", "export let a = await x;"}, + {"if(k00)while((0))", "if (k00) while ((0));"}, } + + re := regexp.MustCompile("\n *") for _, tt := range tests { t.Run(tt.js, func(t *testing.T) { ast, err := Parse(parse.NewInputString(tt.js), Options{}) if err != io.EOF { test.Error(t, err) } - test.String(t, ast.JSString(), tt.expected) + src := ast.JSString() + src = re.ReplaceAllString(src, " ") + test.String(t, src, tt.expected) }) } } diff --git a/js/util.go b/js/util.go index 78a629c..1463dc2 100644 --- a/js/util.go +++ b/js/util.go @@ -1,5 +1,9 @@ package js +import ( + "io" +) + // AsIdentifierName returns true if a valid identifier name is given. func AsIdentifierName(b []byte) bool { if len(b) == 0 || !identifierStartTable[b[0]] { @@ -36,3 +40,39 @@ func AsDecimalLiteral(b []byte) bool { } return i == len(b) } + +type Indenter struct { + w io.Writer + b []byte +} + +func NewIndenter(w io.Writer, n int) Indenter { + if wi, ok := w.(Indenter); ok { + w = wi.w + n += len(wi.b) + } + + b := make([]byte, n) + for i := range b { + b[i] = ' ' + } + return Indenter{ + w: w, + b: b, + } +} + +func (in Indenter) Write(b []byte) (int, error) { + n, j := 0, 0 + for i, c := range b { + if c == '\n' { + m, _ := in.w.Write(b[j : i+1]) + n += m + m, _ = in.w.Write(in.b) + n += m + j = i + 1 + } + } + m, err := in.w.Write(b[j:]) + return n + m, err +} diff --git a/js/walk_test.go b/js/walk_test.go index 9df49d9..63152cc 100644 --- a/js/walk_test.go +++ b/js/walk_test.go @@ -2,6 +2,7 @@ package js import ( "bytes" + "regexp" "testing" "github.com/tdewolff/parse/v2" @@ -38,8 +39,11 @@ func TestWalk(t *testing.T) { Walk(&walker{}, ast) + re := regexp.MustCompile("\n *") t.Run("TestWalk", func(t *testing.T) { - test.String(t, ast.JSString(), "if (true) { for (i = 0; i < 1; i++) { obj.y = i; }; }; ") + src := ast.JSString() + src = re.ReplaceAllString(src, " ") + test.String(t, src, "if (true) { for (i = 0; i < 1; i++) { obj.y = i; } }") }) }