diff --git a/src/Language/JavaScript/Parser/AST.hs b/src/Language/JavaScript/Parser/AST.hs index 9e157db..5bd25b3 100644 --- a/src/Language/JavaScript/Parser/AST.hs +++ b/src/Language/JavaScript/Parser/AST.hs @@ -25,6 +25,12 @@ module Language.JavaScript.Parser.AST -- Modules , JSModuleItem (..) + , JSImportDeclaration (..) + , JSImportClause (..) + , JSFromClause (..) + , JSImportNameSpace (..) + , JSImportsNamed (..) + , JSImportSpecifier (..) , JSExportDeclaration (..) , JSExportLocalSpecifier (..) @@ -57,11 +63,46 @@ data JSAST -- Shift AST -- https://github.com/shapesecurity/shift-spec/blob/83498b92c436180cc0e2115b225a68c08f43c53e/spec.idl#L229-L234 data JSModuleItem - -- = JSImportDeclaration - = JSModuleExportDeclaration !JSAnnot !JSExportDeclaration -- ^export,decl + = JSModuleImportDeclaration !JSAnnot !JSImportDeclaration -- ^import,decl + | JSModuleExportDeclaration !JSAnnot !JSExportDeclaration -- ^export,decl | JSModuleStatementListItem !JSStatement deriving (Data, Eq, Show, Typeable) +data JSImportDeclaration + = JSImportDeclaration !JSImportClause !JSFromClause !JSSemi -- ^imports, module, semi + -- | JSImportDeclarationBare -- ^ module, semi + deriving (Data, Eq, Show, Typeable) + +data JSImportClause + = JSImportClauseDefault !JSIdent -- ^default + | JSImportClauseNameSpace !JSImportNameSpace -- ^namespace + | JSImportClauseNamed !JSImportsNamed -- ^named imports + | JSImportClauseDefaultNameSpace !JSIdent !JSAnnot !JSImportNameSpace -- ^default, comma, namespace + | JSImportClauseDefaultNamed !JSIdent !JSAnnot !JSImportsNamed -- ^default, comma, named imports + deriving (Data, Eq, Show, Typeable) + +data JSFromClause + = JSFromClause !JSAnnot !JSAnnot !String -- ^ from, string literal, string literal contents + deriving (Data, Eq, Show, Typeable) + +-- | Import namespace, e.g. '* as whatever' +data JSImportNameSpace + = JSImportNameSpace !JSBinOp !JSBinOp !JSIdent -- ^ *, as, ident + deriving (Data, Eq, Show, Typeable) + +-- | Named imports, e.g. '{ foo, bar, baz as quux }' +data JSImportsNamed + = JSImportsNamed !JSAnnot !(JSCommaList JSImportSpecifier) !JSAnnot -- ^lb, specifiers, rb + deriving (Data, Eq, Show, Typeable) + +-- | +-- Note that this data type is separate from ExportSpecifier because the +-- grammar is slightly different (e.g. in handling of reserved words). +data JSImportSpecifier + = JSImportSpecifier !JSIdent -- ^ident + | JSImportSpecifierAs !JSIdent !JSBinOp !JSIdent -- ^ident, as, ident + deriving (Data, Eq, Show, Typeable) + data JSExportDeclaration -- = JSExportAllFrom -- | JSExportFrom @@ -338,8 +379,32 @@ instance ShowStripped JSExpression where instance ShowStripped JSModuleItem where ss (JSModuleExportDeclaration _ x1) = "JSModuleExportDeclaration (" ++ ss x1 ++ ")" + ss (JSModuleImportDeclaration _ x1) = "JSModuleImportDeclaration (" ++ ss x1 ++ ")" ss (JSModuleStatementListItem x1) = "JSModuleStatementListItem (" ++ ss x1 ++ ")" +instance ShowStripped JSImportDeclaration where + ss (JSImportDeclaration imp from _) = "JSImportDeclaration (" ++ ss imp ++ "," ++ ss from ++ ")" + +instance ShowStripped JSImportClause where + ss (JSImportClauseDefault x) = "JSImportClauseDefault (" ++ ss x ++ ")" + ss (JSImportClauseNameSpace x) = "JSImportClauseNameSpace (" ++ ss x ++ ")" + ss (JSImportClauseNamed x) = "JSImportClauseNameSpace (" ++ ss x ++ ")" + ss (JSImportClauseDefaultNameSpace x1 _ x2) = "JSImportClauseDefaultNameSpace (" ++ ss x1 ++ "," ++ ss x2 ++ ")" + ss (JSImportClauseDefaultNamed x1 _ x2) = "JSImportClauseDefaultNamed (" ++ ss x1 ++ "," ++ ss x2 ++ ")" + +instance ShowStripped JSFromClause where + ss (JSFromClause _ _ m) = "JSFromClause " ++ singleQuote m + +instance ShowStripped JSImportNameSpace where + ss (JSImportNameSpace _ _ x) = "JSImportNameSpace (" ++ ss x ++ ")" + +instance ShowStripped JSImportsNamed where + ss (JSImportsNamed _ xs _) = "JSImportsNamed (" ++ ss xs ++ ")" + +instance ShowStripped JSImportSpecifier where + ss (JSImportSpecifier x1) = "JSImportSpecifier (" ++ ss x1 ++ ")" + ss (JSImportSpecifierAs x1 _ x2) = "JSImportSpecifierAs (" ++ ss x1 ++ "," ++ ss x2 ++ ")" + instance ShowStripped JSExportDeclaration where ss (JSExportLocals _ xs _ _) = "JSExportLocals (" ++ ss xs ++ ")" ss (JSExport x1 _) = "JSExport (" ++ ss x1 ++ ")" diff --git a/src/Language/JavaScript/Parser/Grammar7.y b/src/Language/JavaScript/Parser/Grammar7.y index f2e9406..fa5e62d 100644 --- a/src/Language/JavaScript/Parser/Grammar7.y +++ b/src/Language/JavaScript/Parser/Grammar7.y @@ -101,8 +101,10 @@ import qualified Language.JavaScript.Parser.AST as AST 'finally' { FinallyToken {} } 'for' { ForToken {} } 'function' { FunctionToken {} } + 'from' { FromToken {} } 'get' { GetToken {} } 'if' { IfToken {} } + 'import' { ImportToken {} } 'in' { InToken {} } 'instanceof' { InstanceofToken {} } 'let' { LetToken {} } @@ -312,6 +314,12 @@ Let : 'let' { mkJSAnnot $1 } Const :: { AST.JSAnnot } Const : 'const' { mkJSAnnot $1 } +Import :: { AST.JSAnnot } +Import : 'import' { mkJSAnnot $1 } + +From :: { AST.JSAnnot } +From : 'from' { mkJSAnnot $1 } + Export :: { AST.JSAnnot } Export : 'export' { mkJSAnnot $1 } @@ -432,6 +440,7 @@ Identifier :: { AST.JSExpression } Identifier : 'ident' { AST.JSIdentifier (mkJSAnnot $1) (tokenLiteral $1) } | 'get' { AST.JSIdentifier (mkJSAnnot $1) "get" } | 'set' { AST.JSIdentifier (mkJSAnnot $1) "set" } + | 'from' { AST.JSIdentifier (mkJSAnnot $1) "from" } -- TODO: make this include any reserved word too, including future ones IdentifierName :: { AST.JSExpression } @@ -453,6 +462,7 @@ IdentifierName : Identifier {$1} | 'finally' { AST.JSIdentifier (mkJSAnnot $1) "finally" } | 'for' { AST.JSIdentifier (mkJSAnnot $1) "for" } | 'function' { AST.JSIdentifier (mkJSAnnot $1) "function" } + | 'from' { AST.JSIdentifier (mkJSAnnot $1) "from" } | 'get' { AST.JSIdentifier (mkJSAnnot $1) "get" } | 'if' { AST.JSIdentifier (mkJSAnnot $1) "if" } | 'in' { AST.JSIdentifier (mkJSAnnot $1) "in" } @@ -1183,11 +1193,53 @@ ModuleItemList : ModuleItem { [$1] {- 'ModuleItemList1' -- ExportDeclaration -- StatementListItem ModuleItem :: { AST.JSModuleItem } -ModuleItem : Export ExportDeclaration +ModuleItem : Import ImportDeclaration + { AST.JSModuleImportDeclaration $1 $2 {- 'ModuleItem1' -} } + | Export ExportDeclaration { AST.JSModuleExportDeclaration $1 $2 {- 'ModuleItem1' -} } | StatementListItem { AST.JSModuleStatementListItem $1 {- 'ModuleItem2' -} } +ImportDeclaration :: { AST.JSImportDeclaration } +ImportDeclaration : ImportClause FromClause AutoSemi + { AST.JSImportDeclaration $1 $2 $3 } + +ImportClause :: { AST.JSImportClause } +ImportClause : IdentifierName + { AST.JSImportClauseDefault (identName $1) } + | NameSpaceImport + { AST.JSImportClauseNameSpace $1 } + | NamedImports + { AST.JSImportClauseNamed $1 } + | IdentifierName ',' NameSpaceImport + { AST.JSImportClauseDefaultNameSpace (identName $1) (mkJSAnnot $2) $3 } + | IdentifierName ',' NamedImports + { AST.JSImportClauseDefaultNamed (identName $1) (mkJSAnnot $2) $3 } + +FromClause :: { AST.JSFromClause } +FromClause : From 'string' + { AST.JSFromClause $1 (mkJSAnnot $2) (tokenLiteral $2) } + +NameSpaceImport :: { AST.JSImportNameSpace } +NameSpaceImport : Mul As IdentifierName + { AST.JSImportNameSpace $1 $2 (identName $3) } + +NamedImports :: { AST.JSImportsNamed } +NamedImports : LBrace ImportsList RBrace + { AST.JSImportsNamed $1 $2 $3 } + +ImportsList :: { AST.JSCommaList AST.JSImportSpecifier } +ImportsList : ImportSpecifier + { AST.JSLOne $1 } + | ImportsList Comma ImportSpecifier + { AST.JSLCons $1 $2 $3 } + +ImportSpecifier :: { AST.JSImportSpecifier } +ImportSpecifier : IdentifierName + { AST.JSImportSpecifier (identName $1) } + | IdentifierName As IdentifierName + { AST.JSImportSpecifierAs (identName $1) $2 (identName $3) } + -- ExportDeclaration : See 15.2.3 -- [ ] export * FromClause ; -- [ ] export ExportClause FromClause ; diff --git a/src/Language/JavaScript/Parser/Lexer.x b/src/Language/JavaScript/Parser/Lexer.x index 863fabd..43aaf7e 100644 --- a/src/Language/JavaScript/Parser/Lexer.x +++ b/src/Language/JavaScript/Parser/Lexer.x @@ -525,7 +525,9 @@ keywordNames = , ( "finally", FinallyToken ) , ( "for", ForToken ) , ( "function", FunctionToken ) + , ( "from", FromToken ) , ( "if", IfToken ) + , ( "import", ImportToken ) , ( "in", InToken ) , ( "instanceof", InstanceofToken ) , ( "let", LetToken ) @@ -566,8 +568,6 @@ keywordNames = -- ( "const", FutureToken ) **** an actual token, used in productions -- enum **** an actual token, used in productions , ( "extends", FutureToken ) - - , ( "import", FutureToken ) , ( "super", FutureToken ) diff --git a/src/Language/JavaScript/Parser/Token.hs b/src/Language/JavaScript/Parser/Token.hs index 4a5a97c..7de69bd 100644 --- a/src/Language/JavaScript/Parser/Token.hs +++ b/src/Language/JavaScript/Parser/Token.hs @@ -73,6 +73,7 @@ data Token | FinallyToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | ForToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | FunctionToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } + | FromToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | IfToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | InToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | InstanceofToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } @@ -88,6 +89,7 @@ data Token | VarToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | VoidToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | WhileToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } + | ImportToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | WithToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } | ExportToken { tokenSpan :: !TokenPosn, tokenLiteral :: !String, tokenComment :: ![CommentAnnotation] } -- Future reserved words diff --git a/src/Language/JavaScript/Pretty/Printer.hs b/src/Language/JavaScript/Pretty/Printer.hs index 3ccc30c..7c360c2 100644 --- a/src/Language/JavaScript/Pretty/Printer.hs +++ b/src/Language/JavaScript/Pretty/Printer.hs @@ -248,6 +248,7 @@ instance RenderJS [JSModuleItem] where (|>) = foldl' (|>) instance RenderJS JSModuleItem where + (|>) pacc (JSModuleImportDeclaration annot decl) = pacc |> annot |> "import" |> decl (|>) pacc (JSModuleExportDeclaration annot decl) = pacc |> annot |> "export" |> decl (|>) pacc (JSModuleStatementListItem s) = pacc |> s @@ -274,6 +275,29 @@ instance RenderJS JSArrayElement where instance RenderJS [JSArrayElement] where (|>) = foldl' (|>) +instance RenderJS JSImportDeclaration where + (|>) pacc (JSImportDeclaration imp from annot) = pacc |> imp |> from |> annot + +instance RenderJS JSImportClause where + (|>) pacc (JSImportClauseDefault x) = pacc |> x + (|>) pacc (JSImportClauseNameSpace x) = pacc |> x + (|>) pacc (JSImportClauseNamed x) = pacc |> x + (|>) pacc (JSImportClauseDefaultNameSpace x1 annot x2) = pacc |> x1 |> annot |> "," |> x2 + (|>) pacc (JSImportClauseDefaultNamed x1 annot x2) = pacc |> x1 |> annot |> "," |> x2 + +instance RenderJS JSFromClause where + (|>) pacc (JSFromClause from annot m) = pacc |> from |> "from" |> annot |> m + +instance RenderJS JSImportNameSpace where + (|>) pacc (JSImportNameSpace star as x) = pacc |> star |> as |> x + +instance RenderJS JSImportsNamed where + (|>) pacc (JSImportsNamed lb xs rb) = pacc |> lb |> "{" |> xs |> rb |> "}" + +instance RenderJS JSImportSpecifier where + (|>) pacc (JSImportSpecifier x1) = pacc |> x1 + (|>) pacc (JSImportSpecifierAs x1 as x2) = pacc |> x1 |> as |> x2 + instance RenderJS JSExportDeclaration where (|>) pacc (JSExport x1 s) = pacc |> " " |> x1 |> s (|>) pacc (JSExportLocals alb JSLNil arb semi) = pacc |> alb |> "{" |> arb |> "}" |> semi diff --git a/src/Language/JavaScript/Process/Minify.hs b/src/Language/JavaScript/Process/Minify.hs index 7e16b99..a2d27f5 100644 --- a/src/Language/JavaScript/Process/Minify.hs +++ b/src/Language/JavaScript/Process/Minify.hs @@ -268,9 +268,40 @@ instance MinifyJS JSAssignOp where fix a (JSBwOrAssign _) = JSBwOrAssign a instance MinifyJS JSModuleItem where + fix _ (JSModuleImportDeclaration _ x1) = JSModuleImportDeclaration emptyAnnot (fixEmpty x1) fix _ (JSModuleExportDeclaration _ x1) = JSModuleExportDeclaration emptyAnnot (fixEmpty x1) fix a (JSModuleStatementListItem s) = JSModuleStatementListItem (fixStmt a noSemi s) +instance MinifyJS JSImportDeclaration where + fix _ (JSImportDeclaration imps from _) = JSImportDeclaration (fixEmpty imps) (fix annot from) noSemi + where + annot = case imps of + JSImportClauseDefault {} -> spaceAnnot + JSImportClauseNameSpace {} -> spaceAnnot + JSImportClauseNamed {} -> emptyAnnot + JSImportClauseDefaultNameSpace {} -> spaceAnnot + JSImportClauseDefaultNamed {} -> emptyAnnot + +instance MinifyJS JSImportClause where + fix _ (JSImportClauseDefault n) = JSImportClauseDefault (fixSpace n) + fix _ (JSImportClauseNameSpace ns) = JSImportClauseNameSpace (fixSpace ns) + fix _ (JSImportClauseNamed named) = JSImportClauseNamed (fixEmpty named) + fix _ (JSImportClauseDefaultNameSpace def _ ns) = JSImportClauseDefaultNameSpace (fixSpace def) emptyAnnot (fixEmpty ns) + fix _ (JSImportClauseDefaultNamed def _ ns) = JSImportClauseDefaultNamed (fixSpace def) emptyAnnot (fixEmpty ns) + +instance MinifyJS JSFromClause where + fix a (JSFromClause _ _ m) = JSFromClause a emptyAnnot m + +instance MinifyJS JSImportNameSpace where + fix a (JSImportNameSpace _ _ ident) = JSImportNameSpace (JSBinOpTimes a) (JSBinOpAs spaceAnnot) (fixSpace ident) + +instance MinifyJS JSImportsNamed where + fix _ (JSImportsNamed _ imps _) = JSImportsNamed emptyAnnot (fixEmpty imps) emptyAnnot + +instance MinifyJS JSImportSpecifier where + fix _ (JSImportSpecifier x1) = JSImportSpecifier (fixEmpty x1) + fix _ (JSImportSpecifierAs x1 as x2) = JSImportSpecifierAs (fixEmpty x1) (fixSpace as) (fixSpace x2) + instance MinifyJS JSExportDeclaration where fix _ (JSExportLocals _ x1 _ _) = JSExportLocals emptyAnnot (fixEmpty x1) emptyAnnot noSemi fix _ (JSExport x1 _) = JSExport (fixStmt emptyAnnot noSemi x1) noSemi diff --git a/test/Test/Language/Javascript/Minify.hs b/test/Test/Language/Javascript/Minify.hs index f8f8eca..3358799 100644 --- a/test/Test/Language/Javascript/Minify.hs +++ b/test/Test/Language/Javascript/Minify.hs @@ -255,7 +255,14 @@ testMinifyProg = describe "Minify programs:" $ do minifyProg " try { } catch (a) {} finally {} ; try { } catch ( b ) { } ; " `shouldBe` "try{}catch(a){}finally{}try{}catch(b){}" testMinifyModule :: Spec -testMinifyModule = describe "Minify modules:" $ +testMinifyModule = describe "Minify modules:" $ do + it "import" $ do + minifyModule "import def from 'mod' ; " `shouldBe` "import def from'mod'" + minifyModule "import * as foo from \"mod\" ; " `shouldBe` "import * as foo from\"mod\"" + minifyModule "import def, * as foo from \"mod\" ; " `shouldBe` "import def,* as foo from\"mod\"" + minifyModule "import { baz, bar as foo } from \"mod\" ; " `shouldBe` "import{baz,bar as foo}from\"mod\"" + minifyModule "import def, { baz, bar as foo } from \"mod\" ; " `shouldBe` "import def,{baz,bar as foo}from\"mod\"" + it "export" $ do minifyModule " export { } ; " `shouldBe` "export{}" minifyModule " export { a } ; " `shouldBe` "export{a}" diff --git a/test/Test/Language/Javascript/ModuleParser.hs b/test/Test/Language/Javascript/ModuleParser.hs index 9b9dfad..50e3053 100644 --- a/test/Test/Language/Javascript/ModuleParser.hs +++ b/test/Test/Language/Javascript/ModuleParser.hs @@ -8,7 +8,30 @@ import Language.JavaScript.Parser testModuleParser :: Spec -testModuleParser = describe "Parse modules:" $ +testModuleParser = describe "Parse modules:" $ do + it "import" $ do + -- Not yet supported + -- test "import 'a';" `shouldBe` "" + + test "import def from 'mod';" + `shouldBe` + "Right (JSAstModule [JSModuleImportDeclaration (JSImportDeclaration (JSImportClauseDefault (JSIdentifier 'def'),JSFromClause ''mod''))])" + test "import def from \"mod\";" + `shouldBe` + "Right (JSAstModule [JSModuleImportDeclaration (JSImportDeclaration (JSImportClauseDefault (JSIdentifier 'def'),JSFromClause '\"mod\"'))])" + test "import * as thing from 'mod';" + `shouldBe` + "Right (JSAstModule [JSModuleImportDeclaration (JSImportDeclaration (JSImportClauseNameSpace (JSImportNameSpace (JSIdentifier 'thing')),JSFromClause ''mod''))])" + test "import { foo, bar, baz as quux } from 'mod';" + `shouldBe` + "Right (JSAstModule [JSModuleImportDeclaration (JSImportDeclaration (JSImportClauseNameSpace (JSImportsNamed ((JSImportSpecifier (JSIdentifier 'foo'),JSImportSpecifier (JSIdentifier 'bar'),JSImportSpecifierAs (JSIdentifier 'baz',JSIdentifier 'quux')))),JSFromClause ''mod''))])" + test "import def, * as thing from 'mod';" + `shouldBe` + "Right (JSAstModule [JSModuleImportDeclaration (JSImportDeclaration (JSImportClauseDefaultNameSpace (JSIdentifier 'def',JSImportNameSpace (JSIdentifier 'thing')),JSFromClause ''mod''))])" + test "import def, { foo, bar, baz as quux } from 'mod';" + `shouldBe` + "Right (JSAstModule [JSModuleImportDeclaration (JSImportDeclaration (JSImportClauseDefaultNamed (JSIdentifier 'def',JSImportsNamed ((JSImportSpecifier (JSIdentifier 'foo'),JSImportSpecifier (JSIdentifier 'bar'),JSImportSpecifierAs (JSIdentifier 'baz',JSIdentifier 'quux')))),JSFromClause ''mod''))])" + it "export" $ do test "export {}" `shouldBe` @@ -26,5 +49,6 @@ testModuleParser = describe "Parse modules:" $ `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 2e2704d..27c2026 100644 --- a/test/Test/Language/Javascript/RoundTrip.hs +++ b/test/Test/Language/Javascript/RoundTrip.hs @@ -101,6 +101,13 @@ testRoundTrip = describe "Roundtrip:" $ do testRT "var x=1;let y=2;" it "module" $ do + testRTModule "import def from 'mod'" + testRTModule "import def from \"mod\";" + testRTModule "import * as foo from \"mod\" ; " + testRTModule "import def, * as foo from \"mod\" ; " + testRTModule "import { baz, bar as foo } from \"mod\" ; " + testRTModule "import def, { baz, bar as foo } from \"mod\" ; " + testRTModule "export {};" testRTModule " export {} ; " testRTModule "export { a , b , c };"