diff --git a/binding.gyp b/binding.gyp index bb43e646..f19ba0db 100644 --- a/binding.gyp +++ b/binding.gyp @@ -7,7 +7,7 @@ " 3) throw new Error( + `Wrong number of arguments to \`#set!\` predicate. Expected 1 or 2. Got ${stepsLength - 1}.` + ); + if (steps.some((s, i) => (i % 2 !== 1) && s !== PREDICATE_STEP_TYPE.STRING)) throw new Error( + `Arguments to \`#set!\` predicate must be a strings.".` + ); + if (!setProperties[i]) setProperties[i] = {}; + setProperties[i][steps[SECOND + 1]] = steps[THIRD] ? steps[THIRD + 1] : null; + break; + + case 'is?': + case 'is-not?': + if (stepsLength < 2 || stepsLength > 3) throw new Error( + `Wrong number of arguments to \`#${operator}\` predicate. Expected 1 or 2. Got ${stepsLength - 1}.` + ); + if (steps.some((s, i) => (i % 2 !== 1) && s !== PREDICATE_STEP_TYPE.STRING)) throw new Error( + `Arguments to \`#${operator}\` predicate must be a strings.".` + ); + const properties = operator === 'is?' ? assertedProperties : refutedProperties; + if (!properties[i]) properties[i] = {}; + properties[i][steps[SECOND + 1]] = steps[THIRD] ? steps[THIRD + 1] : null; + break; + + default: + throw new Error(`Unknown query predicate \`#${steps[FIRST + 1]}\``); + } + } + } + + this.predicates = Object.freeze(predicates); + this.setProperties = Object.freeze(setProperties); + this.assertedProperties = Object.freeze(assertedProperties); + this.refutedProperties = Object.freeze(refutedProperties); +} + +Query.prototype.matches = function(rootNode, startPosition = ZERO_POINT, endPosition = ZERO_POINT) { + marshalNode(rootNode); + const [returnedMatches, returnedNodes] = _matches.call(this, rootNode.tree, + startPosition.row, startPosition.column, + endPosition.row, endPosition.column + ); + const nodes = unmarshalNodes(returnedNodes, rootNode.tree); + const results = []; + + let i = 0 + let nodeIndex = 0; + while (i < returnedMatches.length) { + const patternIndex = returnedMatches[i++]; + const captures = []; + + while (i < returnedMatches.length && typeof returnedMatches[i] === 'string') { + const captureName = returnedMatches[i++]; + captures.push({ + name: captureName, + node: nodes[nodeIndex++], + }) + } + + if (this.predicates[patternIndex].every(p => p(captures))) { + const result = {pattern: patternIndex, captures}; + const setProperties = this.setProperties[patternIndex]; + const assertedProperties = this.assertedProperties[patternIndex]; + const refutedProperties = this.refutedProperties[patternIndex]; + if (setProperties) result.setProperties = setProperties; + if (assertedProperties) result.assertedProperties = assertedProperties; + if (refutedProperties) result.refutedProperties = refutedProperties; + results.push(result); + } + } + + return results; +} + +Query.prototype.captures = function(rootNode, startPosition = ZERO_POINT, endPosition = ZERO_POINT) { + marshalNode(rootNode); + const [returnedMatches, returnedNodes] = _captures.call(this, rootNode.tree, + startPosition.row, startPosition.column, + endPosition.row, endPosition.column + ); + const nodes = unmarshalNodes(returnedNodes, rootNode.tree); + const results = []; + + let i = 0 + let nodeIndex = 0; + while (i < returnedMatches.length) { + const patternIndex = returnedMatches[i++]; + const captureIndex = returnedMatches[i++]; + const captures = []; + + while (i < returnedMatches.length && typeof returnedMatches[i] === 'string') { + const captureName = returnedMatches[i++]; + captures.push({ + name: captureName, + node: nodes[nodeIndex++], + }) + } + + if (this.predicates[patternIndex].every(p => p(captures))) { + const result = captures[captureIndex]; + const setProperties = this.setProperties[patternIndex]; + const assertedProperties = this.assertedProperties[patternIndex]; + const refutedProperties = this.refutedProperties[patternIndex]; + if (setProperties) result.setProperties = setProperties; + if (assertedProperties) result.assertedProperties = assertedProperties; + if (refutedProperties) result.refutedProperties = refutedProperties; + results.push(result); + } + } + + return results; +} + +/* + * Other functions + */ + function getTextFromString (node) { return this.input.substring(node.startIndex, node.endIndex); } @@ -382,37 +616,62 @@ const {pointTransferArray} = binding; const NODE_FIELD_COUNT = 6; const ERROR_TYPE_ID = 0xFFFF -function unmarshalNode(value, tree, offset = 0) { +function getID(buffer, offset) { + const low = BigInt(buffer[offset]); + const high = BigInt(buffer[offset + 1]); + return (high << 32n) + low; +} + +function unmarshalNode(value, tree, offset = 0, cache = null) { + /* case 1: node from the tree cache */ if (typeof value === 'object') { const node = value; return node; - } else { - const nodeTypeId = value; - const NodeClass = nodeTypeId === ERROR_TYPE_ID - ? SyntaxNode - : tree.language.nodeSubclasses[nodeTypeId]; - const {nodeTransferArray} = binding; - if (nodeTransferArray[0] || nodeTransferArray[1]) { - const result = new NodeClass(tree); - for (let i = 0; i < NODE_FIELD_COUNT; i++) { - result[i] = nodeTransferArray[offset + i]; - } - tree._cacheNode(result); - return result; - } + } + + /* case 2: node being transferred */ + const nodeTypeId = value; + const NodeClass = nodeTypeId === ERROR_TYPE_ID + ? SyntaxNode + : tree.language.nodeSubclasses[nodeTypeId]; + + const {nodeTransferArray} = binding; + const id = getID(nodeTransferArray, offset) + if (id === 0n) { return null } + + let cachedResult; + if (cache && (cachedResult = cache.get(id))) + return cachedResult; + + const result = new NodeClass(tree); + for (let i = 0; i < NODE_FIELD_COUNT; i++) { + result[i] = nodeTransferArray[offset + i]; + } + + if (cache) + cache.set(id, result); + else + tree._cacheNode(result); + + return result; } function unmarshalNodes(nodes, tree) { + const cache = new Map(); + let offset = 0; for (let i = 0, {length} = nodes; i < length; i++) { - const node = unmarshalNode(nodes[i], tree, offset); + const node = unmarshalNode(nodes[i], tree, offset, cache); if (node !== nodes[i]) { nodes[i] = node; offset += NODE_FIELD_COUNT } } + + tree._cacheNodes(Array.from(cache.values())); + return nodes; } @@ -491,6 +750,7 @@ function camelCase(name, upperCase) { } module.exports = Parser; +module.exports.Query = Query; module.exports.Tree = Tree; module.exports.SyntaxNode = SyntaxNode; module.exports.TreeCursor = TreeCursor; diff --git a/package.json b/package.json index 8daa62f7..da61a085 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "tree-sitter", - "version": "0.15.13", + "version": "0.20.0", "description": "Incremental parsers for node", "author": "Max Brunsfeld", "license": "MIT", @@ -17,17 +17,19 @@ "dependencies": { "nan": "^2.14.0", "node-addon-api": "git+https://github.com/nodejs/node-addon-api.git", - "prebuild-install": "^5.0.0" + "prebuild-install": "^6.0.1" }, "devDependencies": { - "chai": "3.5.x", - "mocha": "^5.2.0", - "prebuild": "^7.6.0", - "superstring": "^2.4.1", + "@types/node": "^14.14.31", + "chai": "^4.3.3", + "mocha": "^8.3.1", + "prebuild": "^10.0.1", + "superstring": "^2.4.2", "tree-sitter-javascript": "git://github.com/tree-sitter/tree-sitter-javascript.git#master" }, "scripts": { "install": "prebuild-install || node-gyp rebuild", + "build": "node-gyp build", "prebuild": "prebuild -r electron -t 3.0.0 -t 4.0.0 -t 4.0.4 -t 5.0.0 --strip && prebuild -t 10.12.0 -t 12.13.0 --strip", "prebuild:upload": "prebuild --upload-all", "test": "mocha" diff --git a/src/binding.cc b/src/binding.cc index 9ef5ed6f..ee05f659 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -2,6 +2,7 @@ #include "./language.h" #include "./node.h" #include "./parser.h" +#include "./query.h" #include "./tree.h" #include "./tree_cursor.h" #include "./conversions.h" @@ -16,6 +17,7 @@ Object Init(Env env, Object exports) { InitLanguage(exports); InitParser(exports); InitTreeCursor(exports); + Query::Init(exports); Tree::Init(exports); return exports; } diff --git a/src/language.cc b/src/language.cc index 208bd8d6..d24491b5 100644 --- a/src/language.cc +++ b/src/language.cc @@ -14,9 +14,13 @@ using namespace Napi; const TSLanguage *UnwrapLanguage(const Napi::Value &value) { Env env = value.Env(); - const TSLanguage *language = static_cast( - GetInternalFieldPointer(value) - ); + const TSLanguage *language + = value.IsObject() + && value.As().Has("_language") + && value.As().Get("_language").IsExternal() + ? value.As().Get("_language").As>().Data() + : static_cast(GetInternalFieldPointer(value)) + ; if (language) { uint16_t version = ts_language_version(language); diff --git a/src/node.cc b/src/node.cc index e225bac9..afece1a8 100644 --- a/src/node.cc +++ b/src/node.cc @@ -22,6 +22,9 @@ static TSTreeCursor scratch_cursor = {nullptr, nullptr, {0, 0}}; static inline void setup_transfer_buffer(Env env, uint32_t node_count) { uint32_t new_length = node_count * FIELD_COUNT_PER_NODE; if (new_length > transfer_buffer_length) { + if (transfer_buffer) { + free(transfer_buffer); + } transfer_buffer_length = new_length; transfer_buffer = static_cast(malloc(transfer_buffer_length * sizeof(uint32_t))); auto js_transfer_buffer = ArrayBuffer::New( @@ -44,7 +47,8 @@ static inline bool operator<=(const TSPoint &left, const TSPoint &right) { return left.column <= right.column; } -static Value MarshalNodes( + +Value GetMarshalNodes( Env env, const Tree *tree, const TSNode *nodes, @@ -75,7 +79,10 @@ static Value MarshalNodes( return result; } -Value MarshalNode( +Value MarshalNode(Env env, const Tree *tree, TSNode node) { return GetMarshalNode(env, tree, node); } +Value MarshalNodes(Env env, const Tree *tree, const TSNode *nodes, uint32_t node_count) { return GetMarshalNodes(env, tree, nodes, node_count); } + +Value GetMarshalNode( Env env, const Tree *tree, TSNode node @@ -98,6 +105,7 @@ Value MarshalNode( } else { return cache_entry->second->node.Value(); } + return env.Null(); } Value MarshalNullNode(Env env) { @@ -761,6 +769,7 @@ void InitNode(Object &exports) { Env env = exports.Env(); NodeMethods::Init(env, exports); module_exports.Reset(exports, 1); + module_exports.SuppressDestruct(); setup_transfer_buffer(env, 1); } diff --git a/src/node.h b/src/node.h index 1bb95859..09f715b1 100644 --- a/src/node.h +++ b/src/node.h @@ -10,6 +10,8 @@ namespace node_tree_sitter { void InitNode(Napi::Object &exports); Napi::Value MarshalNode(Napi::Env, const Tree *, TSNode); +Napi::Value GetMarshalNode(Napi::Env env, const Tree*, TSNode); +Napi::Value GetMarshalNodes(Napi::Env env, const Tree*, const TSNode*, uint32_t); TSNode UnmarshalNode(Napi::Env env, const Tree *tree); } // namespace node_tree_sitter diff --git a/src/parser.cc b/src/parser.cc index 533f92ee..ee9d454c 100644 --- a/src/parser.cc +++ b/src/parser.cc @@ -51,6 +51,7 @@ class Parser : public ObjectWrap { assert(status == napi_ok); constructor.Reset(ctor, 1); + constructor.SuppressDestruct(); // statics should not destruct // string_slice.Reset(string_slice_fn.As(), 1); exports["Parser"] = ctor; exports["LANGUAGE_VERSION"] = Number::New(env, TREE_SITTER_LANGUAGE_VERSION); diff --git a/src/query.cc b/src/query.cc new file mode 100644 index 00000000..a556a150 --- /dev/null +++ b/src/query.cc @@ -0,0 +1,246 @@ +#include "./query.h" +#include +#include +#include +#include "./node.h" +#include "./language.h" +#include "./logger.h" +#include "./util.h" +#include "./conversions.h" +#include "tree_sitter/api.h" + +namespace node_tree_sitter { + +using std::vector; +using namespace Napi; + +const char *query_error_names[] = { + "TSQueryErrorNone", + "TSQueryErrorSyntax", + "TSQueryErrorNodeType", + "TSQueryErrorField", + "TSQueryErrorCapture", + "TSQueryErrorStructure", +}; + +TSQueryCursor *Query::ts_query_cursor; +Napi::FunctionReference Query::constructor; + +void Query::Init(Napi::Object &exports) { + ts_query_cursor = ts_query_cursor_new(); + Napi::Env env = exports.Env(); + Function ctor = DefineClass(env, "Query", { + InstanceMethod("_matches", &Query::Matches), + InstanceMethod("_captures", &Query::Captures), + InstanceMethod("_getPredicates", &Query::GetPredicates), + }); + + constructor.Reset(ctor, 1); + constructor.SuppressDestruct(); // statics should not destruct + exports["Query"] = ctor; +} + +Query::~Query() { + ts_query_delete(query_); +} + +Query *Query::UnwrapQuery(const Napi::Value &value) { + return Query::Unwrap(value.As()); +} + +Query::Query(const Napi::CallbackInfo& info) + : Napi::ObjectWrap(info) + , query_(nullptr) { + + const TSLanguage *language = UnwrapLanguage(info[0]); + const char *source; + uint32_t source_len; + uint32_t error_offset = 0; + TSQueryError error_type = TSQueryErrorNone; + + if (language == nullptr) { + Napi::TypeError::New(info.Env(), "Missing language argument").ThrowAsJavaScriptException(); + return; + } + + if (info[1].IsString()) { + auto&& utf8_string = info[1].As().Utf8Value(); + source = utf8_string.data(); + source_len = utf8_string.size(); + query_ = ts_query_new(language, source, source_len, &error_offset, &error_type); + } + else if (info[1].IsBuffer()) { + auto&& buffer = info[1].As>(); + source = buffer.Data(); + source_len = buffer.Length(); + query_ = ts_query_new(language, source, source_len, &error_offset, &error_type); + } + else { + Napi::TypeError::New(info.Env(), "Missing source argument").ThrowAsJavaScriptException(); + return; + } + + if (error_offset > 0) { + const char *error_name = query_error_names[error_type]; + std::string message = "Query error of type "; + message += error_name; + message += " at position "; + message += std::to_string(error_offset); + Napi::TypeError::New(info.Env(),message.c_str()).ThrowAsJavaScriptException(); + return; + } + + info.This().As().Get("_init").As().Call(info.This(), {}); +} + +Napi::Value Query::GetPredicates(const Napi::CallbackInfo &info) { + auto pattern_len = ts_query_pattern_count(query_); + + Napi::Array js_predicates = Napi::Array::New(info.Env()); + + for (size_t pattern_index = 0; pattern_index < pattern_len; pattern_index++) { + uint32_t predicates_len; + const TSQueryPredicateStep *predicates = ts_query_predicates_for_pattern( + query_, pattern_index, &predicates_len); + + Napi::Array js_pattern_predicates = Napi::Array::New(info.Env()); + + if (predicates_len > 0) { + Napi::Array js_predicate = Napi::Array::New(info.Env()); + + size_t a_index = 0; + size_t p_index = 0; + for (size_t i = 0; i < predicates_len; i++) { + const TSQueryPredicateStep predicate = predicates[i]; + uint32_t len; + switch (predicate.type) { + case TSQueryPredicateStepTypeCapture: + js_predicate[p_index++] = Napi::Number::New(info.Env(), TSQueryPredicateStepTypeCapture); + js_predicate[p_index++] = Napi::String::New(info.Env(), ts_query_capture_name_for_id(query_, predicate.value_id, &len)); + break; + case TSQueryPredicateStepTypeString: + js_predicate[p_index++] = Napi::Number::New(info.Env(), TSQueryPredicateStepTypeString); + js_predicate[p_index++] = Napi::String::New(info.Env(), ts_query_string_value_for_id(query_, predicate.value_id, &len)); + break; + case TSQueryPredicateStepTypeDone: + js_pattern_predicates[a_index++] = js_predicate; + js_predicate = Napi::Array::New(info.Env()); + p_index = 0; + break; + } + } + } + + js_predicates[pattern_index] = js_pattern_predicates; + } + + return js_predicates; +} + +Napi::Value Query::Matches(const Napi::CallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + uint32_t start_row = info[1].As().Uint32Value(); + uint32_t start_column = info[2].As().Uint32Value() << 1; + uint32_t end_row = info[3].As().Uint32Value(); + uint32_t end_column = info[4].As().Uint32Value() << 1; + + if (tree == nullptr) { + Napi::TypeError::New(info.Env(), "Missing argument tree").ThrowAsJavaScriptException(); + return Env().Null(); + } + + TSNode rootNode = UnmarshalNode(info.Env(), tree); + TSPoint start_point = {start_row, start_column}; + TSPoint end_point = {end_row, end_column}; + ts_query_cursor_set_point_range(ts_query_cursor, start_point, end_point); + ts_query_cursor_exec(ts_query_cursor, query_, rootNode); + + Napi::Array js_matches = Napi::Array::New(info.Env()); + unsigned index = 0; + vector nodes; + TSQueryMatch match; + + while (ts_query_cursor_next_match(ts_query_cursor, &match)) { + js_matches[index++] = Napi::Number::New(info.Env(), match.pattern_index); + + for (uint16_t i = 0; i < match.capture_count; i++) { + const TSQueryCapture &capture = match.captures[i]; + + uint32_t capture_name_len = 0; + const char *capture_name = ts_query_capture_name_for_id( + query_, capture.index, &capture_name_len); + + TSNode node = capture.node; + nodes.push_back(node); + + Napi::Value js_capture = Napi::String::New(info.Env(), capture_name); + js_matches[index++] = js_capture; + } + } + + auto js_nodes = GetMarshalNodes(info.Env(), tree, nodes.data(), nodes.size()); + + auto result = Napi::Array::New(info.Env()); + result[0u] = js_matches; + result[1u] = js_nodes; + return result; +} + +Napi::Value Query::Captures(const Napi::CallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + uint32_t start_row = info[1].As().Uint32Value(); + uint32_t start_column = info[2].As().Uint32Value() << 1; + uint32_t end_row = info[3].As().Uint32Value(); + uint32_t end_column = info[4].As().Uint32Value() << 1; + + if (tree == nullptr) { + Napi::TypeError::New(info.Env(), "Missing argument tree").ThrowAsJavaScriptException(); + return info.Env().Null(); + } + + TSNode rootNode = UnmarshalNode(info.Env(), tree); + TSPoint start_point = {start_row, start_column}; + TSPoint end_point = {end_row, end_column}; + ts_query_cursor_set_point_range(ts_query_cursor, start_point, end_point); + ts_query_cursor_exec(ts_query_cursor, query_, rootNode); + + Napi::Array js_matches = Napi::Array::New(info.Env()); + unsigned index = 0; + vector nodes; + TSQueryMatch match; + uint32_t capture_index; + + while (ts_query_cursor_next_capture( + ts_query_cursor, + &match, + &capture_index + )) { + + js_matches[index++] = Napi::Number::New(info.Env(), match.pattern_index); + js_matches[index++] = Napi::Number::New(info.Env(), capture_index); + + for (uint16_t i = 0; i < match.capture_count; i++) { + const TSQueryCapture &capture = match.captures[i]; + + uint32_t capture_name_len = 0; + const char *capture_name = ts_query_capture_name_for_id( + query_, capture.index, &capture_name_len); + + TSNode node = capture.node; + nodes.push_back(node); + + Napi::String js_capture = Napi::String::New(info.Env(), capture_name); + js_matches[index++] = js_capture; + } + } + + auto js_nodes = GetMarshalNodes(info.Env(), tree, nodes.data(), nodes.size()); + + auto result = Napi::Array::New(info.Env()); + result[0u] = js_matches; + result[1u] = js_nodes; + return result; +} + + +} // namespace node_tree_sitter diff --git a/src/query.h b/src/query.h new file mode 100644 index 00000000..73a66f11 --- /dev/null +++ b/src/query.h @@ -0,0 +1,31 @@ +#ifndef NODE_TREE_SITTER_QUERY_H_ +#define NODE_TREE_SITTER_QUERY_H_ + +#include +#include + +namespace node_tree_sitter { + +class Query : public Napi::ObjectWrap { + public: + static void Init(Napi::Object &); + //static Napi::Value NewInstance(Napi::Env, TSQuery *); + static Query *UnwrapQuery(const Napi::Value &); + + TSQuery *query_; + + explicit Query(const Napi::CallbackInfo &); + ~Query(); + + private: + Napi::Value Matches(const Napi::CallbackInfo &); + Napi::Value Captures(const Napi::CallbackInfo &); + Napi::Value GetPredicates(const Napi::CallbackInfo &); + + static TSQueryCursor *ts_query_cursor; + static Napi::FunctionReference constructor; +}; + +} // namespace node_tree_sitter + +#endif // NODE_TREE_SITTER_QUERY_H_ diff --git a/src/tree.cc b/src/tree.cc index f949755a..ae0c27e7 100644 --- a/src/tree.cc +++ b/src/tree.cc @@ -1,6 +1,6 @@ #include "./tree.h" #include -#include +#include #include "./node.h" #include "./logger.h" #include "./util.h" @@ -22,9 +22,11 @@ void Tree::Init(Object &exports) { InstanceMethod("getChangedRanges", &Tree::GetChangedRanges), InstanceMethod("getEditedRange", &Tree::GetEditedRange), InstanceMethod("_cacheNode", &Tree::CacheNode), + InstanceMethod("_cacheNodes", &Tree::CacheNodes), }); constructor.Reset(ctor, 1); + constructor.SuppressDestruct(); // statics should not destruct exports["Tree"] = ctor; } @@ -169,28 +171,25 @@ Napi::Value Tree::GetEditedRange(const CallbackInfo &info) { } Napi::Value Tree::PrintDotGraph(const CallbackInfo &info) { - ts_tree_print_dot_graph(tree_, stderr); + ts_tree_print_dot_graph(tree_, fileno(stderr)); return info.This(); } static void FinalizeNode(Env env, Tree::NodeCacheEntry *cache_entry) { - assert(!cache_entry->node.IsEmpty()); + //assert(!cache_entry->node.IsEmpty()); cache_entry->node.Reset(); if (cache_entry->tree) { - assert(cache_entry->tree->cached_nodes_.count(cache_entry->key)); + //assert(cache_entry->tree->cached_nodes_.count(cache_entry->key)); cache_entry->tree->cached_nodes_.erase(cache_entry->key); } delete cache_entry; } -Napi::Value Tree::CacheNode(const CallbackInfo &info) { - auto env = info.Env(); - Object js_node = info[0].As(); - +static void CacheNodeForTree(Tree *tree, Napi::Env env, Object js_node) { Napi::Value js_node_field1 = js_node[0u]; Napi::Value js_node_field2 = js_node[1u]; if (!js_node_field1.IsNumber() || !js_node_field2.IsNumber()) { - return env.Undefined(); + return; } uint32_t key_parts[2] = { js_node_field1.As().Uint32Value(), @@ -198,14 +197,31 @@ Napi::Value Tree::CacheNode(const CallbackInfo &info) { }; const void *key = UnmarshalPointer(key_parts); - auto cache_entry = new NodeCacheEntry{this, key, {}}; + auto cache_entry = new Tree::NodeCacheEntry{tree, key, {}}; cache_entry->node.Reset(js_node, 0); js_node.AddFinalizer(&FinalizeNode, cache_entry); - assert(!cached_nodes_.count(key)); + //assert(!cached_nodes_.count(key)); cached_nodes_[key] = cache_entry; return env.Undefined(); } +Napi::Value Tree::CacheNode(const CallbackInfo &info) { + Object js_node = info[0].As(); + CacheNodeForTree(this, info.Env(), js_node); + return info.Env().Undefined(); +} + +Napi::Value Tree::CacheNodes(const CallbackInfo &info) { + Array js_nodes = info[0].As(); + uint32_t length = js_nodes.Length(); + + for (uint32_t i = 0; i < length; i++) { + CacheNodeForTree(this, info.Env(), js_nodes.Get(i).As()); + } + return info.Env().Undefined(); +} + + } // namespace node_tree_sitter diff --git a/src/tree.h b/src/tree.h index 98e4c5eb..e177af36 100644 --- a/src/tree.h +++ b/src/tree.h @@ -33,6 +33,7 @@ class Tree : public Napi::ObjectWrap { Napi::Value GetEditedRange(const Napi::CallbackInfo &); Napi::Value GetChangedRanges(const Napi::CallbackInfo &); Napi::Value CacheNode(const Napi::CallbackInfo &); + Napi::Value CacheNodes(const Napi::CallbackInfo &); static Napi::FunctionReference constructor; }; diff --git a/src/tree_cursor.cc b/src/tree_cursor.cc index c3f5679c..e4801b4f 100644 --- a/src/tree_cursor.cc +++ b/src/tree_cursor.cc @@ -33,6 +33,7 @@ class TreeCursor : public Napi::ObjectWrap { }); constructor.Reset(ctor, 1); + constructor.SuppressDestruct(); // statics should not destruct exports.Set("TreeCursor", ctor); } diff --git a/src/util.cc b/src/util.cc new file mode 100644 index 00000000..41133c66 --- /dev/null +++ b/src/util.cc @@ -0,0 +1,14 @@ +#include +#include +#include "./util.h" + +namespace node_tree_sitter { + +bool instance_of(v8::Local value, v8::Local object) { + auto maybe_bool = value->InstanceOf(Nan::GetCurrentContext(), object); + if (maybe_bool.IsNothing()) + return false; + return maybe_bool.FromJust(); +} + +} // namespace node_tree_sitter diff --git a/src/util.h b/src/util.h index 6c1ebd4d..1d78b06b 100644 --- a/src/util.h +++ b/src/util.h @@ -50,6 +50,8 @@ static inline void *GetInternalFieldPointer(Napi::Value value) { return nullptr; } +bool instance_of(v8::Local value, v8::Local object); + } // namespace node_tree_sitter #endif // NODE_TREE_SITTER_UTIL_H_ diff --git a/test/query_test.js b/test/query_test.js new file mode 100644 index 00000000..35dc3d85 --- /dev/null +++ b/test/query_test.js @@ -0,0 +1,194 @@ +const fs = require("fs"); +const Parser = require(".."); +const JavaScript = require("tree-sitter-javascript"); +const { assert } = require("chai"); +const {Query, QueryCursor} = Parser + +describe("Query", () => { + + const parser = new Parser(); + parser.setLanguage(JavaScript); + + describe("new", () => { + it("works with string", () => { + const query = new Query(JavaScript, ` + (function_declaration name: (identifier) @fn-def) + (call_expression function: (identifier) @fn-ref) + `); + }); + + it("works with Buffer", () => { + const query = new Query(JavaScript, Buffer.from(` + (function_declaration name: (identifier) @fn-def) + (call_expression function: (identifier) @fn-ref) + `)); + }); + }); + + describe(".matches", () => { + it("returns all of the matches for the given query", () => { + const tree = parser.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); + assert.deepEqual(formatMatches(tree, matches), [ + { pattern: 0, captures: [{ name: "fn-def", text: "one" }] }, + { pattern: 1, captures: [{ name: "fn-ref", text: "two" }] }, + { pattern: 0, captures: [{ name: "fn-def", text: "three" }] }, + ]); + }); + + it("can search in a specified ranges", () => { + const tree = parser.parse("[a, b,\nc, d,\ne, f,\ng, h]"); + const query = new Query(JavaScript, "(identifier) @element"); + const matches = query.matches( + tree.rootNode, + { row: 1, column: 1 }, + { row: 3, column: 1 } + ); + assert.deepEqual(formatMatches(tree, matches), [ + { pattern: 0, captures: [{ name: "element", text: "d" }] }, + { pattern: 0, captures: [{ name: "element", text: "e" }] }, + { pattern: 0, captures: [{ name: "element", text: "f" }] }, + { pattern: 0, captures: [{ name: "element", text: "g" }] }, + ]); + }); + }); + + describe(".captures", () => { + it("returns all of the captures for the given query, in order", () => { + const tree = parser.parse(` + a({ + bc: function de() { + const fg = function hi() {} + }, + jk: function lm() { + const no = function pq() {} + }, + }); + `); + const query = new Query(JavaScript, ` + (pair + key: _ @method.def + (function + name: (identifier) @method.alias)) + (variable_declarator + name: _ @function.def + value: (function + name: (identifier) @function.alias)) + ":" @delimiter + "=" @operator + `); + + const captures = query.captures(tree.rootNode); + assert.deepEqual(formatCaptures(tree, captures), [ + { name: "method.def", text: "bc" }, + { name: "delimiter", text: ":" }, + { name: "method.alias", text: "de" }, + { name: "function.def", text: "fg" }, + { name: "operator", text: "=" }, + { name: "function.alias", text: "hi" }, + { name: "method.def", text: "jk" }, + { name: "delimiter", text: ":" }, + { name: "method.alias", text: "lm" }, + { name: "function.def", text: "no" }, + { name: "operator", text: "=" }, + { name: "function.alias", text: "pq" }, + ]); + }); + + it("handles conditions that compare the text of capture to literal strings", () => { + const tree = parser.parse(` + const ab = require('./ab'); + new Cd(EF); + `); + + const query = new Query(JavaScript, ` + (identifier) @variable + ((identifier) @function.builtin + (#eq? @function.builtin "require")) + ((identifier) @constructor + (#match? @constructor "^[A-Z]")) + ((identifier) @constant + (#match? @constant "^[A-Z]{2,}$")) + `); + + const captures = query.captures(tree.rootNode); + assert.deepEqual(formatCaptures(tree, captures), [ + { name: "variable", text: "ab" }, + { name: "variable", text: "require" }, + { name: "function.builtin", text: "require" }, + { name: "variable", text: "Cd" }, + { name: "constructor", text: "Cd" }, + { name: "variable", text: "EF" }, + { name: "constructor", text: "EF" }, + { name: "constant", text: "EF" }, + ]); + }); + + it("handles conditions that compare the text of capture to each other", () => { + const tree = parser.parse(` + ab = abc + 1; + def = de + 1; + ghi = ghi + 1; + `); + + const query = new Query(JavaScript, ` + ( + (assignment_expression + left: (identifier) @id1 + right: (binary_expression + left: (identifier) @id2)) + (#eq? @id1 @id2) + ) + `); + + const captures = query.captures(tree.rootNode); + assert.deepEqual(formatCaptures(tree, captures), [ + { name: "id1", text: "ghi" }, + { name: "id2", text: "ghi" }, + ]); + }); + + it("handles patterns with properties", () => { + const tree = parser.parse(`a(b.c);`); + const query = new Query(JavaScript, ` + ((call_expression (identifier) @func) + (#set! foo) + (#set! bar baz)) + ((property_identifier) @prop + (#is? foo) + (#is-not? bar baz)) + `); + + const captures = query.captures(tree.rootNode); + assert.deepEqual(formatCaptures(tree, captures), [ + { name: "func", text: "a", setProperties: { foo: null, bar: "baz" } }, + { + name: "prop", + text: "c", + assertedProperties: { foo: null }, + refutedProperties: { bar: "baz" }, + }, + ]); + }); + }); +}); + +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; + }); +} diff --git a/tree-sitter.d.ts b/tree-sitter.d.ts index e366db98..3d8bc73c 100644 --- a/tree-sitter.d.ts +++ b/tree-sitter.d.ts @@ -1,10 +1,13 @@ declare module "tree-sitter" { class Parser { - parse(input: string | Parser.Input, previousTree?: Parser.Tree): Parser.Tree; + parse(input: string | Parser.Input | Parser.InputReader, oldTree?: Parser.Tree, options?: { bufferSize?: number, includedRanges?: Parser.Range[] }): Parser.Tree; + parseTextBuffer(buffer: Parser.TextBuffer, oldTree?: Parser.Tree, options?: { syncTimeoutMicros?: number, includedRanges?: Parser.Range[] }): Parser.Tree | Promise; + parseTextBufferSync(buffer: Parser.TextBuffer, oldTree?: Parser.Tree, options?: { includedRanges?: Parser.Range[] }): Parser.Tree; getLanguage(): any; setLanguage(language: any): void; getLogger(): Parser.Logger; setLogger(logFunc: Parser.Logger): void; + printDotGraphs(enabled: boolean): void; } namespace Parser { @@ -14,8 +17,10 @@ declare module "tree-sitter" { }; export type Range = { - start: Point; - end: Point; + startIndex: number, + endIndex: number, + startPosition: Point, + endPosition: Point }; export type Edit = { @@ -33,6 +38,12 @@ declare module "tree-sitter" { type: "parse" | "lex" ) => void; + export type TextBuffer = Buffer; + + export interface InputReader { + (index: any, position: Point): string; + } + export interface Input { seek(index: number): void; read(): any; @@ -41,6 +52,7 @@ declare module "tree-sitter" { export interface SyntaxNode { tree: Tree; type: string; + typeId: string; isNamed: boolean; text: string; startPosition: Point; @@ -92,7 +104,8 @@ declare module "tree-sitter" { endPosition: Point; startIndex: number; endIndex: number; - readonly currentNode: SyntaxNode + readonly currentNode: SyntaxNode; + readonly currentFieldName: string; reset(node: SyntaxNode): void gotoParent(): boolean; @@ -108,6 +121,33 @@ declare module "tree-sitter" { walk(): TreeCursor; getChangedRanges(other: Tree): Range[]; getEditedRange(other: Tree): Range; + printDotGraph(): void; + } + + export interface QueryMatch { + pattern: number, + captures: QueryCapture[], + } + + export interface QueryCapture { + name: string, + text?: string, + node: SyntaxNode, + setProperties?: {[prop: string]: string | null}, + assertedProperties?: {[prop: string]: string | null}, + refutedProperties?: {[prop: string]: string | null}, + } + + export class Query { + readonly predicates: { [name: string]: Function }[]; + readonly setProperties: any[]; + readonly assertedProperties: any[]; + readonly refutedProperties: any[]; + + constructor(language: any, source: string | Buffer); + + matches(rootNode: SyntaxNode, startPosition?: Point, endPosition?: Point): QueryMatch[]; + captures(rootNode: SyntaxNode, startPosition?: Point, endPosition?: Point): QueryCapture[]; } } diff --git a/vendor/tree-sitter b/vendor/tree-sitter index 3214de55..c51896d3 160000 --- a/vendor/tree-sitter +++ b/vendor/tree-sitter @@ -1 +1 @@ -Subproject commit 3214de55f06d292babfdc932d2ffbb66c7d16a33 +Subproject commit c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14