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

Commit a18683b

Browse files
committed
Merge pull request #28 from JamesHenry/tsx
New: Implements JSX syntax (fixes #18)
2 parents e890743 + 0eddb71 commit a18683b

File tree

4 files changed

+203
-6
lines changed

4 files changed

+203
-6
lines changed

lib/ast-converter.js

Lines changed: 181 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@
3131
//------------------------------------------------------------------------------
3232

3333
var ts = require("typescript"),
34-
assign = require("object-assign");
34+
assign = require("object-assign"),
35+
unescape = require("lodash.unescape");
3536

3637
//------------------------------------------------------------------------------
3738
// Private
@@ -309,7 +310,16 @@ function getTokenType(token) {
309310
case SyntaxKind.NumericLiteral:
310311
return "Numeric";
311312

313+
case SyntaxKind.JsxText:
314+
return "JSXText";
315+
312316
case SyntaxKind.StringLiteral:
317+
// A TypeScript-StringLiteral token with a TypeScript-JsxAttribute or TypeScript-JsxElement parent,
318+
// must actually be an ESTree-JSXText token
319+
if (token.parent && (token.parent.kind === SyntaxKind.JsxAttribute || token.parent.kind === SyntaxKind.JsxElement)) {
320+
return "JSXText";
321+
}
322+
313323
return "String";
314324

315325
case SyntaxKind.RegularExpressionLiteral:
@@ -323,6 +333,23 @@ function getTokenType(token) {
323333
default:
324334
}
325335

336+
// Some JSX tokens have to be determined based on their parent
337+
if (token.parent) {
338+
if (token.kind === SyntaxKind.Identifier && token.parent.kind === SyntaxKind.FirstNode) {
339+
return "JSXIdentifier";
340+
}
341+
342+
if (token.parent.kind >= SyntaxKind.JsxElement && token.parent.kind <= SyntaxKind.JsxAttribute) {
343+
if (token.kind === SyntaxKind.FirstNode) {
344+
return "JSXMemberExpression";
345+
}
346+
347+
if (token.kind === SyntaxKind.Identifier) {
348+
return "JSXIdentifier";
349+
}
350+
}
351+
}
352+
326353
return "Identifier";
327354
}
328355

@@ -368,7 +395,6 @@ function convertTokens(ast) {
368395
if (converted) {
369396
result.push(converted);
370397
}
371-
372398
token = ts.findNextToken(token, ast);
373399
}
374400

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

453+
/**
454+
* Converts a TypeScript JSX node.tagName into an ESTree node.name
455+
* @param {Object} tagName the tagName object from a JSX TSNode
456+
* @param {Object} ast the AST object
457+
* @returns {Object} the converted ESTree name object
458+
*/
459+
function convertTypeScriptJSXTagNameToESTreeName(tagName) {
460+
var tagNameToken = convertToken(tagName, ast);
461+
462+
if (tagNameToken.type === "JSXMemberExpression") {
463+
464+
var isNestedMemberExpression = (node.tagName.left.kind === SyntaxKind.FirstNode);
465+
466+
// Convert TSNode left and right objects into ESTreeNode object
467+
// and property objects
468+
tagNameToken.object = convertChild(node.tagName.left);
469+
tagNameToken.property = convertChild(node.tagName.right);
470+
471+
// Assign the appropriate types
472+
tagNameToken.object.type = (isNestedMemberExpression) ? "JSXMemberExpression" : "JSXIdentifier";
473+
tagNameToken.property.type = "JSXIdentifier";
474+
475+
} else {
476+
477+
tagNameToken.name = tagNameToken.value;
478+
}
479+
480+
delete tagNameToken.value;
481+
482+
return tagNameToken;
483+
}
484+
427485
switch (node.kind) {
428486
case SyntaxKind.SourceFile:
429487
assign(result, {
@@ -1318,7 +1376,7 @@ module.exports = function(ast, extra) {
13181376
case SyntaxKind.StringLiteral:
13191377
assign(result, {
13201378
type: "Literal",
1321-
value: node.text,
1379+
value: unescape(node.text),
13221380
raw: ast.text.slice(result.range[0], result.range[1])
13231381
});
13241382
break;
@@ -1369,13 +1427,132 @@ module.exports = function(ast, extra) {
13691427
simplyCopy();
13701428
break;
13711429

1430+
// JSX
1431+
1432+
case SyntaxKind.JsxElement:
1433+
assign(result, {
1434+
type: "JSXElement",
1435+
openingElement: convertChild(node.openingElement),
1436+
closingElement: convertChild(node.closingElement),
1437+
children: node.children.map(convertChild)
1438+
});
1439+
1440+
break;
1441+
1442+
case SyntaxKind.JsxSelfClosingElement:
1443+
// Convert SyntaxKind.JsxSelfClosingElement to SyntaxKind.JsxOpeningElement,
1444+
// TypeScript does not seem to have the idea of openingElement when tag is self-closing
1445+
node.kind = SyntaxKind.JsxOpeningElement;
1446+
assign(result, {
1447+
type: "JSXElement",
1448+
openingElement: convertChild(node),
1449+
closingElement: null,
1450+
children: []
1451+
});
1452+
1453+
break;
1454+
1455+
case SyntaxKind.JsxOpeningElement:
1456+
var openingTagName = convertTypeScriptJSXTagNameToESTreeName(node.tagName);
1457+
assign(result, {
1458+
type: "JSXOpeningElement",
1459+
selfClosing: !(node.parent && node.parent.closingElement),
1460+
name: openingTagName,
1461+
attributes: node.attributes.map(convertChild)
1462+
});
1463+
1464+
break;
1465+
1466+
case SyntaxKind.JsxClosingElement:
1467+
var closingTagName = convertTypeScriptJSXTagNameToESTreeName(node.tagName);
1468+
assign(result, {
1469+
type: "JSXClosingElement",
1470+
name: closingTagName
1471+
});
1472+
1473+
break;
1474+
1475+
case SyntaxKind.JsxExpression:
1476+
var eloc = ast.getLineAndCharacterOfPosition(result.range[0] + 1);
1477+
var expression = (node.expression) ? convertChild(node.expression) : {
1478+
type: "JSXEmptyExpression",
1479+
loc: {
1480+
start: {
1481+
line: eloc.line + 1,
1482+
column: eloc.character
1483+
},
1484+
end: {
1485+
line: result.loc.end.line,
1486+
column: result.loc.end.column - 1
1487+
}
1488+
},
1489+
range: [result.range[0] + 1, result.range[1] - 1]
1490+
};
1491+
1492+
assign(result, {
1493+
type: "JSXExpressionContainer",
1494+
expression: expression
1495+
});
1496+
1497+
break;
1498+
1499+
case SyntaxKind.JsxAttribute:
1500+
var attributeName = convertToken(node.name, ast);
1501+
attributeName.name = attributeName.value;
1502+
delete attributeName.value;
1503+
1504+
assign(result, {
1505+
type: "JSXAttribute",
1506+
name: attributeName,
1507+
value: convertChild(node.initializer)
1508+
});
1509+
1510+
break;
1511+
1512+
case SyntaxKind.JsxText:
1513+
assign(result, {
1514+
type: "Literal",
1515+
value: ast.text.slice(node.pos, node.end),
1516+
raw: ast.text.slice(node.pos, node.end)
1517+
});
1518+
1519+
result.loc.start.column = node.pos;
1520+
result.range[0] = node.pos;
1521+
1522+
break;
1523+
1524+
case SyntaxKind.JsxSpreadAttribute:
1525+
assign(result, {
1526+
type: "JSXSpreadAttribute",
1527+
argument: convertChild(node.expression)
1528+
});
1529+
1530+
break;
1531+
1532+
case SyntaxKind.FirstNode:
1533+
var jsxMemberExpressionObject = convertChild(node.left);
1534+
jsxMemberExpressionObject.type = "JSXIdentifier";
1535+
delete jsxMemberExpressionObject.value;
1536+
1537+
var jsxMemberExpressionProperty = convertChild(node.right);
1538+
jsxMemberExpressionProperty.type = "JSXIdentifier";
1539+
delete jsxMemberExpressionObject.value;
1540+
1541+
assign(result, {
1542+
type: "JSXMemberExpression",
1543+
object: jsxMemberExpressionObject,
1544+
property: jsxMemberExpressionProperty
1545+
});
1546+
1547+
break;
1548+
13721549
// TypeScript specific
13731550

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

13771554
default:
1378-
console.log(node.kind);
1555+
console.log("unsupported node.kind:", node.kind);
13791556
result = null;
13801557
}
13811558

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
"betarelease": "eslint-prerelease beta"
5757
},
5858
"dependencies": {
59+
"lodash.unescape": "4.0.0",
5960
"object-assign": "^4.0.1"
6061
},
6162
"peerDependencies": {

parser.js

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,14 @@ function parse(code, options) {
107107
commentAttachment.reset();
108108
}
109109

110-
var FILENAME = "eslint.ts";
110+
if (options.ecmaFeatures && typeof options.ecmaFeatures === "object") {
111+
// pass through jsx option
112+
extra.ecmaFeatures.jsx = options.ecmaFeatures.jsx;
113+
}
114+
115+
// Even if jsx option is set in typescript compiler, filename still has to
116+
// contain .tsx file extension
117+
var FILENAME = (extra.ecmaFeatures.jsx) ? "eslint.tsx" : "eslint.ts";
111118

112119
var compilerHost = {
113120
fileExists: function() {

tests/lib/ecma-features.js

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,24 @@ var assert = require("chai").assert,
4343
var FIXTURES_DIR = "./tests/fixtures/ecma-features";
4444
// var FIXTURES_MIX_DIR = "./tests/fixtures/ecma-features-mix";
4545

46+
var filesWithOutsandingTSIssues = [
47+
"jsx/embedded-tags", // https://github.com/Microsoft/TypeScript/issues/7410
48+
"jsx/namespaced-attribute-and-value-inserted", // https://github.com/Microsoft/TypeScript/issues/7411
49+
"jsx/namespaced-name-and-attribute", // https://github.com/Microsoft/TypeScript/issues/7411
50+
"jsx/test-content", // https://github.com/Microsoft/TypeScript/issues/7471
51+
"jsx/multiple-blank-spaces"
52+
];
53+
4654
var testFiles = shelljs.find(FIXTURES_DIR).filter(function(filename) {
4755
return filename.indexOf(".src.js") > -1;
56+
}).filter(function(filename) {
57+
return filesWithOutsandingTSIssues.every(function(fileName) {
58+
return filename.indexOf(fileName) === -1;
59+
});
4860
}).map(function(filename) {
4961
return filename.substring(FIXTURES_DIR.length - 1, filename.length - 7); // strip off ".src.js"
5062
}).filter(function(filename) {
51-
return !(/jsx|error\-|invalid\-|globalReturn|experimental|newTarget/.test(filename));
63+
return !(/error\-|invalid\-|globalReturn|experimental|newTarget/.test(filename));
5264
});
5365

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

0 commit comments

Comments
 (0)