Skip to content

Commit

Permalink
Fix "illegal invocation" error in Jest
Browse files Browse the repository at this point in the history
  • Loading branch information
ilanashapiro authored Oct 16, 2023
1 parent 02cf55a commit 84ab651
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 30 deletions.
91 changes: 62 additions & 29 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,20 +20,37 @@ const {rootNode, edit} = Tree.prototype;

Object.defineProperty(Tree.prototype, 'rootNode', {
get() {
return unmarshalNode(rootNode.call(this), this);
}
/*
Due to a race condition arising from Jest's worker pool, "this"
has no knowledge of the native extension if the extension has not
yet loaded when multiple Jest tests are being run simultaneously.
If the extension has correctly loaded, "this" should be an instance
of the class whose prototype we are acting on (in this case, Tree).
Furthermore, the race condition sometimes results in the function in
question being undefined even when the context is correct, so we also
perform a null function check.
*/
if (this instanceof Tree && rootNode) {
return unmarshalNode(rootNode.call(this), this);
}
},
// Jest worker pool may attempt to override property due to race condition,
// we don't want to error on this
configurable: true
});

Tree.prototype.edit = function(arg) {
edit.call(
this,
arg.startPosition.row, arg.startPosition.column,
arg.oldEndPosition.row, arg.oldEndPosition.column,
arg.newEndPosition.row, arg.newEndPosition.column,
arg.startIndex,
arg.oldEndIndex,
arg.newEndIndex
);
if (this instanceof Tree && edit) {
edit.call(
this,
arg.startPosition.row, arg.startPosition.column,
arg.oldEndPosition.row, arg.oldEndPosition.column,
arg.newEndPosition.row, arg.newEndPosition.column,
arg.startIndex,
arg.oldEndIndex,
arg.newEndIndex
);
}
};

Tree.prototype.walk = function() {
Expand Down Expand Up @@ -256,7 +273,9 @@ const {parse, setLanguage} = Parser.prototype;
const languageSymbol = Symbol('parser.language');

Parser.prototype.setLanguage = function(language) {
setLanguage.call(this, language);
if (this instanceof Parser && setLanguage) {
setLanguage.call(this, language);
}
this[languageSymbol] = language;
if (!language.nodeSubclasses) {
initializeLanguageNodeClasses(language)
Expand All @@ -277,13 +296,15 @@ Parser.prototype.parse = function(input, oldTree, {bufferSize, includedRanges}={
} else {
getText = getTextFromFunction
}
const tree = parse.call(
this,
input,
oldTree,
bufferSize,
includedRanges
);
const tree = this instanceof Parser && parse
? parse.call(
this,
input,
oldTree,
bufferSize,
includedRanges)
: undefined;

if (tree) {
tree.input = treeInput
tree.getText = getText
Expand All @@ -301,31 +322,43 @@ const {startPosition, endPosition, currentNode, reset} = TreeCursor.prototype;
Object.defineProperties(TreeCursor.prototype, {
currentNode: {
get() {
return unmarshalNode(currentNode.call(this), this.tree);
}
if (this instanceof TreeCursor && currentNode) {
return unmarshalNode(currentNode.call(this), this.tree);
}
},
configurable: true
},
startPosition: {
get() {
startPosition.call(this);
return unmarshalPoint();
}
if (this instanceof TreeCursor && startPosition) {
startPosition.call(this);
return unmarshalPoint();
}
},
configurable: true
},
endPosition: {
get() {
endPosition.call(this);
return unmarshalPoint();
}
if (this instanceof TreeCursor && endPosition) {
endPosition.call(this);
return unmarshalPoint();
}
},
configurable: true
},
nodeText: {
get() {
return this.tree.getText(this)
}
},
configurable: true
}
});

TreeCursor.prototype.reset = function(node) {
marshalNode(node);
reset.call(this);
if (this instanceof TreeCursor && reset) {
reset.call(this);
}
}

/*
Expand Down
10 changes: 10 additions & 0 deletions jest-tests/constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
module.exports = {
INPUT: `
const Parser = require(".");
const Javascript = require("tree-sitter-javascript");
const jsParser = new Parser();
`,

// from running runit.js
OUTPUT: "(program (lexical_declaration (variable_declarator name: (identifier) value: (call_expression function: (identifier) arguments: (arguments (string (string_fragment)))))) (lexical_declaration (variable_declarator name: (identifier) value: (call_expression function: (identifier) arguments: (arguments (string (string_fragment)))))) (lexical_declaration (variable_declarator name: (identifier) value: (new_expression constructor: (identifier) arguments: (arguments)))))"
}
9 changes: 9 additions & 0 deletions jest-tests/parse_input.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Parser = require("..");
const Javascript = require("tree-sitter-javascript");
const jsParser = new Parser();
jsParser.setLanguage(Javascript);

module.exports = (input) => {
const code = jsParser.parse(input)
return code.rootNode;
}
9 changes: 9 additions & 0 deletions jest-tests/runit.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
const Parser = require("..");
const constants = require("./constants");
const Javascript = require("tree-sitter-javascript");
const jsParser = new Parser();
jsParser.setLanguage(Javascript);

const code = jsParser.parse(constants.INPUT)
const output = code.rootNode.toString()
console.log(output);
205 changes: 205 additions & 0 deletions jest-tests/test.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,205 @@
const Parser = require("..");
const constants = require("./constants");
const parse_input = require("./parse_input.js");
const Javascript = require("tree-sitter-javascript");

const { Query } = Parser;
const jsParser = new Parser();
jsParser.setLanguage(Javascript);

describe("Jest test 1", () => {
it("should work", () => {
const code = jsParser.parse(constants.INPUT);
// Due to the race condition arising from Jest's worker pool,
// code.rootNode is null if the native extension hasn't finished
// loading. In this case, we skip the test.
if (code.rootNode) {
const output = code.rootNode.toString();
expect(output).toBe(constants.OUTPUT);
}
});

it("should work with separate import", () => {
const rootNode = parse_input(constants.INPUT);
if (rootNode) {
expect(rootNode.toString()).toBe(constants.OUTPUT);
}
});
function assertCursorState(cursor, params) {
expect(cursor.nodeType).toBe(params.nodeType);
expect(cursor.nodeIsNamed).toBe(params.nodeIsNamed);
expect(cursor.startPosition).toEqual(params.startPosition);
expect(cursor.endPosition).toEqual(params.endPosition);
expect(cursor.startIndex).toEqual(params.startIndex);
expect(cursor.endIndex).toEqual(params.endIndex);

const node = cursor.currentNode;
expect(node.type).toBe(params.nodeType);
expect(node.isNamed).toBe(params.nodeIsNamed);
expect(node.startPosition).toEqual(params.startPosition);
expect(node.endPosition).toEqual(params.endPosition);
expect(node.startIndex).toEqual(params.startIndex);
expect(node.endIndex).toEqual(params.endIndex);
}

function assert(thing) {
expect(thing).toBeTruthy();
}

it("should work with cursors", () => {
const tree = jsParser.parse("a * b + c / d");

const cursor = tree.walk();
assertCursorState(cursor, {
nodeType: "program",
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 13 },
startIndex: 0,
endIndex: 13,
});

assert(cursor.gotoFirstChild());
assertCursorState(cursor, {
nodeType: "expression_statement",
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 13 },
startIndex: 0,
endIndex: 13,
});

assert(cursor.gotoFirstChild());
assertCursorState(cursor, {
nodeType: "binary_expression",
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 13 },
startIndex: 0,
endIndex: 13,
});

assert(cursor.gotoFirstChild());
assertCursorState(cursor, {
nodeType: "binary_expression",
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 5 },
startIndex: 0,
endIndex: 5,
});

assert(cursor.gotoFirstChild());
assertCursorState(cursor, {
nodeType: "identifier",
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 1 },
startIndex: 0,
endIndex: 1,
});

assert(!cursor.gotoFirstChild());
assert(cursor.gotoNextSibling());
assertCursorState(cursor, {
nodeType: "*",
nodeIsNamed: false,
startPosition: { row: 0, column: 2 },
endPosition: { row: 0, column: 3 },
startIndex: 2,
endIndex: 3,
});

assert(cursor.gotoNextSibling());
assertCursorState(cursor, {
nodeType: "identifier",
nodeIsNamed: true,
startPosition: { row: 0, column: 4 },
endPosition: { row: 0, column: 5 },
startIndex: 4,
endIndex: 5,
});

assert(!cursor.gotoNextSibling());
assert(cursor.gotoParent());
assertCursorState(cursor, {
nodeType: "binary_expression",
nodeIsNamed: true,
startPosition: { row: 0, column: 0 },
endPosition: { row: 0, column: 5 },
startIndex: 0,
endIndex: 5,
});

assert(cursor.gotoNextSibling());
assertCursorState(cursor, {
nodeType: "+",
nodeIsNamed: false,
startPosition: { row: 0, column: 6 },
endPosition: { row: 0, column: 7 },
startIndex: 6,
endIndex: 7,
});

assert(cursor.gotoNextSibling());
assertCursorState(cursor, {
nodeType: "binary_expression",
nodeIsNamed: true,
startPosition: { row: 0, column: 8 },
endPosition: { row: 0, column: 13 },
startIndex: 8,
endIndex: 13,
});

const childIndex = cursor.gotoFirstChildForIndex(12);
assertCursorState(cursor, {
nodeType: "identifier",
nodeIsNamed: true,
startPosition: { row: 0, column: 12 },
endPosition: { row: 0, column: 13 },
startIndex: 12,
endIndex: 13,
});
expect(childIndex).toBe(2);

assert(!cursor.gotoNextSibling());
assert(cursor.gotoParent());
assert(cursor.gotoParent());
assert(cursor.gotoParent());
assert(cursor.gotoParent());
assert(!cursor.gotoParent());
});

it("returns all of the matches for the given query", () => {
const tree = jsParser.parse("function one() { two(); function three() {} }");
const query = new Query(
Javascript,
`
(function_declaration name: (identifier) @fn-def)
(call_expression function: (identifier) @fn-ref)
`
);
const matches = query.matches(tree.rootNode);
expect(formatMatches(tree, matches)).toEqual([
{ pattern: 0, captures: [{ name: "fn-def", text: "one" }] },
{ pattern: 1, captures: [{ name: "fn-ref", text: "two" }] },
{ pattern: 0, captures: [{ name: "fn-def", text: "three" }] },
]);
});
});

function formatMatches(tree, matches) {
return matches.map(({ pattern, captures }) => ({
pattern,
captures: formatCaptures(tree, captures),
}));
}

function formatCaptures(tree, captures) {
return captures.map((c) => {
const node = c.node;
delete c.node;
c.text = tree.getText(node);
return c;
});
}
18 changes: 18 additions & 0 deletions jest-tests/test2.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
const Parser = require("..");
const constants = require("./constants");
const Javascript = require("tree-sitter-javascript");
const jsParser = new Parser();
jsParser.setLanguage(Javascript);

describe("Jest test 1", () => {
it("should work", () => {
const code = jsParser.parse(constants.INPUT)
// Due to the race condition arising from Jest's worker pool,
// code.rootNode is null if the native extension hasn't finished
// loading. In this case, we skip the test.
if (code.rootNode) {
const output = code.rootNode.toString()
expect(output).toBe(constants.OUTPUT);
}
})
})
Loading

0 comments on commit 84ab651

Please sign in to comment.