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

Commit

Permalink
Merge pull request #28 from JamesHenry/tsx
Browse files Browse the repository at this point in the history
New: Implements JSX syntax (fixes #18)
  • Loading branch information
nzakas committed Mar 18, 2016
2 parents e890743 + 0eddb71 commit a18683b
Show file tree
Hide file tree
Showing 4 changed files with 203 additions and 6 deletions.
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"
];

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

0 comments on commit a18683b

Please sign in to comment.