diff --git a/.gitignore b/.gitignore index f9d1730..a39cb33 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,6 @@ /unicode/uc-nl.htm /unicode/uc-pc.htm cabal.sandbox.config + +# stack +/.stack-work/ diff --git a/language-javascript.cabal b/language-javascript.cabal index b1900e5..6e0e06f 100644 --- a/language-javascript.cabal +++ b/language-javascript.cabal @@ -9,7 +9,9 @@ License: BSD3 License-file: LICENSE Author: Alan Zimmerman Maintainer: Erik de Castro Lopo -Copyright: (c) 2010-2015 Alan Zimmerman, 2015 Erik de Castro Lopo +Copyright: (c) 2010-2015 Alan Zimmerman + (c) 2015-2018 Erik de Castro Lopo + (c) 2018 Daniel Gasienica Category: Language Build-type: Simple homepage: https://github.com/erikd/language-javascript @@ -81,6 +83,7 @@ Test-Suite testsuite Test.Language.Javascript.Lexer Test.Language.Javascript.LiteralParser Test.Language.Javascript.Minify + Test.Language.Javascript.ModuleParser Test.Language.Javascript.ProgramParser Test.Language.Javascript.RoundTrip Test.Language.Javascript.StatementParser diff --git a/src/Language/JavaScript/Parser.hs b/src/Language/JavaScript/Parser.hs index a813bc9..750c0ed 100644 --- a/src/Language/JavaScript/Parser.hs +++ b/src/Language/JavaScript/Parser.hs @@ -1,7 +1,9 @@ module Language.JavaScript.Parser ( PA.parse + , PA.parseModule , PA.readJs + , PA.readJsModule , PA.parseFile , PA.parseFileUtf8 , PA.showStripped @@ -40,5 +42,3 @@ import Language.JavaScript.Parser.SrcLocation import Language.JavaScript.Pretty.Printer -- EOF - - diff --git a/src/Language/JavaScript/Parser/AST.hs b/src/Language/JavaScript/Parser/AST.hs index 53e6b31..9e157db 100644 --- a/src/Language/JavaScript/Parser/AST.hs +++ b/src/Language/JavaScript/Parser/AST.hs @@ -23,6 +23,11 @@ module Language.JavaScript.Parser.AST , JSCommaList (..) , JSCommaTrailingList (..) + -- Modules + , JSModuleItem (..) + , JSExportDeclaration (..) + , JSExportLocalSpecifier (..) + , binOpEq , showStripped ) where @@ -42,12 +47,34 @@ data JSAnnot data JSAST - = JSAstProgram ![JSStatement] !JSAnnot -- ^source elements, tailing whitespace + = JSAstProgram ![JSStatement] !JSAnnot -- ^source elements, trailing whitespace + | JSAstModule ![JSModuleItem] !JSAnnot | JSAstStatement !JSStatement !JSAnnot | JSAstExpression !JSExpression !JSAnnot | JSAstLiteral !JSExpression !JSAnnot deriving (Data, Eq, Show, Typeable) +-- Shift AST +-- https://github.com/shapesecurity/shift-spec/blob/83498b92c436180cc0e2115b225a68c08f43c53e/spec.idl#L229-L234 +data JSModuleItem + -- = JSImportDeclaration + = JSModuleExportDeclaration !JSAnnot !JSExportDeclaration -- ^export,decl + | JSModuleStatementListItem !JSStatement + deriving (Data, Eq, Show, Typeable) + +data JSExportDeclaration + -- = JSExportAllFrom + -- | JSExportFrom + = JSExportLocals !JSAnnot !(JSCommaList JSExportLocalSpecifier) !JSAnnot !JSSemi -- ^lb, specifiers, rb, autosemi + | JSExport !JSStatement !JSSemi -- ^body, autosemi + -- | JSExportDefault + deriving (Data, Eq, Show, Typeable) + +data JSExportLocalSpecifier + = JSExportLocalSpecifier !JSIdent -- ^ident + | JSExportLocalSpecifierAs !JSIdent !JSBinOp !JSIdent -- ^ident1, as, ident2 + deriving (Data, Eq, Show, Typeable) + data JSStatement = JSStatementBlock !JSAnnot ![JSStatement] !JSAnnot !JSSemi -- ^lbrace, stmts, rbrace, autosemi | JSBreak !JSAnnot !JSIdent !JSSemi -- ^break,optional identifier, autosemi @@ -71,7 +98,7 @@ data JSStatement | JSSwitch !JSAnnot !JSAnnot !JSExpression !JSAnnot !JSAnnot ![JSSwitchParts] !JSAnnot !JSSemi -- ^switch,lb,expr,rb,caseblock,autosemi | JSThrow !JSAnnot !JSExpression !JSSemi -- ^throw val autosemi | JSTry !JSAnnot !JSBlock ![JSTryCatch] !JSTryFinally -- ^try,block,catches,finally - | JSVariable !JSAnnot !(JSCommaList JSExpression) !JSSemi -- ^var|const, decl, autosemi + | JSVariable !JSAnnot !(JSCommaList JSExpression) !JSSemi -- ^var, decl, autosemi | JSWhile !JSAnnot !JSAnnot !JSExpression !JSAnnot !JSStatement -- ^while,lb,expr,rb,stmt | JSWith !JSAnnot !JSAnnot !JSExpression !JSAnnot !JSStatement !JSSemi -- ^with,lb,expr,rb,stmt list deriving (Data, Eq, Show, Typeable) @@ -112,6 +139,7 @@ data JSExpression data JSBinOp = JSBinOpAnd !JSAnnot + | JSBinOpAs !JSAnnot | JSBinOpBitAnd !JSAnnot | JSBinOpBitOr !JSAnnot | JSBinOpBitXor !JSAnnot @@ -233,11 +261,12 @@ data JSCommaTrailingList a deriving (Data, Eq, Show, Typeable) -- ----------------------------------------------------------------------------- --- | Show the AST elements stipped of their JSAnnot data. +-- | Show the AST elements stripped of their JSAnnot data. -- Strip out the location info showStripped :: JSAST -> String showStripped (JSAstProgram xs _) = "JSAstProgram " ++ ss xs +showStripped (JSAstModule xs _) = "JSAstModule " ++ ss xs showStripped (JSAstStatement s _) = "JSAstStatement (" ++ ss s ++ ")" showStripped (JSAstExpression e _) = "JSAstExpression (" ++ ss e ++ ")" showStripped (JSAstLiteral s _) = "JSAstLiteral (" ++ ss s ++ ")" @@ -246,7 +275,6 @@ showStripped (JSAstLiteral s _) = "JSAstLiteral (" ++ ss s ++ ")" class ShowStripped a where ss :: a -> String - instance ShowStripped JSStatement where ss (JSStatementBlock _ xs _ _) = "JSStatementBlock " ++ ss xs ss (JSBreak _ JSIdentNone s) = "JSBreak" ++ commaIf (ss s) @@ -308,6 +336,18 @@ instance ShowStripped JSExpression where ss (JSVarInitExpression x1 x2) = "JSVarInitExpression (" ++ ss x1 ++ ") " ++ ss x2 ss (JSSpreadExpression _ x1) = "JSSpreadExpression (" ++ ss x1 ++ ")" +instance ShowStripped JSModuleItem where + ss (JSModuleExportDeclaration _ x1) = "JSModuleExportDeclaration (" ++ ss x1 ++ ")" + ss (JSModuleStatementListItem x1) = "JSModuleStatementListItem (" ++ ss x1 ++ ")" + +instance ShowStripped JSExportDeclaration where + ss (JSExportLocals _ xs _ _) = "JSExportLocals (" ++ ss xs ++ ")" + ss (JSExport x1 _) = "JSExport (" ++ ss x1 ++ ")" + +instance ShowStripped JSExportLocalSpecifier where + ss (JSExportLocalSpecifier x1) = "JSExportLocalSpecifier (" ++ ss x1 ++ ")" + ss (JSExportLocalSpecifierAs x1 _ x2) = "JSExportLocalSpecifierAs (" ++ ss x1 ++ "," ++ ss x2 ++ ")" + instance ShowStripped JSTryCatch where ss (JSCatch _ _lb x1 _rb x3) = "JSCatch (" ++ ss x1 ++ "," ++ ss x3 ++ ")" ss (JSCatchIf _ _lb x1 _ ex _rb x3) = "JSCatch (" ++ ss x1 ++ ") if " ++ ss ex ++ " (" ++ ss x3 ++ ")" @@ -342,6 +382,7 @@ instance ShowStripped JSSwitchParts where instance ShowStripped JSBinOp where ss (JSBinOpAnd _) = "'&&'" + ss (JSBinOpAs _) = "'as'" ss (JSBinOpBitAnd _) = "'&'" ss (JSBinOpBitOr _) = "'|'" ss (JSBinOpBitXor _) = "'^'" @@ -437,6 +478,7 @@ commaIf xs = ',' : xs deAnnot :: JSBinOp -> JSBinOp deAnnot (JSBinOpAnd _) = JSBinOpAnd JSNoAnnot +deAnnot (JSBinOpAs _) = JSBinOpAs JSNoAnnot deAnnot (JSBinOpBitAnd _) = JSBinOpBitAnd JSNoAnnot deAnnot (JSBinOpBitOr _) = JSBinOpBitOr JSNoAnnot deAnnot (JSBinOpBitXor _) = JSBinOpBitXor JSNoAnnot diff --git a/src/Language/JavaScript/Parser/Grammar7.y b/src/Language/JavaScript/Parser/Grammar7.y index 1d4b2b9..f2e9406 100644 --- a/src/Language/JavaScript/Parser/Grammar7.y +++ b/src/Language/JavaScript/Parser/Grammar7.y @@ -1,11 +1,12 @@ { {-# LANGUAGE BangPatterns #-} module Language.JavaScript.Parser.Grammar7 - ( parseProgram - , parseStatement - , parseExpression - , parseLiteral - ) where + ( parseProgram + , parseModule + , parseStatement + , parseExpression + , parseLiteral + ) where import Data.Char import Language.JavaScript.Parser.Lexer @@ -18,8 +19,9 @@ import qualified Language.JavaScript.Parser.AST as AST -- The name of the generated function to be exported from the module %name parseProgram Program +%name parseModule Module %name parseLiteral LiteralMain -%name parseExpression ExpressionMain +%name parseExpression ExpressionMain %name parseStatement StatementMain %tokentype { Token } @@ -81,6 +83,7 @@ import qualified Language.JavaScript.Parser.AST as AST '(' { LeftParenToken {} } ')' { RightParenToken {} } + 'as' { AsToken {} } 'autosemi' { AutoSemiToken {} } 'break' { BreakToken {} } 'case' { CaseToken {} } @@ -93,6 +96,7 @@ import qualified Language.JavaScript.Parser.AST as AST 'do' { DoToken {} } 'else' { ElseToken {} } 'enum' { EnumToken {} } + 'export' { ExportToken {} } 'false' { FalseToken {} } 'finally' { FinallyToken {} } 'for' { ForToken {} } @@ -244,6 +248,9 @@ Ge : '>=' { AST.JSBinOpGe (mkJSAnnot $1) } Gt :: { AST.JSBinOp } Gt : '>' { AST.JSBinOpGt (mkJSAnnot $1) } +As :: { AST.JSBinOp } +As : 'as' { AST.JSBinOpAs (mkJSAnnot $1) } + In :: { AST.JSBinOp } In : 'in' { AST.JSBinOpIn (mkJSAnnot $1) } @@ -305,6 +312,9 @@ Let : 'let' { mkJSAnnot $1 } Const :: { AST.JSAnnot } Const : 'const' { mkJSAnnot $1 } +Export :: { AST.JSAnnot } +Export : 'export' { mkJSAnnot $1 } + If :: { AST.JSAnnot } If : 'if' { mkJSAnnot $1 } @@ -426,6 +436,7 @@ Identifier : 'ident' { AST.JSIdentifier (mkJSAnnot $1) (tokenLiteral $1) } -- TODO: make this include any reserved word too, including future ones IdentifierName :: { AST.JSExpression } IdentifierName : Identifier {$1} + | 'as' { AST.JSIdentifier (mkJSAnnot $1) "as" } | 'break' { AST.JSIdentifier (mkJSAnnot $1) "break" } | 'case' { AST.JSIdentifier (mkJSAnnot $1) "case" } | 'catch' { AST.JSIdentifier (mkJSAnnot $1) "catch" } @@ -437,6 +448,7 @@ IdentifierName : Identifier {$1} | 'do' { AST.JSIdentifier (mkJSAnnot $1) "do" } | 'else' { AST.JSIdentifier (mkJSAnnot $1) "else" } | 'enum' { AST.JSIdentifier (mkJSAnnot $1) "enum" } + | 'export' { AST.JSIdentifier (mkJSAnnot $1) "export" } | 'false' { AST.JSIdentifier (mkJSAnnot $1) "false" } | 'finally' { AST.JSIdentifier (mkJSAnnot $1) "finally" } | 'for' { AST.JSIdentifier (mkJSAnnot $1) "for" } @@ -498,9 +510,9 @@ Elision : Comma { [AST.JSArrayComma $1] {- 'Elision1' -} } -- { PropertyNameAndValueList } -- { PropertyNameAndValueList , } ObjectLiteral :: { AST.JSExpression } -ObjectLiteral : LBrace RBrace { AST.JSObjectLiteral $1 (AST.JSCTLNone AST.JSLNil) $2 {- 'ObjectLiteal1' -} } - | LBrace PropertyNameandValueList RBrace { AST.JSObjectLiteral $1 (AST.JSCTLNone $2) $3 {- 'ObjectLiteal2' -} } - | LBrace PropertyNameandValueList Comma RBrace { AST.JSObjectLiteral $1 (AST.JSCTLComma $2 $3) $4 {- 'ObjectLiteal3' -} } +ObjectLiteral : LBrace RBrace { AST.JSObjectLiteral $1 (AST.JSCTLNone AST.JSLNil) $2 {- 'ObjectLiteral1' -} } + | LBrace PropertyNameandValueList RBrace { AST.JSObjectLiteral $1 (AST.JSCTLNone $2) $3 {- 'ObjectLiteral2' -} } + | LBrace PropertyNameandValueList Comma RBrace { AST.JSObjectLiteral $1 (AST.JSCTLComma $2 $3) $4 {- 'ObjectLiteral3' -} } -- ::= ':' -- | ',' ':' @@ -1108,6 +1120,11 @@ StatementOrBlock :: { AST.JSStatement } StatementOrBlock : Block MaybeSemi { blockToStatement $1 $2 } | Expression MaybeSemi { expressionToStatement $1 $2 } +-- StatementListItem : +-- Statement +-- Declaration +StatementListItem :: { AST.JSStatement } +StatementListItem : Statement { $1 } NamedFunctionExpression :: { AST.JSExpression } NamedFunctionExpression : Function Identifier LParen RParen FunctionBody @@ -1145,6 +1162,75 @@ Program :: { AST.JSAST } Program : StatementList Eof { AST.JSAstProgram $1 $2 {- 'Program1' -} } | Eof { AST.JSAstProgram [] $1 {- 'Program2' -} } +-- Module : See 15.2 +-- ModuleBody[opt] +-- +-- ModuleBody : +-- ModuleItemList +Module :: { AST.JSAST } +Module : ModuleItemList Eof { AST.JSAstModule $1 $2 {- 'Module1' -} } + | Eof { AST.JSAstModule [] $1 {- 'Module2' -} } + +-- ModuleItemList : +-- ModuleItem +-- ModuleItemList ModuleItem +ModuleItemList :: { [AST.JSModuleItem] } +ModuleItemList : ModuleItem { [$1] {- 'ModuleItemList1' -} } + | ModuleItemList ModuleItem { ($1++[$2]) {- 'ModuleItemList2' -} } + +-- ModuleItem : +-- ImportDeclaration +-- ExportDeclaration +-- StatementListItem +ModuleItem :: { AST.JSModuleItem } +ModuleItem : Export ExportDeclaration + { AST.JSModuleExportDeclaration $1 $2 {- 'ModuleItem1' -} } + | StatementListItem + { AST.JSModuleStatementListItem $1 {- 'ModuleItem2' -} } + +-- ExportDeclaration : See 15.2.3 +-- [ ] export * FromClause ; +-- [ ] export ExportClause FromClause ; +-- [x] export ExportClause ; +-- [x] export VariableStatement +-- [ ] export Declaration +-- [ ] export default HoistableDeclaration[Default] +-- [ ] export default ClassDeclaration[Default] +-- [ ] export default [lookahead ∉ { function, class }] AssignmentExpression[In] ; +ExportDeclaration :: { AST.JSExportDeclaration } +ExportDeclaration : ExportClause AutoSemi + { $1 {- 'ExportDeclaration1' -} } + | VariableStatement AutoSemi + { AST.JSExport $1 $2 {- 'ExportDeclaration2' -} } + +-- ExportClause : +-- { } +-- { ExportsList } +-- { ExportsList , } +ExportClause :: { AST.JSExportDeclaration } +ExportClause : LBrace RBrace AutoSemi + { AST.JSExportLocals $1 AST.JSLNil $2 $3 {- 'ExportClause1' -} } + | LBrace ExportsList RBrace AutoSemi + { AST.JSExportLocals $1 $2 $3 $4 {- 'ExportClause2' -} } + +-- ExportsList : +-- ExportSpecifier +-- ExportsList , ExportSpecifier +ExportsList :: { AST.JSCommaList AST.JSExportLocalSpecifier } +ExportsList : ExportSpecifier + { AST.JSLOne $1 {- 'ExportsList1' -} } + | ExportsList Comma ExportSpecifier + { AST.JSLCons $1 $2 $3 {- 'ExportsList2' -} } + +-- ExportSpecifier : +-- IdentifierName +-- IdentifierName as IdentifierName +ExportSpecifier :: { AST.JSExportLocalSpecifier } +ExportSpecifier : IdentifierName + { AST.JSExportLocalSpecifier (identName $1) {- 'ExportSpecifier1' -} } + | IdentifierName As IdentifierName + { AST.JSExportLocalSpecifierAs (identName $1) $2 (identName $3) {- 'ExportSpecifier2' -} } + -- For debugging/other entry points LiteralMain :: { AST.JSAST } LiteralMain : Literal Eof { AST.JSAstLiteral $1 $2 {- 'LiteralMain' -} } @@ -1155,7 +1241,6 @@ ExpressionMain : Expression Eof { AST.JSAstExpression $1 $2 {- 'ExpressionMa StatementMain :: { AST.JSAST } StatementMain : StatementNoEmpty Eof { AST.JSAstStatement $1 $2 {- 'StatementMain' -} } - { -- Need this type while build the AST, but is not actually part of the AST. diff --git a/src/Language/JavaScript/Parser/Lexer.x b/src/Language/JavaScript/Parser/Lexer.x index 723c21b..863fabd 100644 --- a/src/Language/JavaScript/Parser/Lexer.x +++ b/src/Language/JavaScript/Parser/Lexer.x @@ -503,7 +503,8 @@ keywords = Map.fromList keywordNames keywordNames :: [(String, TokenPosn -> String -> [CommentAnnotation] -> Token)] keywordNames = - [ ( "break", BreakToken ) + [ ( "as", AsToken ) + , ( "break", BreakToken ) , ( "case", CaseToken ) , ( "catch", CatchToken ) @@ -517,6 +518,7 @@ keywordNames = , ( "else", ElseToken ) , ( "enum", EnumToken ) -- not a keyword, nominally a future reserved word, but actually in use + , ( "export", ExportToken ) , ( "false", FalseToken ) -- boolean literal @@ -563,7 +565,6 @@ keywordNames = -- ( "code", FutureToken ) **** not any more -- ( "const", FutureToken ) **** an actual token, used in productions -- enum **** an actual token, used in productions - , ( "export", FutureToken ) , ( "extends", FutureToken ) , ( "import", FutureToken ) diff --git a/src/Language/JavaScript/Parser/Parser.hs b/src/Language/JavaScript/Parser/Parser.hs index d36b5bc..10757a3 100644 --- a/src/Language/JavaScript/Parser/Parser.hs +++ b/src/Language/JavaScript/Parser/Parser.hs @@ -1,7 +1,9 @@ module Language.JavaScript.Parser.Parser ( -- * Parsing parse + , parseModule , readJs + , readJsModule -- , readJsKeepComments , parseFile , parseFileUtf8 @@ -12,28 +14,44 @@ module Language.JavaScript.Parser.Parser ( , showStrippedMaybe ) where -import Language.JavaScript.Parser.Grammar7 +import qualified Language.JavaScript.Parser.Grammar7 as P import Language.JavaScript.Parser.Lexer import qualified Language.JavaScript.Parser.AST as AST import System.IO --- | Parse one compound statement, or a sequence of simple statements. +-- | Parse JavaScript Program (Script) +-- Parse one compound statement, or a sequence of simple statements. -- Generally used for interactive input, such as from the command line of an interpreter. -- Return comments in addition to the parsed statements. parse :: String -- ^ The input stream (Javascript source code). -> String -- ^ The name of the Javascript source (filename or input device). - -> Either String AST.JSAST + -> Either String AST.JSAST -- ^ An error or maybe the abstract syntax tree (AST) of zero -- or more Javascript statements, plus comments. -parse input _srcName = runAlex input parseProgram +parse = parseUsing P.parseProgram +-- | Parse JavaScript module +parseModule :: String -- ^ The input stream (JavaScript source code). + -> String -- ^ The name of the JavaScript source (filename or input device). + -> Either String AST.JSAST + -- ^ An error or maybe the abstract syntax tree (AST) of zero + -- or more JavaScript statements, plus comments. +parseModule = parseUsing P.parseModule -readJs :: String -> AST.JSAST -readJs input = - case parse input "src" of +readJsWith :: (String -> String -> Either String AST.JSAST) + -> String + -> AST.JSAST +readJsWith f input = + case f input "src" of Left msg -> error (show msg) Right p -> p +readJs :: String -> AST.JSAST +readJs = readJsWith parse + +readJsModule :: String -> AST.JSAST +readJsModule = readJsWith parseModule + -- | Parse the given file. -- For UTF-8 support, make sure your locale is set such that -- "System.IO.localeEncoding" returns "utf8" @@ -74,4 +92,3 @@ parseUsing :: -- or more Javascript statements, plus comments. parseUsing p input _srcName = runAlex input p - diff --git a/src/Language/JavaScript/Parser/Token.hs b/src/Language/JavaScript/Parser/Token.hs index b2f7c4d..4a5a97c 100644 --- a/src/Language/JavaScript/Parser/Token.hs +++ b/src/Language/JavaScript/Parser/Token.hs @@ -56,6 +56,7 @@ data Token -- ^ Literal: Regular Expression -- Keywords + | AsToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | BreakToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | CaseToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | CatchToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } @@ -88,6 +89,7 @@ data Token | VoidToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | WhileToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | WithToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } + | ExportToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } -- Future reserved words | FutureToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } -- Needed, not sure what they are though. diff --git a/src/Language/JavaScript/Pretty/Printer.hs b/src/Language/JavaScript/Pretty/Printer.hs index fd9dc89..3ccc30c 100644 --- a/src/Language/JavaScript/Pretty/Printer.hs +++ b/src/Language/JavaScript/Pretty/Printer.hs @@ -55,6 +55,7 @@ class RenderJS a where instance RenderJS JSAST where (|>) pacc (JSAstProgram xs a) = pacc |> xs |> a + (|>) pacc (JSAstModule xs a) = pacc |> xs |> a (|>) pacc (JSAstStatement s a) = pacc |> s |> a (|>) pacc (JSAstExpression e a) = pacc |> e |> a (|>) pacc (JSAstLiteral x a) = pacc |> x |> a @@ -138,6 +139,7 @@ instance RenderJS [JSExpression] where instance RenderJS JSBinOp where (|>) pacc (JSBinOpAnd annot) = pacc |> annot |> "&&" + (|>) pacc (JSBinOpAs annot) = pacc |> annot |> "as" (|>) pacc (JSBinOpBitAnd annot) = pacc |> annot |> "&" (|>) pacc (JSBinOpBitOr annot) = pacc |> annot |> "|" (|>) pacc (JSBinOpBitXor annot) = pacc |> annot |> "^" @@ -242,6 +244,13 @@ instance RenderJS JSStatement where instance RenderJS [JSStatement] where (|>) = foldl' (|>) +instance RenderJS [JSModuleItem] where + (|>) = foldl' (|>) + +instance RenderJS JSModuleItem where + (|>) pacc (JSModuleExportDeclaration annot decl) = pacc |> annot |> "export" |> decl + (|>) pacc (JSModuleStatementListItem s) = pacc |> s + instance RenderJS JSBlock where (|>) pacc (JSBlock alb ss arb) = pacc |> alb |> "{" |> ss |> arb |> "}" @@ -265,6 +274,15 @@ instance RenderJS JSArrayElement where instance RenderJS [JSArrayElement] where (|>) = foldl' (|>) +instance RenderJS JSExportDeclaration where + (|>) pacc (JSExport x1 s) = pacc |> " " |> x1 |> s + (|>) pacc (JSExportLocals alb JSLNil arb semi) = pacc |> alb |> "{" |> arb |> "}" |> semi + (|>) pacc (JSExportLocals alb s arb semi) = pacc |> alb |> "{" |> s |> arb |> "}" |> semi + +instance RenderJS JSExportLocalSpecifier where + (|>) pacc (JSExportLocalSpecifier i) = pacc |> i + (|>) pacc (JSExportLocalSpecifierAs x1 as x2) = pacc |> x1 |> as |> x2 + instance RenderJS a => RenderJS (JSCommaList a) where (|>) pacc (JSLCons pl a i) = pacc |> pl |> a |> "," |> i (|>) pacc (JSLOne i) = pacc |> i @@ -287,4 +305,3 @@ instance RenderJS JSVarInitializer where (|>) pacc JSVarInitNone = pacc -- EOF - diff --git a/src/Language/JavaScript/Process/Minify.hs b/src/Language/JavaScript/Process/Minify.hs index f4babe3..7e16b99 100644 --- a/src/Language/JavaScript/Process/Minify.hs +++ b/src/Language/JavaScript/Process/Minify.hs @@ -15,6 +15,7 @@ import Language.JavaScript.Parser.Token minifyJS :: JSAST -> JSAST minifyJS (JSAstProgram xs _) = JSAstProgram (fixStatementList noSemi xs) emptyAnnot +minifyJS (JSAstModule xs _) = JSAstModule (map (fix emptyAnnot) xs) emptyAnnot minifyJS (JSAstStatement (JSStatementBlock _ [s] _ _) _) = JSAstStatement (fixStmtE noSemi s) emptyAnnot minifyJS (JSAstStatement s _) = JSAstStatement (fixStmtE noSemi s) emptyAnnot minifyJS (JSAstExpression e _) = JSAstExpression (fixEmpty e) emptyAnnot @@ -209,6 +210,7 @@ normalizeToSQ str = instance MinifyJS JSBinOp where fix _ (JSBinOpAnd _) = JSBinOpAnd emptyAnnot + fix a (JSBinOpAs _) = JSBinOpAs a fix _ (JSBinOpBitAnd _) = JSBinOpBitAnd emptyAnnot fix _ (JSBinOpBitOr _) = JSBinOpBitOr emptyAnnot fix _ (JSBinOpBitXor _) = JSBinOpBitXor emptyAnnot @@ -265,6 +267,17 @@ instance MinifyJS JSAssignOp where fix a (JSBwXorAssign _) = JSBwXorAssign a fix a (JSBwOrAssign _) = JSBwOrAssign a +instance MinifyJS JSModuleItem where + fix _ (JSModuleExportDeclaration _ x1) = JSModuleExportDeclaration emptyAnnot (fixEmpty x1) + fix a (JSModuleStatementListItem s) = JSModuleStatementListItem (fixStmt a noSemi s) + +instance MinifyJS JSExportDeclaration where + fix _ (JSExportLocals _ x1 _ _) = JSExportLocals emptyAnnot (fixEmpty x1) emptyAnnot noSemi + fix _ (JSExport x1 _) = JSExport (fixStmt emptyAnnot noSemi x1) noSemi + +instance MinifyJS JSExportLocalSpecifier where + fix _ (JSExportLocalSpecifier x1) = JSExportLocalSpecifier (fixEmpty x1) + fix _ (JSExportLocalSpecifierAs x1 as x2) = JSExportLocalSpecifierAs (fixEmpty x1) (fixSpace as) (fixSpace x2) instance MinifyJS JSTryCatch where fix a (JSCatch _ _ x1 _ x3) = JSCatch a emptyAnnot (fixEmpty x1) emptyAnnot (fixEmpty x3) diff --git a/test/Test/Language/Javascript/Minify.hs b/test/Test/Language/Javascript/Minify.hs index 2e63b7e..f8f8eca 100644 --- a/test/Test/Language/Javascript/Minify.hs +++ b/test/Test/Language/Javascript/Minify.hs @@ -2,15 +2,18 @@ module Test.Language.Javascript.Minify ( testMinifyExpr , testMinifyStmt , testMinifyProg + , testMinifyModule ) where import Control.Monad (forM_) import Test.Hspec -import Language.JavaScript.Parser +import Language.JavaScript.Parser hiding (parseModule) import Language.JavaScript.Parser.Grammar7 -import Language.JavaScript.Parser.Parser +import Language.JavaScript.Parser.Lexer (Alex) +import Language.JavaScript.Parser.Parser hiding (parseModule) import Language.JavaScript.Process.Minify +import qualified Language.JavaScript.Parser.AST as AST testMinifyExpr :: Spec @@ -251,14 +254,29 @@ testMinifyProg = describe "Minify programs:" $ do it "try/catch/finally" $ minifyProg " try { } catch (a) {} finally {} ; try { } catch ( b ) { } ; " `shouldBe` "try{}catch(a){}finally{}try{}catch(b){}" +testMinifyModule :: Spec +testMinifyModule = describe "Minify modules:" $ + it "export" $ do + minifyModule " export { } ; " `shouldBe` "export{}" + minifyModule " export { a } ; " `shouldBe` "export{a}" + minifyModule " export { a, b } ; " `shouldBe` "export{a,b}" + minifyModule " export { a, b as c , d } ; " `shouldBe` "export{a,b as c,d}" + minifyModule " export const a = 1 ; " `shouldBe` "export const a=1" + -- ----------------------------------------------------------------------------- -- Minify test helpers. minifyExpr :: String -> String -minifyExpr str = either id (renderToString . minifyJS) (parseUsing parseExpression str "src") +minifyExpr = minifyWith parseExpression minifyStmt :: String -> String -minifyStmt str = either id (renderToString . minifyJS) (parseUsing parseStatement str "src") +minifyStmt = minifyWith parseStatement minifyProg :: String -> String -minifyProg str = either id (renderToString . minifyJS) (parseUsing parseProgram str "src") +minifyProg = minifyWith parseProgram + +minifyModule :: String -> String +minifyModule = minifyWith parseModule + +minifyWith :: (Alex AST.JSAST) -> String -> String +minifyWith p str = either id (renderToString . minifyJS) (parseUsing p str "src") diff --git a/test/Test/Language/Javascript/ModuleParser.hs b/test/Test/Language/Javascript/ModuleParser.hs new file mode 100644 index 0000000..9b9dfad --- /dev/null +++ b/test/Test/Language/Javascript/ModuleParser.hs @@ -0,0 +1,30 @@ +module Test.Language.Javascript.ModuleParser + ( testModuleParser + ) where + +import Test.Hspec + +import Language.JavaScript.Parser + + +testModuleParser :: Spec +testModuleParser = describe "Parse modules:" $ + it "export" $ do + test "export {}" + `shouldBe` + "Right (JSAstModule [JSModuleExportDeclaration (JSExportLocals (()))])" + test "export {};" + `shouldBe` + "Right (JSAstModule [JSModuleExportDeclaration (JSExportLocals (()))])" + test "export const a = 1;" + `shouldBe` + "Right (JSAstModule [JSModuleExportDeclaration (JSExport (JSConstant (JSVarInitExpression (JSIdentifier 'a') [JSDecimal '1'])))])" + test "export { a };" + `shouldBe` + "Right (JSAstModule [JSModuleExportDeclaration (JSExportLocals ((JSExportLocalSpecifier (JSIdentifier 'a'))))])" + test "export { a as b };" + `shouldBe` + "Right (JSAstModule [JSModuleExportDeclaration (JSExportLocals ((JSExportLocalSpecifierAs (JSIdentifier 'a',JSIdentifier 'b'))))])" + +test :: String -> String +test str = showStrippedMaybe (parseModule str "src") diff --git a/test/Test/Language/Javascript/RoundTrip.hs b/test/Test/Language/Javascript/RoundTrip.hs index b55efb0..2e2704d 100644 --- a/test/Test/Language/Javascript/RoundTrip.hs +++ b/test/Test/Language/Javascript/RoundTrip.hs @@ -5,6 +5,7 @@ module Test.Language.Javascript.RoundTrip import Test.Hspec import Language.JavaScript.Parser +import qualified Language.JavaScript.Parser.AST as AST testRoundTrip :: Spec @@ -99,7 +100,18 @@ testRoundTrip = describe "Roundtrip:" $ do testRT "switch (x) {default:\ncase 1:break;}" testRT "var x=1;let y=2;" + it "module" $ do + testRTModule "export {};" + testRTModule " export {} ; " + testRTModule "export { a , b , c };" + testRTModule "export { a, X as B, c }" + testRT :: String -> Expectation -testRT str = str `shouldBe` renderToString (readJs str) +testRT = testRTWith readJs + +testRTModule :: String -> Expectation +testRTModule = testRTWith readJsModule +testRTWith :: (String -> AST.JSAST) -> String -> Expectation +testRTWith f str = renderToString (f str) `shouldBe` str diff --git a/test/Test/Language/Javascript/StatementParser.hs b/test/Test/Language/Javascript/StatementParser.hs index e27addf..8f3a7fa 100644 --- a/test/Test/Language/Javascript/StatementParser.hs +++ b/test/Test/Language/Javascript/StatementParser.hs @@ -16,6 +16,7 @@ testStatementParser = describe "Parse statements:" $ do testStmt "x" `shouldBe` "Right (JSAstStatement (JSIdentifier 'x'))" testStmt "null" `shouldBe` "Right (JSAstStatement (JSLiteral 'null'))" testStmt "true?1:2" `shouldBe` "Right (JSAstStatement (JSExpressionTernary (JSLiteral 'true',JSDecimal '1',JSDecimal '2')))" + it "block" $ do testStmt "{}" `shouldBe` "Right (JSAstStatement (JSStatementBlock []))" testStmt "{x=1}" `shouldBe` "Right (JSAstStatement (JSStatementBlock [JSOpAssign ('=',JSIdentifier 'x',JSDecimal '1')]))" @@ -101,4 +102,3 @@ testStatementParser = describe "Parse statements:" $ do testStmt :: String -> String testStmt str = showStrippedMaybe (parseUsing parseStatement str "src") - diff --git a/test/testsuite.hs b/test/testsuite.hs index 7ba14d2..fac4d8d 100644 --- a/test/testsuite.hs +++ b/test/testsuite.hs @@ -5,10 +5,11 @@ import Test.Hspec import Test.Hspec.Runner +import Test.Language.Javascript.ExpressionParser import Test.Language.Javascript.Lexer import Test.Language.Javascript.LiteralParser -import Test.Language.Javascript.ExpressionParser import Test.Language.Javascript.Minify +import Test.Language.Javascript.ModuleParser import Test.Language.Javascript.ProgramParser import Test.Language.Javascript.RoundTrip import Test.Language.Javascript.StatementParser @@ -29,8 +30,9 @@ testAll = do testExpressionParser testStatementParser testProgramParser + testModuleParser testRoundTrip testMinifyExpr testMinifyStmt testMinifyProg - + testMinifyModule