Skip to content
This repository has been archived by the owner on Jan 19, 2019. It is now read-only.

New: Implements JSX syntax (fixes #18) #28

Merged
merged 1 commit into from
Mar 18, 2016
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
185 changes: 181 additions & 4 deletions lib/ast-converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
//------------------------------------------------------------------------------

var ts = require("typescript"),
assign = require("object-assign");
assign = require("object-assign"),
unescape = require("lodash.unescape");

//------------------------------------------------------------------------------
// Private
Expand Down Expand Up @@ -309,7 +310,16 @@ function getTokenType(token) {
case SyntaxKind.NumericLiteral:
return "Numeric";

case SyntaxKind.JsxText:
return "JSXText";

case SyntaxKind.StringLiteral:
// A TypeScript-StringLiteral token with a TypeScript-JsxAttribute or TypeScript-JsxElement parent,
// must actually be an ESTree-JSXText token
if (token.parent && (token.parent.kind === SyntaxKind.JsxAttribute || token.parent.kind === SyntaxKind.JsxElement)) {
return "JSXText";
}

return "String";

case SyntaxKind.RegularExpressionLiteral:
Expand All @@ -323,6 +333,23 @@ function getTokenType(token) {
default:
}

// Some JSX tokens have to be determined based on their parent
if (token.parent) {
if (token.kind === SyntaxKind.Identifier && token.parent.kind === SyntaxKind.FirstNode) {
return "JSXIdentifier";
}

if (token.parent.kind >= SyntaxKind.JsxElement && token.parent.kind <= SyntaxKind.JsxAttribute) {
if (token.kind === SyntaxKind.FirstNode) {
return "JSXMemberExpression";
}

if (token.kind === SyntaxKind.Identifier) {
return "JSXIdentifier";
}
}
}

return "Identifier";
}

Expand Down Expand Up @@ -368,7 +395,6 @@ function convertTokens(ast) {
if (converted) {
result.push(converted);
}

token = ts.findNextToken(token, ast);
}

Expand Down Expand Up @@ -424,6 +450,38 @@ module.exports = function(ast, extra) {
return convert(child, node);
}

/**
* Converts a TypeScript JSX node.tagName into an ESTree node.name
* @param {Object} tagName the tagName object from a JSX TSNode
* @param {Object} ast the AST object
* @returns {Object} the converted ESTree name object
*/
function convertTypeScriptJSXTagNameToESTreeName(tagName) {
var tagNameToken = convertToken(tagName, ast);

if (tagNameToken.type === "JSXMemberExpression") {

var isNestedMemberExpression = (node.tagName.left.kind === SyntaxKind.FirstNode);

// Convert TSNode left and right objects into ESTreeNode object
// and property objects
tagNameToken.object = convertChild(node.tagName.left);
tagNameToken.property = convertChild(node.tagName.right);

// Assign the appropriate types
tagNameToken.object.type = (isNestedMemberExpression) ? "JSXMemberExpression" : "JSXIdentifier";
tagNameToken.property.type = "JSXIdentifier";

} else {

tagNameToken.name = tagNameToken.value;
}

delete tagNameToken.value;

return tagNameToken;
}

switch (node.kind) {
case SyntaxKind.SourceFile:
assign(result, {
Expand Down Expand Up @@ -1318,7 +1376,7 @@ module.exports = function(ast, extra) {
case SyntaxKind.StringLiteral:
assign(result, {
type: "Literal",
value: node.text,
value: unescape(node.text),
raw: ast.text.slice(result.range[0], result.range[1])
});
break;
Expand Down Expand Up @@ -1369,13 +1427,132 @@ module.exports = function(ast, extra) {
simplyCopy();
break;

// JSX

case SyntaxKind.JsxElement:
assign(result, {
type: "JSXElement",
openingElement: convertChild(node.openingElement),
closingElement: convertChild(node.closingElement),
children: node.children.map(convertChild)
});

break;

case SyntaxKind.JsxSelfClosingElement:
// Convert SyntaxKind.JsxSelfClosingElement to SyntaxKind.JsxOpeningElement,
// TypeScript does not seem to have the idea of openingElement when tag is self-closing
node.kind = SyntaxKind.JsxOpeningElement;
assign(result, {
type: "JSXElement",
openingElement: convertChild(node),
closingElement: null,
children: []
});

break;

case SyntaxKind.JsxOpeningElement:
var openingTagName = convertTypeScriptJSXTagNameToESTreeName(node.tagName);
assign(result, {
type: "JSXOpeningElement",
selfClosing: !(node.parent && node.parent.closingElement),
name: openingTagName,
attributes: node.attributes.map(convertChild)
});

break;

case SyntaxKind.JsxClosingElement:
var closingTagName = convertTypeScriptJSXTagNameToESTreeName(node.tagName);
assign(result, {
type: "JSXClosingElement",
name: closingTagName
});

break;

case SyntaxKind.JsxExpression:
var eloc = ast.getLineAndCharacterOfPosition(result.range[0] + 1);
var expression = (node.expression) ? convertChild(node.expression) : {
type: "JSXEmptyExpression",
loc: {
start: {
line: eloc.line + 1,
column: eloc.character
},
end: {
line: result.loc.end.line,
column: result.loc.end.column - 1
}
},
range: [result.range[0] + 1, result.range[1] - 1]
};

assign(result, {
type: "JSXExpressionContainer",
expression: expression
});

break;

case SyntaxKind.JsxAttribute:
var attributeName = convertToken(node.name, ast);
attributeName.name = attributeName.value;
delete attributeName.value;

assign(result, {
type: "JSXAttribute",
name: attributeName,
value: convertChild(node.initializer)
});

break;

case SyntaxKind.JsxText:
assign(result, {
type: "Literal",
value: ast.text.slice(node.pos, node.end),
raw: ast.text.slice(node.pos, node.end)
});

result.loc.start.column = node.pos;
result.range[0] = node.pos;

break;

case SyntaxKind.JsxSpreadAttribute:
assign(result, {
type: "JSXSpreadAttribute",
argument: convertChild(node.expression)
});

break;

case SyntaxKind.FirstNode:
var jsxMemberExpressionObject = convertChild(node.left);
jsxMemberExpressionObject.type = "JSXIdentifier";
delete jsxMemberExpressionObject.value;

var jsxMemberExpressionProperty = convertChild(node.right);
jsxMemberExpressionProperty.type = "JSXIdentifier";
delete jsxMemberExpressionObject.value;

assign(result, {
type: "JSXMemberExpression",
object: jsxMemberExpressionObject,
property: jsxMemberExpressionProperty
});

break;

// TypeScript specific

case SyntaxKind.ParenthesizedExpression:
return convert(node.expression, parent);

default:
console.log(node.kind);
console.log("unsupported node.kind:", node.kind);
result = null;
}

Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"betarelease": "eslint-prerelease beta"
},
"dependencies": {
"lodash.unescape": "4.0.0",
"object-assign": "^4.0.1"
},
"peerDependencies": {
Expand Down
9 changes: 8 additions & 1 deletion parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,14 @@ function parse(code, options) {
commentAttachment.reset();
}

var FILENAME = "eslint.ts";
if (options.ecmaFeatures && typeof options.ecmaFeatures === "object") {
// pass through jsx option
extra.ecmaFeatures.jsx = options.ecmaFeatures.jsx;
}

// Even if jsx option is set in typescript compiler, filename still has to
// contain .tsx file extension
var FILENAME = (extra.ecmaFeatures.jsx) ? "eslint.tsx" : "eslint.ts";

var compilerHost = {
fileExists: function() {
Expand Down
14 changes: 13 additions & 1 deletion tests/lib/ecma-features.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,24 @@ var assert = require("chai").assert,
var FIXTURES_DIR = "./tests/fixtures/ecma-features";
// var FIXTURES_MIX_DIR = "./tests/fixtures/ecma-features-mix";

var filesWithOutsandingTSIssues = [
"jsx/embedded-tags", // https://github.com/Microsoft/TypeScript/issues/7410
"jsx/namespaced-attribute-and-value-inserted", // https://github.com/Microsoft/TypeScript/issues/7411
"jsx/namespaced-name-and-attribute", // https://github.com/Microsoft/TypeScript/issues/7411
"jsx/test-content", // https://github.com/Microsoft/TypeScript/issues/7471
"jsx/multiple-blank-spaces"
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have not had chance to report this one yet (jsx/multiple-blank-spaces), but might be another bug with some internal TypeScript utilities. The body of the AST is correct, but we cannot currently populate tokens array properly because of the issue.

];

var testFiles = shelljs.find(FIXTURES_DIR).filter(function(filename) {
return filename.indexOf(".src.js") > -1;
}).filter(function(filename) {
return filesWithOutsandingTSIssues.every(function(fileName) {
return filename.indexOf(fileName) === -1;
});
}).map(function(filename) {
return filename.substring(FIXTURES_DIR.length - 1, filename.length - 7); // strip off ".src.js"
}).filter(function(filename) {
return !(/jsx|error\-|invalid\-|globalReturn|experimental|newTarget/.test(filename));
return !(/error\-|invalid\-|globalReturn|experimental|newTarget/.test(filename));
});

// var moduleTestFiles = testFiles.filter(function(filename) {
Expand Down