Skip to content

Add support for ES module import statements #82

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Feb 19, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
69 changes: 67 additions & 2 deletions src/Language/JavaScript/Parser/AST.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ module Language.JavaScript.Parser.AST

-- Modules
, JSModuleItem (..)
, JSImportDeclaration (..)
, JSImportClause (..)
, JSFromClause (..)
, JSImportNameSpace (..)
, JSImportsNamed (..)
, JSImportSpecifier (..)
, JSExportDeclaration (..)
, JSExportLocalSpecifier (..)

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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 ++ ")"
Expand Down
54 changes: 53 additions & 1 deletion src/Language/JavaScript/Parser/Grammar7.y
Original file line number Diff line number Diff line change
Expand Up @@ -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 {} }
Expand Down Expand Up @@ -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 }

Expand Down Expand Up @@ -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 }
Expand All @@ -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" }
Expand Down Expand Up @@ -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 ;
Expand Down
4 changes: 2 additions & 2 deletions src/Language/JavaScript/Parser/Lexer.x
Original file line number Diff line number Diff line change
Expand Up @@ -525,7 +525,9 @@ keywordNames =
, ( "finally", FinallyToken )
, ( "for", ForToken )
, ( "function", FunctionToken )
, ( "from", FromToken )
, ( "if", IfToken )
, ( "import", ImportToken )
, ( "in", InToken )
, ( "instanceof", InstanceofToken )
, ( "let", LetToken )
Expand Down Expand Up @@ -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 )


Expand Down
2 changes: 2 additions & 0 deletions src/Language/JavaScript/Parser/Token.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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] }
Expand All @@ -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
Expand Down
24 changes: 24 additions & 0 deletions src/Language/JavaScript/Pretty/Printer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
Expand Down
31 changes: 31 additions & 0 deletions src/Language/JavaScript/Process/Minify.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion test/Test/Language/Javascript/Minify.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
Expand Down
26 changes: 25 additions & 1 deletion test/Test/Language/Javascript/ModuleParser.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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")
7 changes: 7 additions & 0 deletions test/Test/Language/Javascript/RoundTrip.hs
Original file line number Diff line number Diff line change
Expand Up @@ -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 };"
Expand Down