Skip to content

Offer semantic services alongside AST #1

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 25 commits into from
Oct 29, 2018
Merged
Show file tree
Hide file tree
Changes from 21 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
da8b9c6
feat: use cwd to find tsconfig and build program
Sep 26, 2018
80b59ea
fix: use text read by eslint
Sep 28, 2018
a956b37
fix: ensure ast maps are unique to each parse result
Oct 3, 2018
feacd1e
fix: handle files not included in ts project more robustly
Oct 4, 2018
5703f20
chore: fix indentation
Oct 4, 2018
fa07d36
fix: remove duplicated code from merge
Oct 4, 2018
aeac88b
fix: re-add missing argument
Oct 4, 2018
3e6307b
fix: take tsconfig paths from eslintrc rather than finding one
Oct 8, 2018
789b058
perf: reuse existing programs and supply outdated to create new programs
Oct 9, 2018
f3b7be0
fix: appropriately handle malformed tsconfigs
Oct 9, 2018
fc56c25
test: add test for semantic info in isolated file
Oct 11, 2018
7d1772c
test: add bad project input tests
Oct 12, 2018
e6b1098
test: add test validating cross-file type resolution
Oct 12, 2018
86bcf05
chore: add comment to test
Oct 12, 2018
b8da9c1
chore: limit badtsconfig for readability
Oct 12, 2018
ea1f7ce
chore: update test header
Oct 12, 2018
f901b06
perf: use TS watch API to track changes to programs
Oct 17, 2018
1c9289f
fix: ensure changes to the linted file are detected
Oct 18, 2018
c0f4a1b
fix: improve project option validation
Oct 18, 2018
6637f6c
chore: remove unnecessary comments from tsconfig in tests
Oct 18, 2018
7fc634f
fix: report config file errors whenever the program updates
Oct 22, 2018
455ffc8
fix: add sourcefile to more operations
Oct 29, 2018
f25a85f
refactor: only pass projects to project option handling
Oct 29, 2018
4a857b5
refactor: break up program creation for readability
Oct 29, 2018
d5e69af
refactor: add comment and use util function
Oct 29, 2018
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
5 changes: 4 additions & 1 deletion lib/ast-converter.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,5 +73,8 @@ module.exports = (ast, extra) => {
estree.comments = convertComments(ast, extra.code);
}

return estree;
const astMaps = convert.getASTMaps();
convert.resetASTMaps();

return { estree, astMaps };
};
80 changes: 58 additions & 22 deletions lib/convert.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ const SyntaxKind = nodeUtils.SyntaxKind;
// Public
//------------------------------------------------------------------------------

let esTreeNodeToTSNodeMap = new WeakMap();
let tsNodeToESTreeNodeMap = new WeakMap();

function resetASTMaps() {
esTreeNodeToTSNodeMap = new WeakMap();
tsNodeToESTreeNodeMap = new WeakMap();
}

function getASTMaps() {
return { esTreeNodeToTSNodeMap, tsNodeToESTreeNodeMap };
}

/**
* Converts a TypeScript node into an ESTree node
* @param {Object} config configuration options for the conversion
Expand Down Expand Up @@ -53,7 +65,7 @@ module.exports = function convert(config) {
*/
let result = {
type: '',
range: [node.getStart(), node.end],
range: [node.getStart(ast), node.end],
loc: nodeUtils.getLoc(node, ast)
};

Expand Down Expand Up @@ -132,7 +144,7 @@ module.exports = function convert(config) {
if (nodeUtils.isTypeKeyword(typeArgument.kind)) {
return {
type: AST_NODE_TYPES[`TS${SyntaxKind[typeArgument.kind]}`],
range: [typeArgument.getStart(), typeArgument.getEnd()],
range: [typeArgument.getStart(ast), typeArgument.getEnd()],
loc: nodeUtils.getLoc(typeArgument, ast)
};
}
Expand All @@ -146,7 +158,7 @@ module.exports = function convert(config) {
}
return {
type: AST_NODE_TYPES.TSTypeReference,
range: [typeArgument.getStart(), typeArgument.getEnd()],
range: [typeArgument.getStart(ast), typeArgument.getEnd()],
loc: nodeUtils.getLoc(typeArgument, ast),
typeName: convertChild(typeArgument.typeName || typeArgument),
typeParameters: typeArgument.typeArguments
Expand Down Expand Up @@ -199,7 +211,7 @@ module.exports = function convert(config) {

return {
type: AST_NODE_TYPES.TSTypeParameter,
range: [typeParameter.getStart(), typeParameter.getEnd()],
range: [typeParameter.getStart(ast), typeParameter.getEnd()],
loc: nodeUtils.getLoc(typeParameter, ast),
name,
constraint,
Expand Down Expand Up @@ -266,7 +278,7 @@ module.exports = function convert(config) {
const expression = convertChild(decorator.expression);
return {
type: AST_NODE_TYPES.Decorator,
range: [decorator.getStart(), decorator.end],
range: [decorator.getStart(ast), decorator.end],
loc: nodeUtils.getLoc(decorator, ast),
expression
};
Expand Down Expand Up @@ -477,7 +489,11 @@ module.exports = function convert(config) {
});

result.range[1] = node.endOfFileToken.end;
result.loc = nodeUtils.getLocFor(node.getStart(), result.range[1], ast);
result.loc = nodeUtils.getLocFor(
node.getStart(ast),
result.range[1],
ast
);
break;

case SyntaxKind.Block:
Expand Down Expand Up @@ -934,11 +950,12 @@ module.exports = function convert(config) {
return false;
}
return nodeUtils.getTextForTokenKind(token.kind) === '(';
}
},
ast
);

const methodLoc = ast.getLineAndCharacterOfPosition(
openingParen.getStart()
openingParen.getStart(ast)
),
nodeIsMethod = node.kind === SyntaxKind.MethodDeclaration,
method = {
Expand Down Expand Up @@ -1069,10 +1086,10 @@ module.exports = function convert(config) {
};

const constructorIdentifierLocStart = ast.getLineAndCharacterOfPosition(
firstConstructorToken.getStart()
firstConstructorToken.getStart(ast)
),
constructorIdentifierLocEnd = ast.getLineAndCharacterOfPosition(
firstConstructorToken.getEnd()
firstConstructorToken.getEnd(ast)
),
constructorIsComputed =
!!node.name && nodeUtils.isComputedProperty(node.name);
Expand All @@ -1084,7 +1101,10 @@ module.exports = function convert(config) {
type: AST_NODE_TYPES.Literal,
value: 'constructor',
raw: node.name.getText(),
range: [firstConstructorToken.getStart(), firstConstructorToken.end],
range: [
firstConstructorToken.getStart(ast),
firstConstructorToken.end
],
loc: {
start: {
line: constructorIdentifierLocStart.line + 1,
Expand All @@ -1100,7 +1120,10 @@ module.exports = function convert(config) {
constructorKey = {
type: AST_NODE_TYPES.Identifier,
name: 'constructor',
range: [firstConstructorToken.getStart(), firstConstructorToken.end],
range: [
firstConstructorToken.getStart(ast),
firstConstructorToken.end
],
loc: {
start: {
line: constructorIdentifierLocStart.line + 1,
Expand Down Expand Up @@ -1233,9 +1256,9 @@ module.exports = function convert(config) {
type: AST_NODE_TYPES.AssignmentPattern,
left: convertChild(node.name),
right: convertChild(node.initializer),
range: [node.name.getStart(), node.initializer.end],
range: [node.name.getStart(ast), node.initializer.end],
loc: nodeUtils.getLocFor(
node.name.getStart(),
node.name.getStart(ast),
node.initializer.end,
ast
)
Expand Down Expand Up @@ -1292,7 +1315,7 @@ module.exports = function convert(config) {
{
type: AST_NODE_TYPES.TemplateElement,
value: {
raw: ast.text.slice(node.getStart() + 1, node.end - 1),
raw: ast.text.slice(node.getStart(ast) + 1, node.end - 1),
cooked: node.text
},
tail: true,
Expand Down Expand Up @@ -1335,7 +1358,10 @@ module.exports = function convert(config) {
Object.assign(result, {
type: AST_NODE_TYPES.TemplateElement,
value: {
raw: ast.text.slice(node.getStart() + 1, node.end - (tail ? 1 : 2)),
raw: ast.text.slice(
node.getStart(ast) + 1,
node.end - (tail ? 1 : 2)
),
cooked: node.text
},
tail
Expand Down Expand Up @@ -1426,7 +1452,7 @@ module.exports = function convert(config) {
if (node.modifiers) {
return {
type: AST_NODE_TYPES.TSParameterProperty,
range: [node.getStart(), node.end],
range: [node.getStart(ast), node.end],
loc: nodeUtils.getLoc(node, ast),
accessibility: nodeUtils.getTSNodeAccessibility(node) || undefined,
readonly:
Expand Down Expand Up @@ -1523,8 +1549,8 @@ module.exports = function convert(config) {
body: [],

// TODO: Fix location info
range: [openBrace.getStart(), result.range[1]],
loc: nodeUtils.getLocFor(openBrace.getStart(), node.end, ast)
range: [openBrace.getStart(ast), result.range[1]],
loc: nodeUtils.getLocFor(openBrace.getStart(ast), node.end, ast)
},
superClass:
superClass && superClass.types[0]
Expand Down Expand Up @@ -2142,7 +2168,7 @@ module.exports = function convert(config) {
type: AST_NODE_TYPES.VariableDeclarator,
id: convertChild(node.name),
init: convertChild(node.type),
range: [node.name.getStart(), node.end]
range: [node.name.getStart(ast), node.end]
};

typeAliasDeclarator.loc = nodeUtils.getLocFor(
Expand Down Expand Up @@ -2299,8 +2325,12 @@ module.exports = function convert(config) {
const interfaceBody = {
type: AST_NODE_TYPES.TSInterfaceBody,
body: node.members.map(member => convertChild(member)),
range: [interfaceOpenBrace.getStart(), result.range[1]],
loc: nodeUtils.getLocFor(interfaceOpenBrace.getStart(), node.end, ast)
range: [interfaceOpenBrace.getStart(ast), result.range[1]],
loc: nodeUtils.getLocFor(
interfaceOpenBrace.getStart(ast),
node.end,
ast
)
};

Object.assign(result, {
Expand Down Expand Up @@ -2411,5 +2441,11 @@ module.exports = function convert(config) {
deeplyCopy();
}

tsNodeToESTreeNodeMap.set(node, result);
esTreeNodeToTSNodeMap.set(result, node);

return result;
};

module.exports.getASTMaps = getASTMaps;
module.exports.resetASTMaps = resetASTMaps;
78 changes: 64 additions & 14 deletions lib/node-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ function getLocFor(start, end, ast) {
* @returns {Object} the loc data
*/
function getLoc(nodeOrToken, ast) {
return getLocFor(nodeOrToken.getStart(), nodeOrToken.end, ast);
return getLocFor(nodeOrToken.getStart(ast), nodeOrToken.end, ast);
}

/**
Expand Down Expand Up @@ -407,28 +407,44 @@ function hasStaticModifierFlag(node) {
* Finds the next token based on the previous one and its parent
* @param {TSToken} previousToken The previous TSToken
* @param {TSNode} parent The parent TSNode
* @param {ts.SourceFileLike} ast The TS AST
* @returns {TSToken} the next TSToken
*/
function findNextToken(previousToken, parent) {
/**
* TODO: Remove dependency on private TypeScript method
*/
return ts.findNextToken(previousToken, parent);
Copy link

Choose a reason for hiding this comment

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

We should just make this public.

Copy link
Owner Author

Choose a reason for hiding this comment

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

Note that the implementation I wrote differs very slightly (but importantly) from our implementation, so we should fix that too. But that's why I had to change it.

function findNextToken(previousToken, parent, ast) {
return find(parent);

function find(n) {
if (ts.isToken(n) && n.pos === previousToken.end) {
// this is token that starts at the end of previous token - return it
return n;
}
return firstDefined(n.getChildren(ast), child => {
const shouldDiveInChildNode =
// previous token is enclosed somewhere in the child
(child.pos <= previousToken.pos && child.end > previousToken.end) ||
// previous token ends exactly at the beginning of child
child.pos === previousToken.end;
return shouldDiveInChildNode && nodeHasTokens(child, ast)
? find(child)
: undefined;
});
}
}

/**
* Find the first matching token based on the given predicate function.
* @param {TSToken} previousToken The previous TSToken
* @param {TSNode} parent The parent TSNode
* @param {Function} predicate The predicate function to apply to each checked token
* @param {ts.SourceFileLike} ast The TS AST
* @returns {TSToken|undefined} a matching TSToken
*/
function findFirstMatchingToken(previousToken, parent, predicate) {
function findFirstMatchingToken(previousToken, parent, predicate, ast) {
while (previousToken) {
if (predicate(previousToken)) {
return previousToken;
}
previousToken = findNextToken(previousToken, parent);
previousToken = findNextToken(previousToken, parent, ast);
}
return undefined;
}
Expand Down Expand Up @@ -536,9 +552,9 @@ function fixExports(node, result, ast) {
lastModifier = node.modifiers[node.modifiers.length - 1],
declarationIsDefault =
nextModifier && nextModifier.kind === SyntaxKind.DefaultKeyword,
varToken = findNextToken(lastModifier, ast);
varToken = findNextToken(lastModifier, ast, ast);

result.range[0] = varToken.getStart();
result.range[0] = varToken.getStart(ast);
result.loc = getLocFor(result.range[0], result.range[1], ast);

const declarationType = declarationIsDefault
Expand All @@ -548,8 +564,8 @@ function fixExports(node, result, ast) {
const newResult = {
type: declarationType,
declaration: result,
range: [exportKeyword.getStart(), result.range[1]],
loc: getLocFor(exportKeyword.getStart(), result.range[1], ast)
range: [exportKeyword.getStart(ast), result.range[1]],
loc: getLocFor(exportKeyword.getStart(ast), result.range[1], ast)
};

if (!declarationIsDefault) {
Expand Down Expand Up @@ -680,7 +696,7 @@ function convertToken(token, ast) {
const start =
token.kind === SyntaxKind.JsxText
? token.getFullStart()
: token.getStart(),
: token.getStart(ast),
end = token.getEnd(),
value = ast.text.slice(start, end),
newToken = {
Expand Down Expand Up @@ -725,7 +741,7 @@ function convertTokens(ast) {
result.push(converted);
}
} else {
node.getChildren().forEach(walk);
node.getChildren(ast).forEach(walk);
}
}
walk(ast);
Expand Down Expand Up @@ -779,3 +795,37 @@ function createError(ast, start, message) {
message
};
}

/**
* @param {ts.Node} n the TSNode
* @param {ts.SourceFileLike} ast the TS AST
*/
function nodeHasTokens(n, ast) {
// If we have a token or node that has a non-zero width, it must have tokens.
// Note: getWidth() does not take trivia into account.
return n.kind === SyntaxKind.EndOfFileToken
? !!n.jsDoc
: n.getWidth(ast) !== 0;
}

/**
* Like `forEach`, but suitable for use with numbers and strings (which may be falsy).
* @template T
* @template U
* @param {ReadonlyArray<T>|undefined} array
* @param {(element: T, index: number) => (U|undefined)} callback
* @returns {U|undefined}
*/
function firstDefined(array, callback) {
if (array === undefined) {
return undefined;
}

for (let i = 0; i < array.length; i++) {
const result = callback(array[i], i);
if (result !== undefined) {
return result;
}
}
return undefined;
}
Loading