diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index e86841aa..364b431f 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -16,7 +16,6 @@ env: ELECTRON_PREBUILD_CMD: npx prebuild -r electron -t 3.0.0 -t 4.0.0 -t 5.0.0 -t 6.0.0 -t 7.0.0 -t 8.0.0 -t 9.0.0 -t 10.0.0 -t 11.0.0 -t 12.0.0 -t 13.0.0 -t 14.0.0 -t 15.0.0 -t 16.0.0 -t 17.0.0 -t 18.0.0 -t 19.0.0 -t 20.0.0 -t 21.0.0 -t 22.0.0 -t 23.0.0 -t 24.0.0 -t 25.0.0 --strip -u ${{ secrets.GH_TOKEN }} jobs: - test: strategy: matrix: diff --git a/binding.gyp b/binding.gyp index 5e3d479a..9ad82a05 100644 --- a/binding.gyp +++ b/binding.gyp @@ -8,6 +8,7 @@ "src/conversions.cc", "src/language.cc", "src/logger.cc", + "src/lookaheaditerator.cc", "src/node.cc", "src/parser.cc", "src/query.cc", diff --git a/index.js b/index.js index 3893b260..89f91570 100644 --- a/index.js +++ b/index.js @@ -10,13 +10,13 @@ try { } const util = require('util') -const {Query, Parser, NodeMethods, Tree, TreeCursor} = binding; +const {Query, Parser, NodeMethods, Tree, TreeCursor, LookaheadIterator} = binding; /* * Tree */ -const {rootNode, edit} = Tree.prototype; +const {rootNode, rootNodeWithOffset, edit} = Tree.prototype; Object.defineProperty(Tree.prototype, 'rootNode', { get() { @@ -24,6 +24,10 @@ Object.defineProperty(Tree.prototype, 'rootNode', { } }); +Tree.prototype.rootNodeWithOffset = function(offset_bytes, offset_extent) { + return unmarshalNode(rootNodeWithOffset.call(this, offset_bytes, offset_extent.row, offset_extent.column), this); +} + Tree.prototype.edit = function(arg) { edit.call( this, @@ -58,9 +62,9 @@ class SyntaxNode { '}' } - get type() { + get id() { marshalNode(this); - return NodeMethods.type(this.tree); + return NodeMethods.id(this.tree); } get typeId() { @@ -68,6 +72,21 @@ class SyntaxNode { return NodeMethods.typeId(this.tree); } + get grammarId() { + marshalNode(this); + return NodeMethods.grammarId(this.tree); + } + + get type() { + marshalNode(this); + return NodeMethods.type(this.tree); + } + + get grammarName() { + marshalNode(this); + return NodeMethods.grammarName(this.tree); + } + get isNamed() { marshalNode(this); return NodeMethods.isNamed(this.tree); @@ -164,6 +183,21 @@ class SyntaxNode { return unmarshalNode(NodeMethods.previousNamedSibling(this.tree), this.tree); } + get parseState() { + marshalNode(this); + return NodeMethods.parseState(this.tree); + } + + get nextParseState() { + marshalNode(this); + return NodeMethods.nextParseState(this.tree); + } + + get descendantCount() { + marshalNode(this); + return NodeMethods.descendantCount(this.tree); + } + hasChanges() { marshalNode(this); return NodeMethods.hasChanges(this.tree); @@ -179,6 +213,16 @@ class SyntaxNode { return NodeMethods.isMissing(this.tree); } + isExtra() { + marshalNode(this); + return NodeMethods.isExtra(this.tree); + } + + isError() { + marshalNode(this); + return NodeMethods.isError(this.tree); + } + toString() { marshalNode(this); return NodeMethods.toString(this.tree); @@ -194,6 +238,31 @@ class SyntaxNode { return unmarshalNode(NodeMethods.namedChild(this.tree, index), this.tree); } + childForFieldName(fieldName) { + marshalNode(this); + return unmarshalNode(NodeMethods.childForFieldName(this.tree, fieldName), this.tree); + } + + childForFieldId(fieldId) { + marshalNode(this); + return unmarshalNode(NodeMethods.childForFieldId(this.tree, fieldId), this.tree); + } + + fieldNameForChild(childIndex) { + marshalNode(this); + return NodeMethods.fieldNameForChild(this.tree, childIndex); + } + + childrenForFieldName(fieldName, cursor) { + marshalNode(this); + return unmarshalNodes(NodeMethods.childrenForFieldName(this.tree, fieldName, cursor), this.tree); + } + + childrenForFieldId(fieldId) { + marshalNode(this); + return unmarshalNodes(NodeMethods.childrenForFieldId(this.tree, fieldId), this.tree); + } + firstChildForIndex(index) { marshalNode(this); return unmarshalNode(NodeMethods.firstChildForIndex(this.tree, index), this.tree); @@ -282,7 +351,7 @@ Parser.prototype.parse = function(input, oldTree, {bufferSize, includedRanges}={ input, oldTree, bufferSize, - includedRanges + includedRanges, ); if (tree) { tree.input = treeInput @@ -467,11 +536,22 @@ Query.prototype._init = function() { this.refutedProperties = Object.freeze(refutedProperties); } -Query.prototype.matches = function(rootNode, startPosition = ZERO_POINT, endPosition = ZERO_POINT) { +Query.prototype.matches = function( + rootNode, + { + startPosition = ZERO_POINT, + endPosition = ZERO_POINT, + startIndex = 0, + endIndex = 0, + matchLimit = 0xFFFFFFFF, + maxStartDepth = 0xFFFFFFFF + } = {} +) { marshalNode(rootNode); const [returnedMatches, returnedNodes] = _matches.call(this, rootNode.tree, startPosition.row, startPosition.column, - endPosition.row, endPosition.column + endPosition.row, endPosition.column, + startIndex, endIndex, matchLimit, maxStartDepth ); const nodes = unmarshalNodes(returnedNodes, rootNode.tree); const results = []; @@ -505,11 +585,22 @@ Query.prototype.matches = function(rootNode, startPosition = ZERO_POINT, endPosi return results; } -Query.prototype.captures = function(rootNode, startPosition = ZERO_POINT, endPosition = ZERO_POINT) { +Query.prototype.captures = function( + rootNode, + { + startPosition = ZERO_POINT, + endPosition = ZERO_POINT, + startIndex = 0, + endIndex = 0, + matchLimit = 0xFFFFFFFF, + maxStartDepth = 0xFFFFFFFF + } = {} +) { marshalNode(rootNode); const [returnedMatches, returnedNodes] = _captures.call(this, rootNode.tree, startPosition.row, startPosition.column, - endPosition.row, endPosition.column + endPosition.row, endPosition.column, + startIndex, endIndex, matchLimit, maxStartDepth ); const nodes = unmarshalNodes(returnedNodes, rootNode.tree); const results = []; @@ -709,3 +800,4 @@ module.exports.Query = Query; module.exports.Tree = Tree; module.exports.SyntaxNode = SyntaxNode; module.exports.TreeCursor = TreeCursor; +module.exports.LookaheadIterator = LookaheadIterator; diff --git a/package.json b/package.json index 67651762..3cb5c29c 100644 --- a/package.json +++ b/package.json @@ -19,12 +19,21 @@ "prebuild-install": "^7.1.1" }, "devDependencies": { - "@types/node": "^20.5.9", + "@types/node": "^20.6.0", "chai": "^4.3.8", - "mocha": "^8.4.0", + "mocha": "^10.2.0", "node-gyp": "^9.4.0", "prebuild": "^12.0.0", - "tree-sitter-javascript": "https://github.com/tree-sitter/tree-sitter-javascript.git#master" + "tmp": "^0.2.1", + "tree-sitter-c": "https://github.com/tree-sitter/tree-sitter-c.git#master", + "tree-sitter-embedded-template": "https://github.com/tree-sitter/tree-sitter-embedded-template.git#master", + "tree-sitter-html": "https://github.com/tree-sitter/tree-sitter-html.git#master", + "tree-sitter-java": "https://github.com/tree-sitter/tree-sitter-java.git#master", + "tree-sitter-javascript": "https://github.com/tree-sitter/tree-sitter-javascript.git#master", + "tree-sitter-json": "https://github.com/tree-sitter/tree-sitter-json.git#master", + "tree-sitter-python": "https://github.com/tree-sitter/tree-sitter-python.git#master", + "tree-sitter-ruby": "https://github.com/tree-sitter/tree-sitter-ruby.git#master", + "tree-sitter-rust": "https://github.com/tree-sitter/tree-sitter-rust.git#master" }, "scripts": { "install": "prebuild-install || node-gyp rebuild", diff --git a/src/binding.cc b/src/binding.cc index 7db7ebe4..c6ad3550 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -1,12 +1,14 @@ -#include -#include #include "./language.h" +#include "./conversions.h" +#include "./lookaheaditerator.h" #include "./node.h" #include "./parser.h" #include "./query.h" #include "./tree.h" #include "./tree_cursor.h" -#include "./conversions.h" + +#include +#include namespace node_tree_sitter { @@ -16,6 +18,7 @@ void InitAll(Local exports, Local m_, void* v_) { InitConversions(exports); node_methods::Init(exports); language_methods::Init(exports); + LookaheadIterator::Init(exports); Parser::Init(exports); Query::Init(exports); Tree::Init(exports); diff --git a/src/conversions.cc b/src/conversions.cc index 1e013eaa..89e4ea06 100644 --- a/src/conversions.cc +++ b/src/conversions.cc @@ -1,9 +1,10 @@ #include "./node.h" -#include -#include -#include #include "./conversions.h" +#include "tree_sitter/api.h" + #include +#include +#include namespace node_tree_sitter { @@ -16,8 +17,10 @@ Nan::Persistent start_position_key; Nan::Persistent end_index_key; Nan::Persistent end_position_key; -static unsigned BYTES_PER_CHARACTER = 2; -static uint32_t *point_transfer_buffer; +namespace { + const unsigned BYTES_PER_CHARACTER = 2; + uint32_t *point_transfer_buffer; +} // namespace void InitConversions(Local exports) { row_key.Reset(Nan::Persistent(Nan::New("row").ToLocalChecked())); @@ -27,7 +30,7 @@ void InitConversions(Local exports) { end_index_key.Reset(Nan::Persistent(Nan::New("endIndex").ToLocalChecked())); end_position_key.Reset(Nan::Persistent(Nan::New("endPosition").ToLocalChecked())); - point_transfer_buffer = static_cast(malloc(2 * sizeof(uint32_t))); + point_transfer_buffer = new uint32_t[2]; #if defined(_MSC_VER) && NODE_RUNTIME_ELECTRON && NODE_MODULE_VERSION >= 89 auto nodeBuffer = node::Buffer::New(Isolate::GetCurrent(), (char *)point_transfer_buffer, 2 * sizeof(uint32_t), [](char *data, void *hint) {}, nullptr) @@ -74,9 +77,9 @@ Nan::Maybe RangeFromJS(const Local &arg) { Nan::ThrowTypeError("Range must be a {startPosition, endPosition, startIndex, endIndex} object"); \ return Nan::Nothing(); \ } \ - auto field = Convert(value.ToLocalChecked()); \ - if (field.IsJust()) { \ - result.field = field.FromJust(); \ + auto (field) = Convert(value.ToLocalChecked()); \ + if ((field).IsJust()) { \ + result.field = (field).FromJust(); \ } else { \ return Nan::Nothing(); \ } \ diff --git a/src/conversions.h b/src/conversions.h index 69bcfb51..1fbff3d7 100644 --- a/src/conversions.h +++ b/src/conversions.h @@ -1,9 +1,10 @@ #ifndef NODE_TREE_SITTER_CONVERSIONS_H_ #define NODE_TREE_SITTER_CONVERSIONS_H_ +#include "tree_sitter/api.h" + #include #include -#include namespace node_tree_sitter { diff --git a/src/language.cc b/src/language.cc index bdfb90f1..bc841519 100644 --- a/src/language.cc +++ b/src/language.cc @@ -1,22 +1,21 @@ #include "./language.h" +#include "tree_sitter/api.h" + #include -#include -#include #include #include +#include -namespace node_tree_sitter { -namespace language_methods { +namespace node_tree_sitter::language_methods { -using std::vector; using namespace v8; const TSLanguage *UnwrapLanguage(const v8::Local &value) { if (value->IsObject()) { Local arg = Local::Cast(value); if (arg->InternalFieldCount() == 1) { - const TSLanguage *language = (const TSLanguage *)Nan::GetInternalFieldPointer(arg, 0); - if (language) { + const auto *language = static_cast(Nan::GetInternalFieldPointer(arg, 0)); + if (language != nullptr) { uint16_t version = ts_language_version(language); if ( version < TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION || @@ -38,9 +37,12 @@ const TSLanguage *UnwrapLanguage(const v8::Local &value) { return nullptr; } -static void GetNodeTypeNamesById(const Nan::FunctionCallbackInfo &info) { +namespace { +void GetNodeTypeNamesById(const Nan::FunctionCallbackInfo &info) { const TSLanguage *language = UnwrapLanguage(info[0]); - if (!language) return; + if (language == nullptr) { + return; + } auto result = Nan::New(); uint32_t length = ts_language_symbol_count(language); @@ -57,15 +59,17 @@ static void GetNodeTypeNamesById(const Nan::FunctionCallbackInfo &info) { info.GetReturnValue().Set(result); } -static void GetNodeFieldNamesById(const Nan::FunctionCallbackInfo &info) { +void GetNodeFieldNamesById(const Nan::FunctionCallbackInfo &info) { const TSLanguage *language = UnwrapLanguage(info[0]); - if (!language) return; + if (language == nullptr) { + return; + } auto result = Nan::New(); uint32_t length = ts_language_field_count(language); for (uint32_t i = 0; i < length + 1; i++) { const char *name = ts_language_field_name_for_id(language, i); - if (name) { + if (name != nullptr) { Nan::Set(result, i, Nan::New(name).ToLocalChecked()); } else { Nan::Set(result, i, Nan::Null()); @@ -73,6 +77,7 @@ static void GetNodeFieldNamesById(const Nan::FunctionCallbackInfo &info) } info.GetReturnValue().Set(result); } +} // namespace void Init(Local exports) { Nan::Set( @@ -88,5 +93,4 @@ void Init(Local exports) { ); } -} // namespace language_methods -} // namespace node_tree_sitter +} // namespace node_tree_sitter::language_methods diff --git a/src/language.h b/src/language.h index 76f7ac28..c778aedb 100644 --- a/src/language.h +++ b/src/language.h @@ -1,20 +1,19 @@ #ifndef NODE_TREE_SITTER_LANGUAGE_H_ #define NODE_TREE_SITTER_LANGUAGE_H_ +#include "tree_sitter/api.h" +#include "./tree.h" + #include -#include #include -#include -#include "./tree.h" +#include -namespace node_tree_sitter { -namespace language_methods { +namespace node_tree_sitter::language_methods { void Init(v8::Local); const TSLanguage *UnwrapLanguage(const v8::Local &); -} // namespace language_methods -} // namespace node_tree_sitter +} // namespace node_tree_sitter::language_methods #endif // NODE_TREE_SITTER_LANGUAGE_H_ diff --git a/src/logger.cc b/src/logger.cc index 9c3d47da..77372cf9 100644 --- a/src/logger.cc +++ b/src/logger.cc @@ -1,9 +1,10 @@ #include "./logger.h" +#include "./util.h" +#include "tree_sitter/api.h" + +#include #include #include -#include -#include -#include "./util.h" namespace node_tree_sitter { @@ -11,10 +12,11 @@ using namespace v8; using std::string; void Logger::Log(void *payload, TSLogType type, const char *message_str) { - Logger *debugger = (Logger *)payload; + auto *debugger = static_cast(payload); Local fn = Nan::New(debugger->func); - if (!fn->IsFunction()) + if (!fn->IsFunction()) { return; + } string message(message_str); string param_sep = " "; @@ -26,10 +28,11 @@ void Logger::Log(void *payload, TSLogType type, const char *message_str) { while (param_sep_pos != string::npos) { size_t key_pos = param_sep_pos + param_sep.size(); - size_t value_sep_pos = message.find(":", key_pos); + size_t value_sep_pos = message.find(':', key_pos); - if (value_sep_pos == string::npos) + if (value_sep_pos == string::npos) { break; + } size_t val_pos = value_sep_pos + 1; param_sep = ", "; @@ -57,9 +60,9 @@ void Logger::Log(void *payload, TSLogType type, const char *message_str) { TSLogger Logger::Make(Local func) { TSLogger result; - Logger *logger = new Logger(); + auto *logger = new Logger(); logger->func.Reset(Nan::Persistent(func)); - result.payload = (void *)logger; + result.payload = static_cast(logger); result.log = Log; return result; } diff --git a/src/logger.h b/src/logger.h index af68413f..f3c2be8c 100644 --- a/src/logger.h +++ b/src/logger.h @@ -1,13 +1,13 @@ #ifndef NODE_TREE_SITTER_LOGGER_H_ #define NODE_TREE_SITTER_LOGGER_H_ -#include +#include "tree_sitter/api.h" #include -#include +#include namespace node_tree_sitter { -class Logger { +class Logger final { public: static TSLogger Make(v8::Local); Nan::Persistent func; diff --git a/src/lookaheaditerator.cc b/src/lookaheaditerator.cc new file mode 100644 index 00000000..31301bdf --- /dev/null +++ b/src/lookaheaditerator.cc @@ -0,0 +1,176 @@ +#include "./lookaheaditerator.h" +#include "./language.h" +#include "./util.h" + +#include +#include +#include + +namespace node_tree_sitter { + +using namespace v8; + +Nan::Persistent LookaheadIterator::constructor; +Nan::Persistent LookaheadIterator::constructor_template; + +void LookaheadIterator::Init(Local exports) { + Local tpl = Nan::New(New); + tpl->InstanceTemplate()->SetInternalFieldCount(1); + Local class_name = Nan::New("LookaheadIterator").ToLocalChecked(); + tpl->SetClassName(class_name); + + GetterPair getters[] = { + {"currentSymbol", CurrentSymbol}, + {"currentSymbolName", CurrentSymbolName}, + }; + + FunctionPair methods[] = { + {"reset", Reset}, + {"resetState", ResetState}, + {"next", Next}, + {"iterNames", IterNames}, + }; + + for (auto &getter : getters) { + Nan::SetAccessor( + tpl->InstanceTemplate(), + Nan::New(getter.name).ToLocalChecked(), + getter.callback + ); + } + + for (auto &method : methods) { + Nan::SetPrototypeMethod(tpl, method.name, method.callback); + } + + Local ctor = Nan::GetFunction(tpl).ToLocalChecked(); + + constructor_template.Reset(tpl); + constructor.Reset(ctor); + Nan::Set(exports, class_name, ctor); +} + +LookaheadIterator::LookaheadIterator(TSLookaheadIterator *iterator) : iterator_(iterator) {} + +LookaheadIterator::~LookaheadIterator() { + ts_lookahead_iterator_delete(iterator_); +} + +Local LookaheadIterator::NewInstance(TSLookaheadIterator *iterator) { + if (iterator != nullptr) { + Local self; + MaybeLocal maybe_self = Nan::NewInstance(Nan::New(constructor)); + if (maybe_self.ToLocal(&self)) { + (new LookaheadIterator(iterator))->Wrap(self); + return self; + } + } + return Nan::Null(); +} + +LookaheadIterator *LookaheadIterator::UnwrapLookaheadIterator(const v8::Local &value) { + if (!value->IsObject()) { + return nullptr; + } + Local js_iterator = Local::Cast(value); + if (!Nan::New(constructor_template)->HasInstance(js_iterator)) { + return nullptr; + } + return ObjectWrap::Unwrap(js_iterator); +} + +void LookaheadIterator::New(const Nan::FunctionCallbackInfo &info) { + if (!info.IsConstructCall()) { + Local self; + MaybeLocal maybe_self = Nan::NewInstance(Nan::New(constructor)); + if (maybe_self.ToLocal(&self)) { + info.GetReturnValue().Set(self); + } else { + info.GetReturnValue().Set(Nan::Null()); + } + return; + } + + const TSLanguage *language = language_methods::UnwrapLanguage(info[0]); + + if (language == nullptr) { + Nan::ThrowError("Missing language argument"); + return; + } + + if (!info[1]->IsNumber()) { + Nan::ThrowError("Missing state argument"); + return; + } + TSStateId state = Nan::To(info[1]).ToChecked(); + + TSLookaheadIterator *iterator = ts_lookahead_iterator_new(language, state); + if (iterator == nullptr) { + Nan::ThrowError("Invalid state argument"); + return; + } + auto self = info.This(); + auto *lookahead_iterator_wrapper = new LookaheadIterator(ts_lookahead_iterator_new(language, state)); + lookahead_iterator_wrapper->Wrap(self); + + info.GetReturnValue().Set(self); +} + +void LookaheadIterator::CurrentSymbolName(Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + LookaheadIterator *iterator = UnwrapLookaheadIterator(info.This()); + const char *name = ts_lookahead_iterator_current_symbol_name(iterator->iterator_); + info.GetReturnValue().Set(Nan::New(name).ToLocalChecked()); +} + +void LookaheadIterator::CurrentSymbol(Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + LookaheadIterator *iterator = UnwrapLookaheadIterator(info.This()); + TSSymbol symbol = ts_lookahead_iterator_current_symbol(iterator->iterator_); + info.GetReturnValue().Set(Nan::New(symbol)); +} + +void LookaheadIterator::Reset(const Nan::FunctionCallbackInfo &info) { + LookaheadIterator *iterator = UnwrapLookaheadIterator(info.This()); + const TSLanguage *language = language_methods::UnwrapLanguage(info[0]); + if (language == nullptr) { + Nan::ThrowError("Invalid language argument"); + return; + } + if (!info[1]->IsNumber()) { + Nan::ThrowError("Missing state argument"); + return; + } + TSStateId state = Nan::To(info[1]).ToChecked(); + info.GetReturnValue().Set(Nan::New(ts_lookahead_iterator_reset(iterator->iterator_, language, state))); +} + +void LookaheadIterator::ResetState(const Nan::FunctionCallbackInfo &info) { + LookaheadIterator *iterator = UnwrapLookaheadIterator(info.This()); + if (!info[0]->IsNumber()) { + Nan::ThrowError("Missing state argument"); + return; + } + TSStateId state = Nan::To(info[0]).ToChecked(); + info.GetReturnValue().Set(Nan::New(ts_lookahead_iterator_reset_state(iterator->iterator_, state))); +} + +void LookaheadIterator::Next(const Nan::FunctionCallbackInfo &info) { + LookaheadIterator *iterator = UnwrapLookaheadIterator(info.This()); + bool result = ts_lookahead_iterator_next(iterator->iterator_); + if (!result) { + info.GetReturnValue().Set(Nan::Null()); + return; + } + info.GetReturnValue().Set(Nan::New(ts_lookahead_iterator_current_symbol(iterator->iterator_))); +} + +void LookaheadIterator::IterNames(const Nan::FunctionCallbackInfo &info) { + LookaheadIterator *iterator = UnwrapLookaheadIterator(info.This()); + auto result = Nan::New(); + while (ts_lookahead_iterator_next(iterator->iterator_)) { + const char *name = ts_lookahead_iterator_current_symbol_name(iterator->iterator_); + Nan::Set(result, result->Length(), Nan::New(name).ToLocalChecked()); + } + info.GetReturnValue().Set(result); +} + +} // namespace node_tree_sitter diff --git a/src/lookaheaditerator.h b/src/lookaheaditerator.h new file mode 100644 index 00000000..c32d5c9a --- /dev/null +++ b/src/lookaheaditerator.h @@ -0,0 +1,41 @@ +#ifndef NODE_TREE_SITTER_LOOKAHEAD_ITERATOR_H_ +#define NODE_TREE_SITTER_LOOKAHEAD_ITERATOR_H_ + +#include "tree_sitter/api.h" + +#include +#include +#include + +namespace node_tree_sitter { + +class LookaheadIterator final : public Nan::ObjectWrap { +public: + static void Init(v8::Local exports); + static v8::Local NewInstance(TSLookaheadIterator *); + static LookaheadIterator *UnwrapLookaheadIterator(const v8::Local &); + + TSLookaheadIterator *iterator_; + +private: + explicit LookaheadIterator(TSLookaheadIterator *); + ~LookaheadIterator() final; + + static void New(const Nan::FunctionCallbackInfo &); + static void Reset(const Nan::FunctionCallbackInfo &); + static void ResetState(const Nan::FunctionCallbackInfo &); + static void Next(const Nan::FunctionCallbackInfo &); + static void IterNames(const Nan::FunctionCallbackInfo &); + + static void CurrentSymbol(v8::Local, const Nan::PropertyCallbackInfo &); + static void CurrentSymbolName(v8::Local, const Nan::PropertyCallbackInfo &); + + static Nan::Persistent constructor; + static Nan::Persistent constructor_template; +}; + +} // namespace node_tree_sitter + + + +#endif // NODE_TREE_SITTER_LOOKAHEAD_ITERATOR_H_ diff --git a/src/node.cc b/src/node.cc index c45b5cf6..73fd8109 100644 --- a/src/node.cc +++ b/src/node.cc @@ -1,34 +1,33 @@ #include "./node.h" -#include -#include -#include -#include -#include "./util.h" #include "./conversions.h" #include "./tree.h" #include "./tree_cursor.h" +#include "./util.h" +#include "tree_sitter/api.h" -namespace node_tree_sitter { -namespace node_methods { +#include +#include +#include + +namespace node_tree_sitter::node_methods { using std::vector; using namespace v8; static const uint32_t FIELD_COUNT_PER_NODE = 6; -static uint32_t *transfer_buffer = nullptr; -static uint32_t transfer_buffer_length = 0; -static Nan::Persistent module_exports; -static TSTreeCursor scratch_cursor = {nullptr, nullptr, {0, 0}}; +namespace { +uint32_t *transfer_buffer = nullptr; +uint32_t transfer_buffer_length = 0; +Nan::Persistent module_exports; +TSTreeCursor scratch_cursor = {nullptr, nullptr, {0, 0}}; -static inline void setup_transfer_buffer(uint32_t node_count) { +inline void setup_transfer_buffer(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); - } + delete transfer_buffer; transfer_buffer_length = new_length; - transfer_buffer = static_cast(malloc(transfer_buffer_length * sizeof(uint32_t))); + transfer_buffer = new uint32_t[transfer_buffer_length]; #if defined(_MSC_VER) && NODE_RUNTIME_ELECTRON && NODE_MODULE_VERSION >= 89 auto nodeBuffer = node::Buffer::New(Isolate::GetCurrent(), (char *)transfer_buffer, transfer_buffer_length * sizeof(uint32_t), [](char *data, void *hint) {}, nullptr) @@ -50,22 +49,39 @@ static inline void setup_transfer_buffer(uint32_t node_count) { } } -static inline bool operator<=(const TSPoint &left, const TSPoint &right) { - if (left.row < right.row) return true; - if (left.row > right.row) return false; +inline bool operator<=(const TSPoint &left, const TSPoint &right) { + if (left.row < right.row) { + return true; + } + if (left.row > right.row) { + return false; + } return left.column <= right.column; } -static void MarshalNodes(const Nan::FunctionCallbackInfo &info, +#define read_number_from_js(out, value, name) \ + maybe_number = Nan::To(value); \ + if (maybe_number.IsNothing()) { \ + Nan::ThrowTypeError(name " must be an integer"); \ + return; \ + } \ + *(out) = maybe_number.FromJust(); + +#define read_byte_count_from_js(out, value, name) \ + read_number_from_js(out, value, name); \ + *(out) *= 2 + +void MarshalNodes(const Nan::FunctionCallbackInfo &info, const Tree *tree, const TSNode *nodes, uint32_t node_count) { info.GetReturnValue().Set(GetMarshalNodes(info, tree, nodes, node_count)); } +} // namespace void MarshalNode(const Nan::FunctionCallbackInfo &info, const Tree *tree, TSNode node) { info.GetReturnValue().Set(GetMarshalNode(info, tree, node)); } -Local GetMarshalNodes(const Nan::FunctionCallbackInfo &info, +Local GetMarshalNodes(const Nan::FunctionCallbackInfo & /*info*/, const Tree *tree, const TSNode *nodes, uint32_t node_count) { auto result = Nan::New(); setup_transfer_buffer(node_count); @@ -80,7 +96,7 @@ Local GetMarshalNodes(const Nan::FunctionCallbackInfo &info, *(p++) = node.context[1]; *(p++) = node.context[2]; *(p++) = node.context[3]; - if (node.id) { + if (node.id != nullptr) { Nan::Set(result, i, Nan::New(ts_node_symbol(node))); } else { Nan::Set(result, i, Nan::Null()); @@ -92,7 +108,7 @@ Local GetMarshalNodes(const Nan::FunctionCallbackInfo &info, return result; } -Local GetMarshalNode(const Nan::FunctionCallbackInfo &info, const Tree *tree, TSNode node) { +Local GetMarshalNode(const Nan::FunctionCallbackInfo &/*info*/, const Tree *tree, TSNode node) { const auto &cache_entry = tree->cached_nodes_.find(node.id); if (cache_entry == tree->cached_nodes_.end()) { setup_transfer_buffer(1); @@ -103,7 +119,7 @@ Local GetMarshalNode(const Nan::FunctionCallbackInfo &info, const *(p++) = node.context[1]; *(p++) = node.context[2]; *(p++) = node.context[3]; - if (node.id) { + if (node.id != nullptr) { return Nan::New(ts_node_symbol(node)); } } else { @@ -119,7 +135,7 @@ void MarshalNullNode() { TSNode UnmarshalNode(const Tree *tree) { TSNode result = {{0, 0, 0, 0}, nullptr, nullptr}; result.tree = tree->tree_; - if (!result.tree) { + if (result.tree == nullptr) { Nan::ThrowTypeError("Argument must be a tree"); return result; } @@ -132,47 +148,100 @@ TSNode UnmarshalNode(const Tree *tree) { return result; } -static void ToString(const Nan::FunctionCallbackInfo &info) { +namespace { +void ToString(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { - const char *string = ts_node_string(node); + + if (node.id != nullptr) { + char *string = ts_node_string(node); info.GetReturnValue().Set(Nan::New(string).ToLocalChecked()); - free((char *)string); + free(string); } } -static void IsMissing(const Nan::FunctionCallbackInfo &info) { +void IsMissing(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { bool result = ts_node_is_missing(node); info.GetReturnValue().Set(Nan::New(result)); } } -static void HasChanges(const Nan::FunctionCallbackInfo &info) { +void IsExtra(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + bool result = ts_node_is_extra(node); + info.GetReturnValue().Set(Nan::New(result)); + } +} + +void HasChanges(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { bool result = ts_node_has_changes(node); info.GetReturnValue().Set(Nan::New(result)); } } -static void HasError(const Nan::FunctionCallbackInfo &info) { +void HasError(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { bool result = ts_node_has_error(node); info.GetReturnValue().Set(Nan::New(result)); } } -static void FirstNamedChildForIndex(const Nan::FunctionCallbackInfo &info) { +void IsError(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { + bool result = ts_node_is_error(node); + info.GetReturnValue().Set(Nan::New(result)); + } +} + +void ParseState(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + info.GetReturnValue().Set(Nan::New(ts_node_parse_state(node))); + } +} + +void NextParseState(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + info.GetReturnValue().Set(Nan::New(ts_node_next_parse_state(node))); + } +} + +void DescendantCount(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + info.GetReturnValue().Set(Nan::New(ts_node_descendant_count(node))); + } +} + +void FirstNamedChildForIndex(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { Nan::Maybe byte = ByteCountFromJS(info[1]); if (byte.IsJust()) { MarshalNode(info, tree, ts_node_first_named_child_for_byte(node, byte.FromJust())); @@ -182,11 +251,11 @@ static void FirstNamedChildForIndex(const Nan::FunctionCallbackInfo &info MarshalNullNode(); } -static void FirstChildForIndex(const Nan::FunctionCallbackInfo &info) { +void FirstChildForIndex(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id && info.Length() > 1) { + if (node.id != nullptr && info.Length() > 1) { Nan::Maybe byte = ByteCountFromJS(info[1]); if (byte.IsJust()) { MarshalNode(info, tree, ts_node_first_child_for_byte(node, byte.FromJust())); @@ -196,11 +265,11 @@ static void FirstChildForIndex(const Nan::FunctionCallbackInfo &info) { MarshalNullNode(); } -static void NamedDescendantForIndex(const Nan::FunctionCallbackInfo &info) { +void NamedDescendantForIndex(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { Nan::Maybe maybe_min = ByteCountFromJS(info[1]); Nan::Maybe maybe_max = ByteCountFromJS(info[2]); if (maybe_min.IsJust() && maybe_max.IsJust()) { @@ -213,11 +282,11 @@ static void NamedDescendantForIndex(const Nan::FunctionCallbackInfo &info MarshalNullNode(); } -static void DescendantForIndex(const Nan::FunctionCallbackInfo &info) { +void DescendantForIndex(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { Nan::Maybe maybe_min = ByteCountFromJS(info[1]); Nan::Maybe maybe_max = ByteCountFromJS(info[2]); if (maybe_min.IsJust() && maybe_max.IsJust()) { @@ -230,11 +299,11 @@ static void DescendantForIndex(const Nan::FunctionCallbackInfo &info) { MarshalNullNode(); } -static void NamedDescendantForPosition(const Nan::FunctionCallbackInfo &info) { +void NamedDescendantForPosition(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { Nan::Maybe maybe_min = PointFromJS(info[1]); Nan::Maybe maybe_max = PointFromJS(info[2]); if (maybe_min.IsJust() && maybe_max.IsJust()) { @@ -247,11 +316,11 @@ static void NamedDescendantForPosition(const Nan::FunctionCallbackInfo &i MarshalNullNode(); } -static void DescendantForPosition(const Nan::FunctionCallbackInfo &info) { +void DescendantForPosition(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { Nan::Maybe maybe_min = PointFromJS(info[1]); Nan::Maybe maybe_max = PointFromJS(info[2]); if (maybe_min.IsJust() && maybe_max.IsJust()) { @@ -264,79 +333,107 @@ static void DescendantForPosition(const Nan::FunctionCallbackInfo &info) MarshalNullNode(); } -static void Type(const Nan::FunctionCallbackInfo &info) { +void Id(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { - const char *result = ts_node_type(node); - info.GetReturnValue().Set(Nan::New(result).ToLocalChecked()); + if (node.id != nullptr) { + info.GetReturnValue().Set(Nan::New(reinterpret_cast(node.id))); } } -static void TypeId(const Nan::FunctionCallbackInfo &info) { +void TypeId(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { TSSymbol result = ts_node_symbol(node); info.GetReturnValue().Set(Nan::New(result)); } } -static void IsNamed(const Nan::FunctionCallbackInfo &info) { +void GrammarId(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { + info.GetReturnValue().Set(Nan::New(ts_node_grammar_symbol(node))); + } +} + +void Type(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + const char *result = ts_node_type(node); + info.GetReturnValue().Set(Nan::New(result).ToLocalChecked()); + } +} + +void GrammarName(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + const char *result = ts_node_grammar_type(node); + info.GetReturnValue().Set(Nan::New(result).ToLocalChecked()); + } +} + +void IsNamed(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { bool result = ts_node_is_named(node); info.GetReturnValue().Set(Nan::New(result)); } } -static void StartIndex(const Nan::FunctionCallbackInfo &info) { +void StartIndex(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { - int32_t result = ts_node_start_byte(node) / 2; + if (node.id != nullptr) { + auto result = static_cast(ts_node_start_byte(node) / 2); info.GetReturnValue().Set(Nan::New(result)); } } -static void EndIndex(const Nan::FunctionCallbackInfo &info) { +void EndIndex(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { - int32_t result = ts_node_end_byte(node) / 2; + if (node.id != nullptr) { + auto result = static_cast(ts_node_end_byte(node) / 2); info.GetReturnValue().Set(Nan::New(result)); } } -static void StartPosition(const Nan::FunctionCallbackInfo &info) { +void StartPosition(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { TransferPoint(ts_node_start_point(node)); } } -static void EndPosition(const Nan::FunctionCallbackInfo &info) { +void EndPosition(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { TransferPoint(ts_node_end_point(node)); } } -static void Child(const Nan::FunctionCallbackInfo &info) { +void Child(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { if (!info[1]->IsUint32()) { Nan::ThrowTypeError("Second argument must be an integer"); return; @@ -348,11 +445,11 @@ static void Child(const Nan::FunctionCallbackInfo &info) { MarshalNullNode(); } -static void NamedChild(const Nan::FunctionCallbackInfo &info) { +void NamedChild(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { if (!info[1]->IsUint32()) { Nan::ThrowTypeError("Second argument must be an integer"); return; @@ -364,48 +461,99 @@ static void NamedChild(const Nan::FunctionCallbackInfo &info) { MarshalNullNode(); } -static void ChildCount(const Nan::FunctionCallbackInfo &info) { +void ChildCount(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { info.GetReturnValue().Set(Nan::New(ts_node_child_count(node))); } } -static void NamedChildCount(const Nan::FunctionCallbackInfo &info) { +void NamedChildCount(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { info.GetReturnValue().Set(Nan::New(ts_node_named_child_count(node))); } } -static void FirstChild(const Nan::FunctionCallbackInfo &info) { +void ChildForFieldName(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + if (!info[1]->IsString()) { + Nan::ThrowTypeError("fieldName must be a string"); + return; + } + Nan::Utf8String field_name(info[1]); + MarshalNode(info, tree, ts_node_child_by_field_name(node, *field_name, field_name.length())); + return; + } + MarshalNullNode(); +} + +void ChildForFieldId(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + TSFieldId field_id = 0; + Nan::Maybe maybe_number = Nan::Nothing(); + read_number_from_js(&field_id, info[1], "fieldId"); + + MarshalNode(info, tree, ts_node_child_by_field_id(node, field_id)); + return; + } + MarshalNullNode(); +} + +void FieldNameForChild(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + + if (node.id != nullptr) { + Nan::Maybe maybe_number = Nan::Nothing(); + uint32_t child_index = 0; + read_number_from_js(&child_index, info[1], "childIndex"); + const char *result = ts_node_field_name_for_child(node, child_index); + if (result != nullptr) { + info.GetReturnValue().Set(Nan::New(result).ToLocalChecked()); + } else { + info.GetReturnValue().Set(Nan::Null()); + } + } +} + +void FirstChild(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { MarshalNode(info, tree, ts_node_child(node, 0)); return; } MarshalNullNode(); } -static void FirstNamedChild(const Nan::FunctionCallbackInfo &info) { +void FirstNamedChild(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { MarshalNode(info, tree, ts_node_named_child(node, 0)); return; } MarshalNullNode(); } -static void LastChild(const Nan::FunctionCallbackInfo &info) { +void LastChild(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { uint32_t child_count = ts_node_child_count(node); if (child_count > 0) { MarshalNode(info, tree, ts_node_child(node, child_count - 1)); @@ -415,10 +563,11 @@ static void LastChild(const Nan::FunctionCallbackInfo &info) { MarshalNullNode(); } -static void LastNamedChild(const Nan::FunctionCallbackInfo &info) { +void LastNamedChild(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { uint32_t child_count = ts_node_named_child_count(node); if (child_count > 0) { MarshalNode(info, tree, ts_node_named_child(node, child_count - 1)); @@ -428,60 +577,67 @@ static void LastNamedChild(const Nan::FunctionCallbackInfo &info) { MarshalNullNode(); } -static void Parent(const Nan::FunctionCallbackInfo &info) { +void Parent(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { MarshalNode(info, tree, ts_node_parent(node)); return; } MarshalNullNode(); } -static void NextSibling(const Nan::FunctionCallbackInfo &info) { +void NextSibling(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { MarshalNode(info, tree, ts_node_next_sibling(node)); return; } MarshalNullNode(); } -static void NextNamedSibling(const Nan::FunctionCallbackInfo &info) { +void NextNamedSibling(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { MarshalNode(info, tree, ts_node_next_named_sibling(node)); return; } MarshalNullNode(); } -static void PreviousSibling(const Nan::FunctionCallbackInfo &info) { +void PreviousSibling(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { MarshalNode(info, tree, ts_node_prev_sibling(node)); return; } MarshalNullNode(); } -static void PreviousNamedSibling(const Nan::FunctionCallbackInfo &info) { +void PreviousNamedSibling(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + + if (node.id != nullptr) { MarshalNode(info, tree, ts_node_prev_named_sibling(node)); return; } MarshalNullNode(); } +} // namespace struct SymbolSet { - std::basic_string symbols; void add(TSSymbol symbol) { symbols += symbol; } - bool contains(TSSymbol symbol) { return symbols.find(symbol) != symbols.npos; } + [[nodiscard]] bool contains(TSSymbol symbol) const { return symbols.find(symbol) != std::string::npos; } +private: + std::basic_string symbols; }; bool symbol_set_from_js(SymbolSet *symbols, const Local &value, const TSLanguage *language) { @@ -493,7 +649,7 @@ bool symbol_set_from_js(SymbolSet *symbols, const Local &value, const TSL unsigned symbol_count = ts_language_symbol_count(language); Local js_types = Local::Cast(value); - for (unsigned i = 0, n = js_types->Length(); i < n; i++) { + for (unsigned i = 0, cnt = js_types->Length(); i < cnt; i++) { Local js_node_type_value; if (Nan::Get(js_types, i).ToLocal(&js_node_type_value)) { Local js_node_type; @@ -512,13 +668,13 @@ bool symbol_set_from_js(SymbolSet *symbols, const Local &value, const TSL Isolate::GetCurrent(), #endif - &node_type[0] + node_type.data() ); if (node_type == "ERROR") { symbols->add(static_cast(-1)); } else { - for (TSSymbol j = 0; j < symbol_count; j++) { + for (TSSymbol j = 0; j < static_cast(symbol_count); j++) { if (node_type == ts_language_symbol_name(language, j)) { symbols->add(j); } @@ -536,10 +692,13 @@ bool symbol_set_from_js(SymbolSet *symbols, const Local &value, const TSL return true; } -static void Children(const Nan::FunctionCallbackInfo &info) { +namespace { +void Children(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (!node.id) return; + if (node.id == nullptr) { + return; + } vector result; ts_tree_cursor_reset(&scratch_cursor, node); @@ -553,45 +712,147 @@ static void Children(const Nan::FunctionCallbackInfo &info) { MarshalNodes(info, tree, result.data(), result.size()); } -static void NamedChildren(const Nan::FunctionCallbackInfo &info) { +void NamedChildren(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (!node.id) return; + if (node.id == nullptr) { + return; + } vector result; ts_tree_cursor_reset(&scratch_cursor, node); - if (ts_tree_cursor_goto_first_child(&scratch_cursor)) { - do { - TSNode child = ts_tree_cursor_current_node(&scratch_cursor); - if (ts_node_is_named(child)) { - result.push_back(child); + ts_tree_cursor_goto_first_child(&scratch_cursor); + for (uint32_t i = 0, cnt = ts_node_named_child_count(node); i < cnt; i++) { + while (!ts_node_is_named(ts_tree_cursor_current_node(&scratch_cursor))) { + if (!ts_tree_cursor_goto_next_sibling(&scratch_cursor)) { + return; } - } while (ts_tree_cursor_goto_next_sibling(&scratch_cursor)); + } + TSNode result_node = ts_tree_cursor_current_node(&scratch_cursor); + ts_tree_cursor_goto_next_sibling(&scratch_cursor); + result.push_back(result_node); } MarshalNodes(info, tree, result.data(), result.size()); } -static void DescendantsOfType(const Nan::FunctionCallbackInfo &info) { +void ChildrenForFieldName(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (!node.id) return; + if (node.id == nullptr) { + return; + } + + if (!info[1]->IsString()) { + Nan::ThrowTypeError("First argument must be a string"); + return; + } + Nan::Utf8String field_name(info[1]); + + if (!info[2]->IsObject()) { + Nan::ThrowTypeError("Second argument must be a string"); + return; + } + TreeCursor *tree_cursor = TreeCursor::UnwrapTreeCursor(info[2]); + TSTreeCursor ts_tree_cursor = tree_cursor->cursor_; + + const TSLanguage* language = ts_tree_language(node.tree); + TSFieldId field_id = ts_language_field_id_for_name(language, *field_name, field_name.length()); + + bool done = field_id == 0; + if (!done) { + ts_tree_cursor_reset(&ts_tree_cursor, node); + ts_tree_cursor_goto_first_child(&ts_tree_cursor); + } + + vector result; + while (!done) { + while (ts_tree_cursor_current_field_id(&ts_tree_cursor) != field_id) { + if (!ts_tree_cursor_goto_next_sibling(&ts_tree_cursor)) { + done = true; + break; + } + } + if (done) { + break; + } + TSNode result_node = ts_tree_cursor_current_node(&ts_tree_cursor); + if (!ts_tree_cursor_goto_next_sibling(&ts_tree_cursor)) { + done = true; + } + result.push_back(result_node); + } + + MarshalNodes(info, tree, result.data(), result.size()); +} + +void ChildrenForFieldId(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + const TSLanguage* language = ts_tree_language(tree->tree_); + TSNode node = UnmarshalNode(tree); + if (node.id == nullptr) { + return; + } + + TSFieldId field_id = 0; + Nan::Maybe maybe_number = Nan::Nothing(); + read_number_from_js(&field_id, info[1], "fieldId"); + + bool done = field_id == 0; + if (!done) { + ts_tree_cursor_reset(&scratch_cursor, node); + ts_tree_cursor_goto_first_child(&scratch_cursor); + } + + vector result; + while (!done) { + while (ts_tree_cursor_current_field_id(&scratch_cursor) != field_id) { + if (!ts_tree_cursor_goto_next_sibling(&scratch_cursor)) { + done = true; + break; + } + } + if (done) { + break; + } + TSNode result_node = ts_tree_cursor_current_node(&scratch_cursor); + if (!ts_tree_cursor_goto_next_sibling(&scratch_cursor)) { + done = true; + } + result.push_back(result_node); + } + + MarshalNodes(info, tree, result.data(), result.size()); +} + +void DescendantsOfType(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = Tree::UnwrapTree(info[0]); + TSNode node = UnmarshalNode(tree); + if (node.id == nullptr) { + return; + } SymbolSet symbols; - if (!symbol_set_from_js(&symbols, info[1], ts_tree_language(node.tree))) return; + if (!symbol_set_from_js(&symbols, info[1], ts_tree_language(node.tree))) { + return; + } TSPoint start_point = {0, 0}; TSPoint end_point = {UINT32_MAX, UINT32_MAX}; if (info.Length() > 2 && info[2]->IsObject()) { auto maybe_start_point = PointFromJS(info[2]); - if (maybe_start_point.IsNothing()) return; + if (maybe_start_point.IsNothing()) { + return; + } start_point = maybe_start_point.FromJust(); } if (info.Length() > 3 && info[3]->IsObject()) { auto maybe_end_point = PointFromJS(info[3]); - if (maybe_end_point.IsNothing()) return; + if (maybe_end_point.IsNothing()) { + return; + } end_point = maybe_end_point.FromJust(); } @@ -606,31 +867,37 @@ static void DescendantsOfType(const Nan::FunctionCallbackInfo &info) { if (ts_tree_cursor_goto_next_sibling(&scratch_cursor)) { already_visited_children = false; } else { - if (!ts_tree_cursor_goto_parent(&scratch_cursor)) break; + if (!ts_tree_cursor_goto_parent(&scratch_cursor)) { + break; + } already_visited_children = true; } continue; } - if (end_point <= ts_node_start_point(descendant)) break; + if (end_point <= ts_node_start_point(descendant)) { + break; + } if (symbols.contains(ts_node_symbol(descendant))) { found.push_back(descendant); } - if (ts_tree_cursor_goto_first_child(&scratch_cursor)) { - already_visited_children = false; - } else if (ts_tree_cursor_goto_next_sibling(&scratch_cursor)) { + if (ts_tree_cursor_goto_first_child(&scratch_cursor) || ts_tree_cursor_goto_next_sibling(&scratch_cursor)) { already_visited_children = false; } else { - if (!ts_tree_cursor_goto_parent(&scratch_cursor)) break; + if (!ts_tree_cursor_goto_parent(&scratch_cursor)) { + break; + } already_visited_children = true; } } else { if (ts_tree_cursor_goto_next_sibling(&scratch_cursor)) { already_visited_children = false; } else { - if (!ts_tree_cursor_goto_parent(&scratch_cursor)) break; + if (!ts_tree_cursor_goto_parent(&scratch_cursor)) { + break; + } } } } @@ -638,10 +905,12 @@ static void DescendantsOfType(const Nan::FunctionCallbackInfo &info) { MarshalNodes(info, tree, found.data(), found.size()); } -static void ChildNodesForFieldId(const Nan::FunctionCallbackInfo &info) { +void ChildNodesForFieldId(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (!node.id) return; + if (node.id == nullptr) { + return; + } auto maybe_field_id = Nan::To(info[1]); if (!maybe_field_id.IsJust()) { @@ -664,11 +933,11 @@ static void ChildNodesForFieldId(const Nan::FunctionCallbackInfo &info) { MarshalNodes(info, tree, result.data(), result.size()); } -static void ChildNodeForFieldId(const Nan::FunctionCallbackInfo &info) { +void ChildNodeForFieldId(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (node.id) { + if (node.id != nullptr) { auto maybe_field_id = Nan::To(info[1]); if (!maybe_field_id.IsJust()) { Nan::ThrowTypeError("Second argument must be an integer"); @@ -681,17 +950,23 @@ static void ChildNodeForFieldId(const Nan::FunctionCallbackInfo &info) { MarshalNullNode(); } -static void Closest(const Nan::FunctionCallbackInfo &info) { +void Closest(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); - if (!node.id) return; + if (node.id == nullptr) { + return; + } SymbolSet symbols; - if (!symbol_set_from_js(&symbols, info[1], ts_tree_language(node.tree))) return; + if (!symbol_set_from_js(&symbols, info[1], ts_tree_language(node.tree))) { + return; + } for (;;) { TSNode parent = ts_node_parent(node); - if (!parent.id) break; + if (parent.id == nullptr) { + break; + } if (symbols.contains(ts_node_symbol(parent))) { MarshalNode(info, tree, parent); return; @@ -702,61 +977,75 @@ static void Closest(const Nan::FunctionCallbackInfo &info) { MarshalNullNode(); } -static void Walk(const Nan::FunctionCallbackInfo &info) { +void Walk(const Nan::FunctionCallbackInfo &info) { const Tree *tree = Tree::UnwrapTree(info[0]); TSNode node = UnmarshalNode(tree); TSTreeCursor cursor = ts_tree_cursor_new(node); info.GetReturnValue().Set(TreeCursor::NewInstance(cursor)); } +} // namespace void Init(Local exports) { Local result = Nan::New(); FunctionPair methods[] = { - {"startIndex", StartIndex}, - {"endIndex", EndIndex}, - {"type", Type}, + {"id", Id}, {"typeId", TypeId}, + {"grammarId", GrammarId}, + {"type", Type}, + {"grammarName", GrammarName}, {"isNamed", IsNamed}, - {"parent", Parent}, + {"isExtra", IsExtra}, + {"hasChanges", HasChanges}, + {"hasError", HasError}, + {"isError", IsError}, + {"parseState", ParseState}, + {"nextParseState", NextParseState}, + {"isMissing", IsMissing}, + {"startIndex", StartIndex}, + {"endIndex", EndIndex}, + {"startPosition", StartPosition}, + {"endPosition", EndPosition}, {"child", Child}, + {"childCount", ChildCount}, {"namedChild", NamedChild}, + {"namedChildCount", NamedChildCount}, + {"childForFieldName", ChildForFieldName}, + {"childForFieldId", ChildForFieldId}, + {"fieldNameForChild", FieldNameForChild}, {"children", Children}, {"namedChildren", NamedChildren}, - {"childCount", ChildCount}, - {"namedChildCount", NamedChildCount}, - {"firstChild", FirstChild}, - {"lastChild", LastChild}, - {"firstNamedChild", FirstNamedChild}, - {"lastNamedChild", LastNamedChild}, + {"childrenForFieldName", ChildrenForFieldName}, + {"childrenForFieldId", ChildrenForFieldId}, + {"parent", Parent}, {"nextSibling", NextSibling}, - {"nextNamedSibling", NextNamedSibling}, {"previousSibling", PreviousSibling}, + {"nextNamedSibling", NextNamedSibling}, {"previousNamedSibling", PreviousNamedSibling}, - {"startPosition", StartPosition}, - {"endPosition", EndPosition}, - {"isMissing", IsMissing}, - {"toString", ToString}, - {"firstChildForIndex", FirstChildForIndex}, - {"firstNamedChildForIndex", FirstNamedChildForIndex}, + {"descendantCount", DescendantCount}, {"descendantForIndex", DescendantForIndex}, {"namedDescendantForIndex", NamedDescendantForIndex}, {"descendantForPosition", DescendantForPosition}, {"namedDescendantForPosition", NamedDescendantForPosition}, - {"hasChanges", HasChanges}, - {"hasError", HasError}, - {"descendantsOfType", DescendantsOfType}, + {"toString", ToString}, {"walk", Walk}, + {"firstChild", FirstChild}, + {"lastChild", LastChild}, + {"firstNamedChild", FirstNamedChild}, + {"lastNamedChild", LastNamedChild}, + {"firstChildForIndex", FirstChildForIndex}, + {"firstNamedChildForIndex", FirstNamedChildForIndex}, + {"descendantsOfType", DescendantsOfType}, {"closest", Closest}, {"childNodeForFieldId", ChildNodeForFieldId}, {"childNodesForFieldId", ChildNodesForFieldId}, }; - for (size_t i = 0; i < length_of_array(methods); i++) { + for (auto & method : methods) { Nan::Set( result, - Nan::New(methods[i].name).ToLocalChecked(), - Nan::GetFunction(Nan::New(methods[i].callback)).ToLocalChecked() + Nan::New(method.name).ToLocalChecked(), + Nan::GetFunction(Nan::New(method.callback)).ToLocalChecked() ); } @@ -766,5 +1055,4 @@ void Init(Local exports) { Nan::Set(exports, Nan::New("NodeMethods").ToLocalChecked(), result); } -} // namespace node_methods -} // namespace node_tree_sitter +} // namespace node_tree_sitter::node_methods diff --git a/src/node.h b/src/node.h index 8c050e20..8b18b2ee 100644 --- a/src/node.h +++ b/src/node.h @@ -1,35 +1,32 @@ #ifndef NODE_TREE_SITTER_NODE_H_ #define NODE_TREE_SITTER_NODE_H_ -#include -#include -#include -#include +#include "tree_sitter/api.h" #include "./tree.h" -using namespace v8; +#include +#include +#include -namespace node_tree_sitter { -namespace node_methods { +namespace node_tree_sitter::node_methods { void Init(v8::Local); void MarshalNode(const Nan::FunctionCallbackInfo &info, const Tree *, TSNode); -Local GetMarshalNode(const Nan::FunctionCallbackInfo &info, const Tree *tree, TSNode node); -Local GetMarshalNodes(const Nan::FunctionCallbackInfo &info, const Tree *tree, const TSNode *nodes, uint32_t node_count); +v8::Local GetMarshalNode(const Nan::FunctionCallbackInfo &info, const Tree *tree, TSNode node); +v8::Local GetMarshalNodes(const Nan::FunctionCallbackInfo &info, const Tree *tree, const TSNode *nodes, uint32_t node_count); TSNode UnmarshalNode(const Tree *tree); static inline const void *UnmarshalNodeId(const uint32_t *buffer) { - const void *result; + const void *result = nullptr; memcpy(&result, buffer, sizeof(result)); return result; } -static inline void MarshalNodeId(const void *id, uint32_t *buffer) { +static inline void MarshalNodeId(const void *node_id, uint32_t *buffer) { memset(buffer, 0, sizeof(uint64_t)); - memcpy(buffer, &id, sizeof(id)); + memcpy(buffer, &node_id, sizeof(node_id)); } -} // namespace node_methods -} // namespace node_tree_sitter +} // namespace node_tree_sitter::node_methods #endif // NODE_TREE_SITTER_NODE_H_ diff --git a/src/parser.cc b/src/parser.cc index 4eab1b5e..5dbc740f 100644 --- a/src/parser.cc +++ b/src/parser.cc @@ -1,38 +1,39 @@ #include "./parser.h" -#include -#include -#include -#include -#include #include "./conversions.h" #include "./language.h" #include "./logger.h" #include "./tree.h" #include "./util.h" + +#include #include +#include +#include +#include +#include +#include namespace node_tree_sitter { using namespace v8; using std::vector; -using std::pair; Nan::Persistent Parser::constructor; -class CallbackInput { +class CallbackInput final { public: CallbackInput(v8::Local callback, v8::Local js_buffer_size) - : callback(callback), - byte_offset(0), - partial_string_offset(0) { + : callback(callback) { uint32_t buffer_size = Nan::To(js_buffer_size).FromMaybe(0); - if (buffer_size == 0) buffer_size = 32 * 1024; + if (buffer_size == 0) { + buffer_size = 32 * 1024; + } buffer.resize(buffer_size); } TSInput Input() { TSInput result; - result.payload = (void *)this; + result.payload = static_cast(this); result.encoding = TSInputEncodingUTF16; result.read = Read; return result; @@ -40,7 +41,7 @@ class CallbackInput { private: static const char * Read(void *payload, uint32_t byte, TSPoint position, uint32_t *bytes_read) { - CallbackInput *reader = (CallbackInput *)payload; + auto *reader = static_cast(payload); if (byte != reader->byte_offset) { reader->byte_offset = byte; @@ -51,7 +52,7 @@ class CallbackInput { *bytes_read = 0; Local result; uint32_t start = 0; - if (reader->partial_string_offset) { + if (reader->partial_string_offset != 0) { result = Nan::New(reader->partial_string); start = reader->partial_string_offset; } else { @@ -60,12 +61,20 @@ class CallbackInput { Local argv[2] = { Nan::New(utf16_unit), PointToJS(position) }; TryCatch try_catch(Isolate::GetCurrent()); auto maybe_result_value = Nan::Call(callback, GetGlobal(callback), 2, argv); - if (try_catch.HasCaught()) return nullptr; + if (try_catch.HasCaught()) { + return nullptr; + } Local result_value; - if (!maybe_result_value.ToLocal(&result_value)) return nullptr; - if (!result_value->IsString()) return nullptr; - if (!Nan::To(result_value).ToLocal(&result)) return nullptr; + if (!maybe_result_value.ToLocal(&result_value)) { + return nullptr; + } + if (!result_value->IsString()) { + return nullptr; + } + if (!Nan::To(result_value).ToLocal(&result)) { + return nullptr; + } } int utf16_units_read = result->Write( @@ -76,11 +85,11 @@ class CallbackInput { #endif reader->buffer.data(), - start, - reader->buffer.size(), + static_cast(start), + static_cast(reader->buffer.size()), String::NO_NULL_TERMINATION ); - int end = start + utf16_units_read; + int end = static_cast(start) + utf16_units_read; *bytes_read = 2 * utf16_units_read; reader->byte_offset += *bytes_read; @@ -93,14 +102,14 @@ class CallbackInput { reader->partial_string.Reset(); } - return (const char *)reader->buffer.data(); + return reinterpret_cast(reader->buffer.data()); } Nan::Persistent callback; std::vector buffer; - size_t byte_offset; + size_t byte_offset{}; Nan::Persistent partial_string; - size_t partial_string_offset; + size_t partial_string_offset{}; }; void Parser::Init(Local exports) { @@ -110,15 +119,19 @@ void Parser::Init(Local exports) { tpl->SetClassName(class_name); FunctionPair methods[] = { + {"setLanguage", SetLanguage}, + {"parse", Parse}, + {"getIncludedRanges", IncludedRanges}, + {"getTimeoutMicros", TimeoutMicros}, + {"setTimeoutMicros", SetTimeoutMicros}, {"getLogger", GetLogger}, {"setLogger", SetLogger}, - {"setLanguage", SetLanguage}, {"printDotGraphs", PrintDotGraphs}, - {"parse", Parse}, + {"reset", Reset}, }; - for (size_t i = 0; i < length_of_array(methods); i++) { - Nan::SetPrototypeMethod(tpl, methods[i].name, methods[i].callback); + for (auto & method : methods) { + Nan::SetPrototypeMethod(tpl, method.name, method.callback); } constructor.Reset(Nan::Persistent(Nan::GetFunction(tpl).ToLocalChecked())); @@ -128,18 +141,27 @@ void Parser::Init(Local exports) { Parser::Parser() : parser_(ts_parser_new()) {} -Parser::~Parser() { ts_parser_delete(parser_); } +Parser::~Parser() { + ts_parser_print_dot_graphs(parser_, -1); + ts_parser_set_logger(parser_, { nullptr, nullptr }); + ts_parser_delete(parser_); +} -static bool handle_included_ranges(TSParser *parser, Local arg) { +namespace { +bool handle_included_ranges(TSParser *parser, Local arg) { uint32_t last_included_range_end = 0; if (arg->IsArray()) { auto js_included_ranges = Local::Cast(arg); vector included_ranges; for (unsigned i = 0; i < js_included_ranges->Length(); i++) { Local range_value; - if (!Nan::Get(js_included_ranges, i).ToLocal(&range_value)) return false; + if (!Nan::Get(js_included_ranges, i).ToLocal(&range_value)) { + return false; + } auto maybe_range = RangeFromJS(range_value); - if (!maybe_range.IsJust()) return false; + if (!maybe_range.IsJust()) { + return false; + } auto range = maybe_range.FromJust(); if (range.start_byte < last_included_range_end) { Nan::ThrowRangeError("Overlapping ranges"); @@ -155,10 +177,11 @@ static bool handle_included_ranges(TSParser *parser, Local arg) { return true; } +} // namespace void Parser::New(const Nan::FunctionCallbackInfo &info) { if (info.IsConstructCall()) { - Parser *parser = new Parser(); + auto *parser = new Parser(); parser->Wrap(info.This()); info.GetReturnValue().Set(info.This()); } else { @@ -173,17 +196,17 @@ void Parser::New(const Nan::FunctionCallbackInfo &info) { } void Parser::SetLanguage(const Nan::FunctionCallbackInfo &info) { - Parser *parser = ObjectWrap::Unwrap(info.This()); + auto *parser = ObjectWrap::Unwrap(info.This()); const TSLanguage *language = language_methods::UnwrapLanguage(info[0]); - if (language) { + if (language != nullptr) { ts_parser_set_language(parser->parser_, language); info.GetReturnValue().Set(info.This()); } } void Parser::Parse(const Nan::FunctionCallbackInfo &info) { - Parser *parser = ObjectWrap::Unwrap(info.This()); + auto *parser = ObjectWrap::Unwrap(info.This()); if (!info[0]->IsFunction()) { Nan::ThrowTypeError("Input must be a function"); @@ -196,7 +219,7 @@ void Parser::Parse(const Nan::FunctionCallbackInfo &info) { const TSTree *old_tree = nullptr; if (info.Length() > 1 && !info[1]->IsNull() && !info[1]->IsUndefined() && Nan::To(info[1]).ToLocal(&js_old_tree)) { const Tree *tree = Tree::UnwrapTree(js_old_tree); - if (!tree) { + if (tree == nullptr) { Nan::ThrowTypeError("Second argument must be a tree"); return; } @@ -204,9 +227,13 @@ void Parser::Parse(const Nan::FunctionCallbackInfo &info) { } Local buffer_size = Nan::Null(); - if (info.Length() > 2) buffer_size = info[2]; + if (info.Length() > 2) { + buffer_size = info[2]; + } - if (!handle_included_ranges(parser->parser_, info[3])) return; + if (!handle_included_ranges(parser->parser_, info[3])) { + return; + } CallbackInput callback_input(callback, buffer_size); TSTree *tree = ts_parser_parse(parser->parser_, old_tree, callback_input.Input()); @@ -214,12 +241,44 @@ void Parser::Parse(const Nan::FunctionCallbackInfo &info) { info.GetReturnValue().Set(result); } +void Parser::IncludedRanges(const Nan::FunctionCallbackInfo &info) { + auto *parser = ObjectWrap::Unwrap(info.This()); + uint32_t count; + + const TSRange *ranges = ts_parser_included_ranges(parser->parser_, &count); + + Local result = Nan::New(count); + for (uint32_t i = 0; i < count; i++) { + Nan::Set(result, i, RangeToJS(ranges[i])); + } + + info.GetReturnValue().Set(result); +} + +void Parser::TimeoutMicros(const Nan::FunctionCallbackInfo &info) { + auto *parser = ObjectWrap::Unwrap(info.This()); + uint64_t timeout_micros = ts_parser_timeout_micros(parser->parser_); + info.GetReturnValue().Set(Nan::New(timeout_micros)); +} + +void Parser::SetTimeoutMicros(const Nan::FunctionCallbackInfo &info) { + auto *parser = ObjectWrap::Unwrap(info.This()); + uint64_t timeout_micros; + if (info[0]->IsNumber()) { + timeout_micros = Nan::To(info[0]).ToLocalChecked()->IntegerValue(Nan::GetCurrentContext()).FromJust(); + ts_parser_set_timeout_micros(parser->parser_, timeout_micros); + } else { + Nan::ThrowTypeError("First argument must be a number"); + return; + } +} + void Parser::GetLogger(const Nan::FunctionCallbackInfo &info) { - Parser *parser = ObjectWrap::Unwrap(info.This()); + auto *parser = ObjectWrap::Unwrap(info.This()); TSLogger current_logger = ts_parser_logger(parser->parser_); - if (current_logger.payload && current_logger.log == Logger::Log) { - Logger *logger = (Logger *)current_logger.payload; + if ((current_logger.payload != nullptr) && current_logger.log == Logger::Log) { + auto *logger = static_cast(current_logger.payload); info.GetReturnValue().Set(Nan::New(logger->func)); } else { info.GetReturnValue().Set(Nan::Null()); @@ -227,16 +286,20 @@ void Parser::GetLogger(const Nan::FunctionCallbackInfo &info) { } void Parser::SetLogger(const Nan::FunctionCallbackInfo &info) { - Parser *parser = ObjectWrap::Unwrap(info.This()); + auto *parser = ObjectWrap::Unwrap(info.This()); TSLogger current_logger = ts_parser_logger(parser->parser_); if (info[0]->IsFunction()) { - if (current_logger.payload) delete (Logger *)current_logger.payload; + if (current_logger.payload != nullptr) { + delete static_cast(current_logger.payload); + } ts_parser_set_logger(parser->parser_, Logger::Make(Local::Cast(info[0]))); } else if (!Nan::To(info[0]).FromMaybe(true)) { - if (current_logger.payload) delete (Logger *)current_logger.payload; - ts_parser_set_logger(parser->parser_, { 0, 0 }); + if (current_logger.payload != nullptr) { + delete static_cast(current_logger.payload); + } + ts_parser_set_logger(parser->parser_, { nullptr, nullptr }); } else { Nan::ThrowTypeError("Logger callback must either be a function or a falsy value"); return; @@ -246,10 +309,25 @@ void Parser::SetLogger(const Nan::FunctionCallbackInfo &info) { } void Parser::PrintDotGraphs(const Nan::FunctionCallbackInfo &info) { - Parser *parser = ObjectWrap::Unwrap(info.This()); + auto *parser = ObjectWrap::Unwrap(info.This()); + + if (!info[0]->IsBoolean()) { + Nan::ThrowTypeError("First argument must be a boolean"); + return; + } + + int32_t fd = 2; + + if (info.Length() > 1) { + if (!info[1]->IsNumber()) { + Nan::ThrowTypeError("Second argument must be a number (file descriptor)"); + return; + } + fd = Nan::To(info[1]).FromJust(); + } if (Nan::To(info[0]).FromMaybe(false)) { - ts_parser_print_dot_graphs(parser->parser_, 2); + ts_parser_print_dot_graphs(parser->parser_, (fd >= 0) ? fd : 2); } else { ts_parser_print_dot_graphs(parser->parser_, -1); } @@ -257,4 +335,11 @@ void Parser::PrintDotGraphs(const Nan::FunctionCallbackInfo &info) { info.GetReturnValue().Set(info.This()); } +void Parser::Reset(const Nan::FunctionCallbackInfo &info) { + auto *parser = ObjectWrap::Unwrap(info.This()); + + ts_parser_reset(parser->parser_); + + info.GetReturnValue().Set(info.This()); +} } // namespace node_tree_sitter diff --git a/src/parser.h b/src/parser.h index 8fb893a0..913db1cb 100644 --- a/src/parser.h +++ b/src/parser.h @@ -1,30 +1,34 @@ #ifndef NODE_TREE_SITTER_PARSER_H_ #define NODE_TREE_SITTER_PARSER_H_ -#include +#include "tree_sitter/api.h" + #include #include -#include +#include namespace node_tree_sitter { -class Parser : public Nan::ObjectWrap { +class Parser final : public Nan::ObjectWrap { public: - static void Init(v8::Local exports); - - TSParser *parser_; + static void Init(v8::Local exports); private: explicit Parser(); - ~Parser(); + ~Parser() final; static void New(const Nan::FunctionCallbackInfo &); static void SetLanguage(const Nan::FunctionCallbackInfo &); + static void Parse(const Nan::FunctionCallbackInfo &); + static void IncludedRanges(const Nan::FunctionCallbackInfo &); + static void TimeoutMicros(const Nan::FunctionCallbackInfo &); + static void SetTimeoutMicros(const Nan::FunctionCallbackInfo &); static void GetLogger(const Nan::FunctionCallbackInfo &); static void SetLogger(const Nan::FunctionCallbackInfo &); - static void Parse(const Nan::FunctionCallbackInfo &); static void PrintDotGraphs(const Nan::FunctionCallbackInfo &); + static void Reset(const Nan::FunctionCallbackInfo &); + TSParser *parser_; static Nan::Persistent constructor; }; diff --git a/src/query.cc b/src/query.cc index 2c722b11..97f1af0c 100644 --- a/src/query.cc +++ b/src/query.cc @@ -1,21 +1,21 @@ #include "./query.h" -#include -#include -#include -#include -#include "./node.h" +#include "./conversions.h" #include "./language.h" #include "./logger.h" +#include "./node.h" #include "./util.h" -#include "./conversions.h" + +#include +#include +#include +#include namespace node_tree_sitter { using std::vector; using namespace v8; -using node_methods::UnmarshalNodeId; -const char *query_error_names[] = { +const char *const query_error_names[] = { "TSQueryErrorNone", "TSQueryErrorSyntax", "TSQueryErrorNodeType", @@ -36,14 +36,33 @@ void Query::Init(Local exports) { Local class_name = Nan::New("Query").ToLocalChecked(); tpl->SetClassName(class_name); + GetterPair getters[] = { + {"matchLimit", MatchLimit}, + }; + FunctionPair methods[] = { {"_matches", Matches}, {"_captures", Captures}, {"_getPredicates", GetPredicates}, + {"disableCapture", DisableCapture}, + {"disablePattern", DisablePattern}, + {"isPatternGuaranteedAtStep", IsPatternGuaranteedAtStep}, + {"isPatternRooted", IsPatternRooted}, + {"isPatternNonLocal", IsPatternNonLocal}, + {"startIndexForPattern", StartIndexForPattern}, + {"didExceedMatchLimit", DidExceedMatchLimit}, }; - for (size_t i = 0; i < length_of_array(methods); i++) { - Nan::SetPrototypeMethod(tpl, methods[i].name, methods[i].callback); + for (auto & getter : getters) { + Nan::SetAccessor( + tpl->InstanceTemplate(), + Nan::New(getter.name).ToLocalChecked(), + getter.callback + ); + } + + for (auto & method : methods) { + Nan::SetPrototypeMethod(tpl, method.name, method.callback); } Local ctor = Nan::GetFunction(tpl).ToLocalChecked(); @@ -60,7 +79,7 @@ Query::~Query() { } Local Query::NewInstance(TSQuery *query) { - if (query) { + if (query != nullptr) { Local self; MaybeLocal maybe_self = Nan::NewInstance(Nan::New(constructor)); if (maybe_self.ToLocal(&self)) { @@ -72,9 +91,13 @@ Local Query::NewInstance(TSQuery *query) { } Query *Query::UnwrapQuery(const Local &value) { - if (!value->IsObject()) return nullptr; + if (!value->IsObject()) { + return nullptr; + } Local js_query = Local::Cast(value); - if (!Nan::New(constructor_template)->HasInstance(js_query)) return nullptr; + if (!Nan::New(constructor_template)->HasInstance(js_query)) { + return nullptr; + } return ObjectWrap::Unwrap(js_query); } @@ -131,7 +154,7 @@ void Query::New(const Nan::FunctionCallbackInfo &info) { auto self = info.This(); - Query *query_wrapper = new Query(query); + auto *query_wrapper = new Query(query); query_wrapper->Wrap(self); auto init = @@ -145,7 +168,7 @@ void Query::New(const Nan::FunctionCallbackInfo &info) { void Query::GetPredicates(const Nan::FunctionCallbackInfo &info) { Query *query = Query::UnwrapQuery(info.This()); - auto ts_query = query->query_; + auto *ts_query = query->query_; auto pattern_len = ts_query_pattern_count(ts_query); @@ -203,6 +226,10 @@ void Query::Matches(const Nan::FunctionCallbackInfo &info) { uint32_t start_column = Nan::To(info[2]).ToChecked() << 1; uint32_t end_row = Nan::To(info[3]).ToChecked(); uint32_t end_column = Nan::To(info[4]).ToChecked() << 1; + uint32_t start_index = Nan::To(info[5]).ToChecked(); + uint32_t end_index = Nan::To(info[6]).ToChecked() << 1; + uint32_t match_limit = Nan::To(info[7]).ToChecked(); + uint32_t max_start_depth = Nan::To(info[8]).ToChecked(); if (query == nullptr) { Nan::ThrowError("Missing argument query"); @@ -219,6 +246,9 @@ void Query::Matches(const Nan::FunctionCallbackInfo &info) { 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_set_byte_range(ts_query_cursor, start_index, end_index); + ts_query_cursor_set_match_limit(ts_query_cursor, match_limit); + ts_query_cursor_set_max_start_depth(ts_query_cursor, max_start_depth); ts_query_cursor_exec(ts_query_cursor, ts_query, rootNode); Local js_matches = Nan::New(); @@ -259,6 +289,10 @@ void Query::Captures(const Nan::FunctionCallbackInfo &info) { uint32_t start_column = Nan::To(info[2]).ToChecked() << 1; uint32_t end_row = Nan::To(info[3]).ToChecked(); uint32_t end_column = Nan::To(info[4]).ToChecked() << 1; + uint32_t start_index = Nan::To(info[5]).ToChecked(); + uint32_t end_index = Nan::To(info[6]).ToChecked() << 1; + uint32_t match_limit = Nan::To(info[7]).ToChecked(); + uint32_t max_start_depth = Nan::To(info[8]).ToChecked(); if (query == nullptr) { Nan::ThrowError("Missing argument query"); @@ -275,6 +309,9 @@ void Query::Captures(const Nan::FunctionCallbackInfo &info) { 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_set_byte_range(ts_query_cursor, start_index, end_index); + ts_query_cursor_set_match_limit(ts_query_cursor, match_limit); + ts_query_cursor_set_max_start_depth(ts_query_cursor, max_start_depth); ts_query_cursor_exec(ts_query_cursor, ts_query, rootNode); Local js_matches = Nan::New(); @@ -315,5 +352,52 @@ void Query::Captures(const Nan::FunctionCallbackInfo &info) { info.GetReturnValue().Set(result); } +void Query::DisableCapture(const Nan::FunctionCallbackInfo &info) { + Query *query = Query::UnwrapQuery(info.This()); + auto string = Nan::To(info[0]).ToLocalChecked(); + Nan::Utf8String utf8_string(string); + const char *capture_name = *utf8_string; + ts_query_disable_capture(query->query_, capture_name, utf8_string.length()); +} + +void Query::DisablePattern(const Nan::FunctionCallbackInfo &info) { + Query *query = Query::UnwrapQuery(info.This()); + uint32_t pattern_index = Nan::To(info[0]).ToChecked(); + ts_query_disable_pattern(query->query_, pattern_index); +} + +void Query::IsPatternGuaranteedAtStep(const Nan::FunctionCallbackInfo &info) { + Query *query = Query::UnwrapQuery(info.This()); + uint32_t byte_offset = Nan::To(info[0]).ToChecked(); + info.GetReturnValue().Set(Nan::New(ts_query_is_pattern_guaranteed_at_step(query->query_, byte_offset))); +} + +void Query::IsPatternRooted(const Nan::FunctionCallbackInfo &info) { + Query *query = Query::UnwrapQuery(info.This()); + uint32_t pattern_index = Nan::To(info[0]).ToChecked(); + info.GetReturnValue().Set(Nan::New(ts_query_is_pattern_rooted(query->query_, pattern_index))); +} + +void Query::IsPatternNonLocal(const Nan::FunctionCallbackInfo &info) { + Query *query = Query::UnwrapQuery(info.This()); + uint32_t pattern_index = Nan::To(info[0]).ToChecked(); + info.GetReturnValue().Set(Nan::New(ts_query_is_pattern_non_local(query->query_, pattern_index))); +} + +void Query::StartIndexForPattern(const Nan::FunctionCallbackInfo &info) { + Query *query = Query::UnwrapQuery(info.This()); + uint32_t pattern_index = Nan::To(info[0]).ToChecked(); + info.GetReturnValue().Set(Nan::New(ts_query_start_byte_for_pattern(query->query_, pattern_index))); +} + +void Query::DidExceedMatchLimit(const Nan::FunctionCallbackInfo &info) { + Query *query = Query::UnwrapQuery(info.This()); + info.GetReturnValue().Set(Nan::New(ts_query_cursor_did_exceed_match_limit(ts_query_cursor))); +} + +void Query::MatchLimit(Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + Query *query = Query::UnwrapQuery(info.This()); + info.GetReturnValue().Set(Nan::New(ts_query_cursor_match_limit(ts_query_cursor))); +} } // namespace node_tree_sitter diff --git a/src/query.h b/src/query.h index 955e621f..e4e99261 100644 --- a/src/query.h +++ b/src/query.h @@ -1,15 +1,15 @@ #ifndef NODE_TREE_SITTER_QUERY_H_ #define NODE_TREE_SITTER_QUERY_H_ -#include +#include "tree_sitter/api.h" + #include #include -#include -#include +#include namespace node_tree_sitter { -class Query : public Nan::ObjectWrap { +class Query final : public Nan::ObjectWrap { public: static void Init(v8::Local exports); static v8::Local NewInstance(TSQuery *); @@ -19,18 +19,26 @@ class Query : public Nan::ObjectWrap { private: explicit Query(TSQuery *); - ~Query(); + ~Query() final; static void New(const Nan::FunctionCallbackInfo &); static void Matches(const Nan::FunctionCallbackInfo &); static void Captures(const Nan::FunctionCallbackInfo &); static void GetPredicates(const Nan::FunctionCallbackInfo &); + static void DisableCapture(const Nan::FunctionCallbackInfo &); + static void DisablePattern(const Nan::FunctionCallbackInfo &); + static void IsPatternGuaranteedAtStep(const Nan::FunctionCallbackInfo &); + static void IsPatternRooted(const Nan::FunctionCallbackInfo &); + static void IsPatternNonLocal(const Nan::FunctionCallbackInfo &); + static void StartIndexForPattern(const Nan::FunctionCallbackInfo &); + static void DidExceedMatchLimit(const Nan::FunctionCallbackInfo &); + static void MatchLimit(v8::Local, const Nan::PropertyCallbackInfo &); static TSQueryCursor *ts_query_cursor; static Nan::Persistent constructor; static Nan::Persistent constructor_template; }; -} // namespace node_tree_sitter +} // namespace node_tree_sitter #endif // NODE_TREE_SITTER_QUERY_H_ diff --git a/src/tree.cc b/src/tree.cc index 66384a17..5c04df84 100644 --- a/src/tree.cc +++ b/src/tree.cc @@ -1,11 +1,13 @@ #include "./tree.h" -#include -#include -#include -#include "./node.h" +#include "./conversions.h" #include "./logger.h" +#include "./node.h" #include "./util.h" -#include "./conversions.h" +#include "tree_sitter/api.h" + +#include +#include +#include namespace node_tree_sitter { @@ -24,15 +26,17 @@ void Tree::Init(Local exports) { FunctionPair methods[] = { {"edit", Edit}, {"rootNode", RootNode}, + {"rootNodeWithOffset", RootNodeWithOffset}, {"printDotGraph", PrintDotGraph}, {"getChangedRanges", GetChangedRanges}, + {"getIncludedRanges", GetIncludedRanges}, {"getEditedRange", GetEditedRange}, {"_cacheNode", CacheNode}, {"_cacheNodes", CacheNodes}, }; - for (size_t i = 0; i < length_of_array(methods); i++) { - Nan::SetPrototypeMethod(tpl, methods[i].name, methods[i].callback); + for (auto & method : methods) { + Nan::SetPrototypeMethod(tpl, method.name, method.callback); } Local ctor = Nan::GetFunction(tpl).ToLocalChecked(); @@ -52,7 +56,7 @@ Tree::~Tree() { } Local Tree::NewInstance(TSTree *tree) { - if (tree) { + if (tree != nullptr) { Local self; MaybeLocal maybe_self = Nan::NewInstance(Nan::New(constructor)); if (maybe_self.ToLocal(&self)) { @@ -64,9 +68,13 @@ Local Tree::NewInstance(TSTree *tree) { } const Tree *Tree::UnwrapTree(const Local &value) { - if (!value->IsObject()) return nullptr; + if (!value->IsObject()) { + return nullptr; + } Local js_tree = Local::Cast(value); - if (!Nan::New(constructor_template)->HasInstance(js_tree)) return nullptr; + if (!Nan::New(constructor_template)->HasInstance(js_tree)) { + return nullptr; + } return ObjectWrap::Unwrap(js_tree); } @@ -82,7 +90,7 @@ void Tree::New(const Nan::FunctionCallbackInfo &info) {} #define read_byte_count_from_js(out, value, name) \ read_number_from_js(out, value, name); \ - (*out) *= 2 + *(out) *= 2 void Tree::Edit(const Nan::FunctionCallbackInfo &info) { Tree *tree = ObjectWrap::Unwrap(info.This()); @@ -127,10 +135,23 @@ void Tree::RootNode(const Nan::FunctionCallbackInfo &info) { node_methods::MarshalNode(info, tree, ts_tree_root_node(tree->tree_)); } +void Tree::RootNodeWithOffset(const Nan::FunctionCallbackInfo &info) { + Tree *tree = ObjectWrap::Unwrap(info.This()); + + uint32_t offset_bytes = 0; + TSPoint offset_extent; + Nan::Maybe maybe_number = Nan::Nothing(); + read_byte_count_from_js(&offset_bytes, info[0], "offsetBytes"); + read_number_from_js(&offset_extent.row, info[1], "offsetExtent.row"); + read_byte_count_from_js(&offset_extent.column, info[2], "offsetExtent.column"); + + node_methods::MarshalNode(info, tree, ts_tree_root_node_with_offset(tree->tree_, offset_bytes, offset_extent)); +} + void Tree::GetChangedRanges(const Nan::FunctionCallbackInfo &info) { const Tree *tree = ObjectWrap::Unwrap(info.This()); const Tree *other_tree = UnwrapTree(info[0]); - if (!other_tree) { + if (other_tree == nullptr) { Nan::ThrowTypeError("Argument must be a tree"); return; } @@ -148,10 +169,28 @@ void Tree::GetChangedRanges(const Nan::FunctionCallbackInfo &info) { info.GetReturnValue().Set(result); } +void Tree::GetIncludedRanges(const Nan::FunctionCallbackInfo &info) { + const Tree *tree = ObjectWrap::Unwrap(info.This()); + uint32_t range_count; + + TSRange *ranges = ts_tree_included_ranges(tree->tree_, &range_count); + + Local result = Nan::New(); + for (size_t i = 0; i < range_count; i++) { + Nan::Set(result, i, RangeToJS(ranges[i])); + } + + free(ranges); + + info.GetReturnValue().Set(result); +} + void Tree::GetEditedRange(const Nan::FunctionCallbackInfo &info) { Tree *tree = ObjectWrap::Unwrap(info.This()); TSNode root = ts_tree_root_node(tree->tree_); - if (!ts_node_has_changes(root)) return; + if (!ts_node_has_changes(root)) { + return; + } TSRange result = { ts_node_start_point(root), ts_node_end_point(root), @@ -162,14 +201,17 @@ void Tree::GetEditedRange(const Nan::FunctionCallbackInfo &info) { TSTreeCursor cursor = ts_tree_cursor_new(root); while (true) { - if (!ts_tree_cursor_goto_first_child(&cursor)) break; + if (!ts_tree_cursor_goto_first_child(&cursor)) { + break; + } while (true) { TSNode node = ts_tree_cursor_current_node(&cursor); if (ts_node_has_changes(node)) { result.start_byte = ts_node_start_byte(node); result.start_point = ts_node_start_point(node); break; - } else if (!ts_tree_cursor_goto_next_sibling(&cursor)) { + } + if (!ts_tree_cursor_goto_next_sibling(&cursor)) { break; } } @@ -178,7 +220,9 @@ void Tree::GetEditedRange(const Nan::FunctionCallbackInfo &info) { while (ts_tree_cursor_goto_parent(&cursor)) {} while (true) { - if (!ts_tree_cursor_goto_first_child(&cursor)) break; + if (!ts_tree_cursor_goto_first_child(&cursor)) { + break; + } while (true) { TSNode node = ts_tree_cursor_current_node(&cursor); if (ts_node_has_changes(node)) { @@ -198,32 +242,49 @@ void Tree::GetEditedRange(const Nan::FunctionCallbackInfo &info) { void Tree::PrintDotGraph(const Nan::FunctionCallbackInfo &info) { Tree *tree = ObjectWrap::Unwrap(info.This()); - ts_tree_print_dot_graph(tree->tree_, stderr); + + if (info[1]->IsNumber()) { + auto fd = Nan::To(info[1]).FromJust(); + FILE *file = fdopen(fd, "w"); + if (file != nullptr) { + ts_tree_print_dot_graph(tree->tree_, (long)file); + } else { + Nan::ThrowError("Failed to open file in write mode using file descriptor"); + } + } else { + ts_tree_print_dot_graph(tree->tree_, (long)stderr); + } + info.GetReturnValue().Set(info.This()); } -static void FinalizeNode(const v8::WeakCallbackInfo &info) { +namespace { +void FinalizeNode(const v8::WeakCallbackInfo &info) { Tree::NodeCacheEntry *cache_entry = info.GetParameter(); assert(!cache_entry->node.IsEmpty()); cache_entry->node.Reset(); - if (cache_entry->tree) { + if (cache_entry->tree != nullptr) { assert(cache_entry->tree->cached_nodes_.count(cache_entry->key)); cache_entry->tree->cached_nodes_.erase(cache_entry->key); } delete cache_entry; } -static void CacheNodeForTree(Tree *tree, Isolate *isolate, Local js_node) { +void CacheNodeForTree(Tree *tree, Isolate *isolate, Local js_node) { Local js_node_field1, js_node_field2; - if (!Nan::Get(js_node, 0).ToLocal(&js_node_field1)) return; - if (!Nan::Get(js_node, 1).ToLocal(&js_node_field2)) return; + if (!Nan::Get(js_node, 0).ToLocal(&js_node_field1)) { + return; + } + if (!Nan::Get(js_node, 1).ToLocal(&js_node_field2)) { + return; + } uint32_t key_parts[2] = { Nan::To(js_node_field1).FromMaybe(0), Nan::To(js_node_field2).FromMaybe(0) }; const void *key = UnmarshalNodeId(key_parts); - auto cache_entry = new Tree::NodeCacheEntry{tree, key, {}}; + auto *cache_entry = new Tree::NodeCacheEntry{tree, key, {}}; cache_entry->node.Reset(isolate, js_node); cache_entry->node.SetWeak(cache_entry, &FinalizeNode, Nan::WeakCallbackType::kParameter); @@ -231,6 +292,7 @@ static void CacheNodeForTree(Tree *tree, Isolate *isolate, Local js_node tree->cached_nodes_[key] = cache_entry; } +} // namespace void Tree::CacheNode(const Nan::FunctionCallbackInfo &info) { Tree *tree = ObjectWrap::Unwrap(info.This()); diff --git a/src/tree.h b/src/tree.h index 5ed4e316..335a4efe 100644 --- a/src/tree.h +++ b/src/tree.h @@ -1,15 +1,16 @@ #ifndef NODE_TREE_SITTER_TREE_H_ #define NODE_TREE_SITTER_TREE_H_ -#include +#include "tree_sitter/api.h" + #include #include #include -#include +#include namespace node_tree_sitter { -class Tree : public Nan::ObjectWrap { +class Tree final : public Nan::ObjectWrap { public: static void Init(v8::Local exports); static v8::Local NewInstance(TSTree *); @@ -26,14 +27,16 @@ class Tree : public Nan::ObjectWrap { private: explicit Tree(TSTree *); - ~Tree(); + ~Tree() final; static void New(const Nan::FunctionCallbackInfo &); static void Edit(const Nan::FunctionCallbackInfo &); static void RootNode(const Nan::FunctionCallbackInfo &); + static void RootNodeWithOffset(const Nan::FunctionCallbackInfo &); static void PrintDotGraph(const Nan::FunctionCallbackInfo &); static void GetEditedRange(const Nan::FunctionCallbackInfo &); static void GetChangedRanges(const Nan::FunctionCallbackInfo &); + static void GetIncludedRanges(const Nan::FunctionCallbackInfo &); static void CacheNode(const Nan::FunctionCallbackInfo &); static void CacheNodes(const Nan::FunctionCallbackInfo &); diff --git a/src/tree_cursor.cc b/src/tree_cursor.cc index adcadf67..2e43c3db 100644 --- a/src/tree_cursor.cc +++ b/src/tree_cursor.cc @@ -1,17 +1,20 @@ #include "./tree_cursor.h" -#include -#include -#include -#include "./util.h" #include "./conversions.h" #include "./node.h" #include "./tree.h" +#include "./util.h" +#include "tree_sitter/api.h" + +#include +#include +#include namespace node_tree_sitter { using namespace v8; Nan::Persistent TreeCursor::constructor; +Nan::Persistent TreeCursor::constructor_template; void TreeCursor::Init(v8::Local exports) { Local tpl = Nan::New(New); @@ -25,75 +28,135 @@ void TreeCursor::Init(v8::Local exports) { {"nodeType", NodeType}, {"nodeIsNamed", NodeIsNamed}, {"nodeIsMissing", NodeIsMissing}, + {"currentFieldId", CurrentFieldId}, {"currentFieldName", CurrentFieldName}, + {"currentDepth", CurrentDepth}, + {"currentDescendantIndex", CurrentDescendantIndex}, }; FunctionPair methods[] = { {"startPosition", StartPosition}, {"endPosition", EndPosition}, - {"gotoParent", GotoParent}, {"gotoFirstChild", GotoFirstChild}, - {"gotoFirstChildForIndex", GotoFirstChildForIndex}, + {"gotoLastChild", GotoLastChild}, + {"gotoParent", GotoParent}, {"gotoNextSibling", GotoNextSibling}, + {"gotoPreviousSibling", GotoPreviousSibling}, + {"gotoDescendant", GotoDescendant}, + {"gotoFirstChildForIndex", GotoFirstChildForIndex}, + {"gotoFirstChildForPosition", GotoFirstChildForPosition}, {"currentNode", CurrentNode}, {"reset", Reset}, + {"resetTo", ResetTo}, }; - for (size_t i = 0; i < length_of_array(getters); i++) { + for (auto & getter : getters) { Nan::SetAccessor( tpl->InstanceTemplate(), - Nan::New(getters[i].name).ToLocalChecked(), - getters[i].callback); + Nan::New(getter.name).ToLocalChecked(), + getter.callback + ); } - for (size_t i = 0; i < length_of_array(methods); i++) { - Nan::SetPrototypeMethod(tpl, methods[i].name, methods[i].callback); + for (auto & method : methods) { + Nan::SetPrototypeMethod(tpl, method.name, method.callback); } Local constructor_local = Nan::GetFunction(tpl).ToLocalChecked(); Nan::Set(exports, class_name, constructor_local); + constructor_template.Reset(tpl); constructor.Reset(Nan::Persistent(constructor_local)); } +TreeCursor::TreeCursor(TSTreeCursor cursor) : cursor_(cursor) {} + +TreeCursor::~TreeCursor() { ts_tree_cursor_delete(&cursor_); } + Local TreeCursor::NewInstance(TSTreeCursor cursor) { Local self; MaybeLocal maybe_self = Nan::New(constructor)->NewInstance(Nan::GetCurrentContext()); if (maybe_self.ToLocal(&self)) { (new TreeCursor(cursor))->Wrap(self); return self; - } else { - return Nan::Null(); } + return Nan::Null(); } -TreeCursor::TreeCursor(TSTreeCursor cursor) : cursor_(cursor) {} - -TreeCursor::~TreeCursor() { ts_tree_cursor_delete(&cursor_); } +TreeCursor *TreeCursor::UnwrapTreeCursor(const Local &value) { + if (!value->IsObject()) { + return nullptr; + } + Local js_cursor = Local::Cast(value); + if (!Nan::New(constructor_template)->HasInstance(js_cursor)) { { +} + return nullptr; + } + return ObjectWrap::Unwrap(js_cursor); +} void TreeCursor::New(const Nan::FunctionCallbackInfo &info) { info.GetReturnValue().Set(Nan::Null()); } +#define read_number_from_js(out, value, name) \ + maybe_number = Nan::To(value); \ + if (maybe_number.IsNothing()) { \ + Nan::ThrowTypeError(name " must be an integer"); \ + return; \ + } \ + *(out) = maybe_number.FromJust(); + +#define read_byte_count_from_js(out, value, name) \ + read_number_from_js(out, value, name); \ + *(out) *= 2 + +void TreeCursor::GotoFirstChild(const Nan::FunctionCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + bool result = ts_tree_cursor_goto_first_child(&cursor->cursor_); + info.GetReturnValue().Set(Nan::New(result)); +} + +void TreeCursor::GotoLastChild(const Nan::FunctionCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + bool result = ts_tree_cursor_goto_last_child(&cursor->cursor_); + info.GetReturnValue().Set(Nan::New(result)); +} + void TreeCursor::GotoParent(const Nan::FunctionCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); bool result = ts_tree_cursor_goto_parent(&cursor->cursor_); info.GetReturnValue().Set(Nan::New(result)); } -void TreeCursor::GotoFirstChild(const Nan::FunctionCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); - bool result = ts_tree_cursor_goto_first_child(&cursor->cursor_); +void TreeCursor::GotoNextSibling(const Nan::FunctionCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + bool result = ts_tree_cursor_goto_next_sibling(&cursor->cursor_); info.GetReturnValue().Set(Nan::New(result)); } +void TreeCursor::GotoPreviousSibling(const Nan::FunctionCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + bool result = ts_tree_cursor_goto_previous_sibling(&cursor->cursor_); + info.GetReturnValue().Set(Nan::New(result)); +} + +void TreeCursor::GotoDescendant(const Nan::FunctionCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + + uint32_t goal_descendant_index = 0; + Nan::Maybe maybe_number = Nan::Nothing(); + read_number_from_js(&goal_descendant_index, info[0], "descendantIndex"); + + ts_tree_cursor_goto_descendant(&cursor->cursor_, goal_descendant_index); +} + void TreeCursor::GotoFirstChildForIndex(const Nan::FunctionCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); - auto maybe_index = Nan::To(info[0]); - if (maybe_index.IsNothing()) { - Nan::ThrowTypeError("Argument must be an integer"); - return; - } - uint32_t goal_byte = maybe_index.FromJust() * 2; + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + + uint32_t goal_byte = 0; + Nan::Maybe maybe_number = Nan::Nothing(); + read_byte_count_from_js(&goal_byte, info[0], "goalByte"); + int64_t child_index = ts_tree_cursor_goto_first_child_for_byte(&cursor->cursor_, goal_byte); if (child_index < 0) { info.GetReturnValue().Set(Nan::Null()); @@ -102,26 +165,36 @@ void TreeCursor::GotoFirstChildForIndex(const Nan::FunctionCallbackInfo & } } -void TreeCursor::GotoNextSibling(const Nan::FunctionCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); - bool result = ts_tree_cursor_goto_next_sibling(&cursor->cursor_); - info.GetReturnValue().Set(Nan::New(result)); +void TreeCursor::GotoFirstChildForPosition(const Nan::FunctionCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + + TSPoint goal_point; + Nan::Maybe maybe_number = Nan::Nothing(); + read_number_from_js(&goal_point.row, info[0], "goalPoint.row"); + read_byte_count_from_js(&goal_point.column, info[1], "goalPoint.column"); + + int64_t child_index = ts_tree_cursor_goto_first_child_for_point(&cursor->cursor_, goal_point); + if (child_index < 0) { + info.GetReturnValue().Set(Nan::Null()); + } else { + info.GetReturnValue().Set(Nan::New(static_cast(child_index))); + } } void TreeCursor::StartPosition(const Nan::FunctionCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); TSNode node = ts_tree_cursor_current_node(&cursor->cursor_); TransferPoint(ts_node_start_point(node)); } void TreeCursor::EndPosition(const Nan::FunctionCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); TSNode node = ts_tree_cursor_current_node(&cursor->cursor_); TransferPoint(ts_node_end_point(node)); } void TreeCursor::CurrentNode(const Nan::FunctionCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); Local key = Nan::New("tree").ToLocalChecked(); const Tree *tree = Tree::UnwrapTree(Nan::Get(info.This(), key).ToLocalChecked()); TSNode node = ts_tree_cursor_current_node(&cursor->cursor_); @@ -129,51 +202,80 @@ void TreeCursor::CurrentNode(const Nan::FunctionCallbackInfo &info) { } void TreeCursor::Reset(const Nan::FunctionCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); Local key = Nan::New("tree").ToLocalChecked(); const Tree *tree = Tree::UnwrapTree(Nan::Get(info.This(), key).ToLocalChecked()); TSNode node = node_methods::UnmarshalNode(tree); ts_tree_cursor_reset(&cursor->cursor_, node); } -void TreeCursor::NodeType(v8::Local prop, const Nan::PropertyCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); +void TreeCursor::ResetTo(const Nan::FunctionCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + auto *other_cursor = Nan::ObjectWrap::Unwrap(info[0]->ToObject(Nan::GetCurrentContext()).ToLocalChecked()); + + ts_tree_cursor_reset_to(&cursor->cursor_, &other_cursor->cursor_); +} + +void TreeCursor::NodeType(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); TSNode node = ts_tree_cursor_current_node(&cursor->cursor_); info.GetReturnValue().Set(Nan::New(ts_node_type(node)).ToLocalChecked()); } -void TreeCursor::NodeIsNamed(v8::Local prop, const Nan::PropertyCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); +void TreeCursor::NodeIsNamed(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); TSNode node = ts_tree_cursor_current_node(&cursor->cursor_); info.GetReturnValue().Set(Nan::New(ts_node_is_named(node))); } -void TreeCursor::NodeIsMissing(v8::Local prop, const Nan::PropertyCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); +void TreeCursor::NodeIsMissing(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); TSNode node = ts_tree_cursor_current_node(&cursor->cursor_); info.GetReturnValue().Set(Nan::New(ts_node_is_missing(node))); } -void TreeCursor::CurrentFieldName(v8::Local prop, const Nan::PropertyCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); +void TreeCursor::CurrentFieldId(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + TSFieldId field_id = ts_tree_cursor_current_field_id(&cursor->cursor_); + + if (field_id != 0) { + info.GetReturnValue().Set(Nan::New(field_id)); + } +} + +void TreeCursor::CurrentFieldName(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); const char *field_name = ts_tree_cursor_current_field_name(&cursor->cursor_); - if (field_name) { + + if (field_name != nullptr) { info.GetReturnValue().Set(Nan::New(field_name).ToLocalChecked()); } } -void TreeCursor::StartIndex(v8::Local prop, const Nan::PropertyCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); +void TreeCursor::CurrentDepth(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + + info.GetReturnValue().Set(Nan::New(ts_tree_cursor_current_depth(&cursor->cursor_))); +} + +void TreeCursor::CurrentDescendantIndex(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); + + info.GetReturnValue().Set(Nan::New(ts_tree_cursor_current_descendant_index(&cursor->cursor_))); +} + +void TreeCursor::StartIndex(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); TSNode node = ts_tree_cursor_current_node(&cursor->cursor_); info.GetReturnValue().Set(ByteCountToJS(ts_node_start_byte(node))); } -void TreeCursor::EndIndex(v8::Local prop, const Nan::PropertyCallbackInfo &info) { - TreeCursor *cursor = Nan::ObjectWrap::Unwrap(info.This()); +void TreeCursor::EndIndex(v8::Local /*prop*/, const Nan::PropertyCallbackInfo &info) { + auto *cursor = Nan::ObjectWrap::Unwrap(info.This()); TSNode node = ts_tree_cursor_current_node(&cursor->cursor_); info.GetReturnValue().Set(ByteCountToJS(ts_node_end_byte(node))); } -} +} // namespace node_tree_sitter diff --git a/src/tree_cursor.h b/src/tree_cursor.h index e02f5a6a..f4c0fdcf 100644 --- a/src/tree_cursor.h +++ b/src/tree_cursor.h @@ -4,37 +4,47 @@ #include #include #include -#include +#include "tree_sitter/api.h" namespace node_tree_sitter { -class TreeCursor : public Nan::ObjectWrap { +class TreeCursor final : public Nan::ObjectWrap { public: static void Init(v8::Local exports); static v8::Local NewInstance(TSTreeCursor); + static TreeCursor *UnwrapTreeCursor(const v8::Local &); + + TSTreeCursor cursor_; private: explicit TreeCursor(TSTreeCursor); - ~TreeCursor(); + ~TreeCursor() final; static void New(const Nan::FunctionCallbackInfo &); - static void GotoParent(const Nan::FunctionCallbackInfo &); static void GotoFirstChild(const Nan::FunctionCallbackInfo &); - static void GotoFirstChildForIndex(const Nan::FunctionCallbackInfo &); + static void GotoLastChild(const Nan::FunctionCallbackInfo &); + static void GotoParent(const Nan::FunctionCallbackInfo &); static void GotoNextSibling(const Nan::FunctionCallbackInfo &); + static void GotoPreviousSibling(const Nan::FunctionCallbackInfo &); + static void GotoDescendant(const Nan::FunctionCallbackInfo &); + static void GotoFirstChildForIndex(const Nan::FunctionCallbackInfo &); + static void GotoFirstChildForPosition(const Nan::FunctionCallbackInfo &); static void StartPosition(const Nan::FunctionCallbackInfo &); static void EndPosition(const Nan::FunctionCallbackInfo &); static void CurrentNode(const Nan::FunctionCallbackInfo &); static void Reset(const Nan::FunctionCallbackInfo &); + static void ResetTo(const Nan::FunctionCallbackInfo &); static void NodeType(v8::Local, const Nan::PropertyCallbackInfo &); static void NodeIsNamed(v8::Local, const Nan::PropertyCallbackInfo &); static void NodeIsMissing(v8::Local, const Nan::PropertyCallbackInfo &); + static void CurrentFieldId(v8::Local, const Nan::PropertyCallbackInfo &); static void CurrentFieldName(v8::Local, const Nan::PropertyCallbackInfo &); + static void CurrentDepth(v8::Local, const Nan::PropertyCallbackInfo &); + static void CurrentDescendantIndex(v8::Local, const Nan::PropertyCallbackInfo &); static void StartIndex(v8::Local, const Nan::PropertyCallbackInfo &); static void EndIndex(v8::Local, const Nan::PropertyCallbackInfo &); - TSTreeCursor cursor_; static Nan::Persistent constructor; static Nan::Persistent constructor_template; }; diff --git a/src/util.cc b/src/util.cc index cff335db..1c314ec8 100644 --- a/src/util.cc +++ b/src/util.cc @@ -1,15 +1,9 @@ -#include -#include #include "./util.h" -namespace node_tree_sitter { +#include +#include -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 { v8::Local GetGlobal(v8::Local& callback) { #if (V8_MAJOR_VERSION > 9 || (V8_MAJOR_VERSION == 9 && V8_MINOR_VERSION > 4)) diff --git a/src/util.h b/src/util.h index 42de5d13..3d093c06 100644 --- a/src/util.h +++ b/src/util.h @@ -1,13 +1,11 @@ #ifndef NODE_TREE_SITTER_UTIL_H_ #define NODE_TREE_SITTER_UTIL_H_ -#include #include +#include namespace node_tree_sitter { -#define length_of_array(a) (sizeof(a) / sizeof(a[0])) - struct GetterPair { const char *name; Nan::GetterCallback callback; @@ -18,10 +16,8 @@ struct FunctionPair { Nan::FunctionCallback callback; }; -bool instance_of(v8::Local value, v8::Local object); - v8::Local GetGlobal(v8::Local& callback); -} // namespace node_tree_sitter +} // namespace node_tree_sitter #endif // NODE_TREE_SITTER_UTIL_H_ diff --git a/test/lookahead_iterator_test.js b/test/lookahead_iterator_test.js new file mode 100644 index 00000000..456cb153 --- /dev/null +++ b/test/lookahead_iterator_test.js @@ -0,0 +1,38 @@ +const Parser = require(".."); +const Rust = require("tree-sitter-rust"); +const { assert } = require("chai"); +const {LookaheadIterator} = Parser; + +describe("LookaheadIterator", () => { + + const parser = new Parser(); + parser.setLanguage(Rust); + + describe("test lookahead iterator", () => { + it("should iterate correctly", () => { + const tree = parser.parse("struct Stuff {}"); + + const cursor = tree.walk(); + + assert(cursor.gotoFirstChild()); // struct + assert(cursor.gotoFirstChild()); // struct keyword + + const nextState = cursor.currentNode.nextParseState; + assert.notEqual(nextState, 0); + assert(cursor.gotoNextSibling()); // type_identifier + assert.equal(nextState, cursor.currentNode.parseState); + assert.equal(cursor.currentNode.grammarName, "identifier"); + assert.notEqual(cursor.currentNode.grammarId, cursor.currentNode.typeId); + + const expectedSymbols = ["identifier", "block_comment", "line_comment"] + const lookahead = new LookaheadIterator(Rust, nextState); + assert.deepEqual(lookahead.iterNames(), expectedSymbols); + + assert(lookahead.resetState(nextState)); + assert.deepEqual(lookahead.iterNames(), expectedSymbols); + + assert(lookahead.reset(Rust, nextState)); + assert.deepEqual(lookahead.iterNames(), expectedSymbols); + }); + }); +}); diff --git a/test/node_test.js b/test/node_test.js index b81c47b8..7989909d 100644 --- a/test/node_test.js +++ b/test/node_test.js @@ -1,7 +1,41 @@ const Parser = require(".."); +const C = require('tree-sitter-c'); +const EmbeddedTemplate = require('tree-sitter-embedded-template'); const JavaScript = require('tree-sitter-javascript'); +const JSON = require('tree-sitter-json'); +const Python = require('tree-sitter-python'); const { assert } = require("chai"); +const JSON_EXAMPLE = ` + +[ + 123, + false, + { + "x": null + } +] +` + +function getAllNodes(tree) { + const result = []; + let visitedChildren = false; + let cursor = tree.walk(); + while (true) { + if (!visitedChildren) { + result.push(cursor.currentNode); + if (!cursor.gotoFirstChild()) { + visitedChildren = true; + } + } else if (cursor.gotoNextSibling()) { + visitedChildren = false; + } else if (!cursor.gotoParent()) { + break; + } + } + return result; +} + describe("Node", () => { let parser; @@ -51,6 +85,171 @@ describe("Node", () => { }) }); + describe(".child", () => { + it("returns the child at the given index", () => { + parser.setLanguage(JSON); + const tree = parser.parse(JSON_EXAMPLE); + const arrayNode = tree.rootNode.child(0); + + assert.equal(arrayNode.type, "array"); + assert.equal(arrayNode.namedChildCount, 3); + assert.equal(arrayNode.startIndex, JSON_EXAMPLE.indexOf("[")); + assert.equal(arrayNode.endIndex, JSON_EXAMPLE.indexOf("]") + 1); + assert.deepEqual(arrayNode.startPosition, { row: 2, column: 0 }); + assert.deepEqual(arrayNode.endPosition, { row: 8, column: 1 }); + assert.equal(arrayNode.childCount, 7); + + const leftBracketNode = arrayNode.child(0); + const numberNode = arrayNode.child(1); + const commaNode1 = arrayNode.child(2); + const falseNode = arrayNode.child(3); + const commaNode2 = arrayNode.child(4); + const objectNode = arrayNode.child(5); + const rightBracketNode = arrayNode.child(6); + + assert.equal(leftBracketNode.type, "["); + assert.equal(numberNode.type, "number"); + assert.equal(commaNode1.type, ","); + assert.equal(falseNode.type, "false"); + assert.equal(commaNode2.type, ","); + assert.equal(objectNode.type, "object"); + assert.equal(rightBracketNode.type, "]"); + + assert(!leftBracketNode.isNamed); + assert(numberNode.isNamed); + assert(!commaNode1.isNamed); + assert(falseNode.isNamed); + assert(!commaNode2.isNamed); + assert(objectNode.isNamed); + assert(!rightBracketNode.isNamed); + + assert.equal(numberNode.startIndex, JSON_EXAMPLE.indexOf("123")); + assert.equal(numberNode.endIndex, JSON_EXAMPLE.indexOf("123") + 3); + assert.deepEqual(numberNode.startPosition, { row: 3, column: 2 }); + assert.deepEqual(numberNode.endPosition, { row: 3, column: 5 }); + + assert.equal(falseNode.startIndex, JSON_EXAMPLE.indexOf("false")); + assert.equal(falseNode.endIndex, JSON_EXAMPLE.indexOf("false") + 5); + assert.deepEqual(falseNode.startPosition, { row: 4, column: 2 }); + assert.deepEqual(falseNode.endPosition, { row: 4, column: 7 }); + + assert.equal(objectNode.startIndex, JSON_EXAMPLE.indexOf("{")); + assert.equal(objectNode.endIndex, JSON_EXAMPLE.indexOf("}") + 1); + assert.deepEqual(objectNode.startPosition, { row: 5, column: 2 }); + assert.deepEqual(objectNode.endPosition, { row: 7, column: 3 }); + + assert.equal(objectNode.childCount, 3); + const leftBraceNode = objectNode.child(0); + const pairNode = objectNode.child(1); + const rightBraceNode = objectNode.child(2); + + assert.equal(leftBraceNode.type, "{"); + assert.equal(pairNode.type, "pair"); + assert.equal(rightBraceNode.type, "}"); + + assert(!leftBraceNode.isNamed); + assert(pairNode.isNamed); + assert(!rightBraceNode.isNamed); + + assert.equal(pairNode.startIndex, JSON_EXAMPLE.indexOf('"x"')); + assert.equal(pairNode.endIndex, JSON_EXAMPLE.indexOf("null") + 4); + assert.deepEqual(pairNode.startPosition, { row: 6, column: 4 }); + assert.deepEqual(pairNode.endPosition, { row: 6, column: 13 }); + + assert.equal(pairNode.childCount, 3); + const stringNode = pairNode.child(0); + const colonNode = pairNode.child(1); + const nullNode = pairNode.child(2); + + assert.equal(stringNode.type, "string"); + assert.equal(colonNode.type, ":"); + assert.equal(nullNode.type, "null"); + + assert(stringNode.isNamed); + assert(!colonNode.isNamed); + assert(nullNode.isNamed); + + assert.equal(stringNode.startIndex, JSON_EXAMPLE.indexOf('"x"')); + assert.equal(stringNode.endIndex, JSON_EXAMPLE.indexOf('"x"') + 3); + assert.deepEqual(stringNode.startPosition, { row: 6, column: 4 }); + assert.deepEqual(stringNode.endPosition, { row: 6, column: 7 }); + + assert.equal(nullNode.startIndex, JSON_EXAMPLE.indexOf("null")); + assert.equal(nullNode.endIndex, JSON_EXAMPLE.indexOf("null") + 4); + assert.deepEqual(nullNode.startPosition, { row: 6, column: 9 }); + assert.deepEqual(nullNode.endPosition, { row: 6, column: 13 }); + + assert.equal(stringNode.parent, pairNode); + assert.equal(nullNode.parent, pairNode); + assert.equal(pairNode.parent, objectNode); + assert.equal(numberNode.parent, arrayNode); + assert.equal(falseNode.parent, arrayNode); + assert.equal(objectNode.parent, arrayNode); + assert.equal(arrayNode.parent, tree.rootNode); + assert.equal(tree.rootNode.parent, null); + }); + }); + + describe(".namedChild", () => { + it("returns the named child at the given index", () => { + parser.setLanguage(JSON); + const tree = parser.parse(JSON_EXAMPLE); + const arrayNode = tree.rootNode.namedChild(0); + + const numberNode = arrayNode.namedChild(0); + const falseNode = arrayNode.namedChild(1); + const objectNode = arrayNode.namedChild(2); + + assert.equal(numberNode.type, "number"); + assert.equal(numberNode.startIndex, JSON_EXAMPLE.indexOf("123")); + assert.equal(numberNode.endIndex, JSON_EXAMPLE.indexOf("123") + 3); + assert.deepEqual(numberNode.startPosition, { row: 3, column: 2 }); + assert.deepEqual(numberNode.endPosition, { row: 3, column: 5 }); + + assert.equal(falseNode.type, "false"); + assert.equal(falseNode.startIndex, JSON_EXAMPLE.indexOf("false")); + assert.equal(falseNode.endIndex, JSON_EXAMPLE.indexOf("false") + 5); + assert.deepEqual(falseNode.startPosition, { row: 4, column: 2 }); + assert.deepEqual(falseNode.endPosition, { row: 4, column: 7 }); + + assert.equal(objectNode.type, "object"); + assert.equal(objectNode.startIndex, JSON_EXAMPLE.indexOf("{")); + assert.deepEqual(objectNode.startPosition, { row: 5, column: 2 }); + assert.deepEqual(objectNode.endPosition, { row: 7, column: 3 }); + + assert.equal(objectNode.namedChildCount, 1); + + const pairNode = objectNode.namedChild(0); + assert.equal(pairNode.type, "pair"); + assert.equal(pairNode.startIndex, JSON_EXAMPLE.indexOf('"x"')); + assert.equal(pairNode.endIndex, JSON_EXAMPLE.indexOf("null") + 4); + assert.deepEqual(pairNode.startPosition, { row: 6, column: 4 }); + assert.deepEqual(pairNode.endPosition, { row: 6, column: 13 }); + + const stringNode = pairNode.namedChild(0); + const nullNode = pairNode.namedChild(1); + + assert.equal(stringNode.startIndex, JSON_EXAMPLE.indexOf('"x"')); + assert.equal(stringNode.endIndex, JSON_EXAMPLE.indexOf('"x"') + 3); + assert.deepEqual(stringNode.startPosition, { row: 6, column: 4 }); + assert.deepEqual(stringNode.endPosition, { row: 6, column: 7 }); + + assert.equal(nullNode.startIndex, JSON_EXAMPLE.indexOf("null")); + assert.equal(nullNode.endIndex, JSON_EXAMPLE.indexOf("null") + 4); + assert.deepEqual(nullNode.startPosition, { row: 6, column: 9 }); + assert.deepEqual(nullNode.endPosition, { row: 6, column: 13 }); + + assert.equal(stringNode.parent, pairNode); + assert.equal(nullNode.parent, pairNode); + assert.equal(pairNode.parent, objectNode); + assert.equal(numberNode.parent, arrayNode); + assert.equal(falseNode.parent, arrayNode); + assert.equal(objectNode.parent, arrayNode); + assert.equal(arrayNode.parent, tree.rootNode); + assert.equal(tree.rootNode.parent, null); + }); + }); + describe(".children", () => { it("returns an array of child nodes", () => { const tree = parser.parse("x10 + 1000"); @@ -75,6 +274,74 @@ describe("Node", () => { }); }); + describe(".childrenForFieldName", () => { + it("returns an array of child nodes for the given field name", () => { + parser.setLanguage(Python); + const source = ` + if one: + a() + elif two: + b() + elif three: + c() + elif four: + d()` + + const tree = parser.parse(source); + const node = tree.rootNode.firstChild; + assert.equal(node.type, 'if_statement'); + const cursor = tree.walk(); + const alternatives = node.childrenForFieldName('alternative', cursor); + const alternative_texts = alternatives.map(n => { + const condition = n.childForFieldName('condition'); + return source.slice(condition.startIndex, condition.endIndex); + }); + assert.deepEqual(alternative_texts, ['two', 'three', 'four']); + }); + }); + + describe(".childForFieldName", () => { + it("checks the parent node", () => { + const tree = parser.parse("foo(a().b[0].c.d.e())"); + const callNode = tree.rootNode.firstNamedChild.firstNamedChild; + assert.equal(callNode.type, "call_expression"); + + // Regression test - when a field points to a hidden node (in this case, `_expression`) + // the hidden node should not be added to the node parent cache. + assert.equal( + callNode.childForFieldName("function").parent, + callNode + ); + }); + + it("checks that extra hidden children are skipped", () => { + parser.setLanguage(Python); + const tree = parser.parse("while a:\n pass"); + const whileNode = tree.rootNode.firstChild; + assert.equal(whileNode.type, "while_statement"); + assert.equal(whileNode.childForFieldName("body"), whileNode.child(3)); + }); + }); + + describe(".fieldNameForChild", () => { + it("returns the field name for the given child node", () => { + parser.setLanguage(C); + const tree = parser.parse("int w = x + y;"); + const translation_unit_node = tree.rootNode; + const declaration_node = translation_unit_node.firstNamedChild; + + const binary_expression_node = declaration_node + .childForFieldName("declarator") + .childForFieldName("value"); + + assert.equal(binary_expression_node.fieldNameForChild(0), "left"); + assert.equal(binary_expression_node.fieldNameForChild(1), "operator"); + assert.equal(binary_expression_node.fieldNameForChild(2), "right"); + // Negative test - Not a valid child index + assert.equal(binary_expression_node.fieldNameForChild(3), null); + }); + }); + describe(".startIndex and .endIndex", () => { it("returns the character index where the node starts/ends in the text", () => { const tree = parser.parse("a👍👎1 / b👎c👎"); @@ -389,6 +656,19 @@ describe("Node", () => { }); }); + describe(".isExtra()", () => { + it("returns true if the node is an extra node like comments", () => { + const tree = parser.parse("foo(/* hi */);"); + const node = tree.rootNode; + const comment_node = node.descendantForIndex(7, 7); + + assert.equal(node.type, "program"); + assert.equal(comment_node.type, "comment"); + assert(!node.isExtra()); + assert(comment_node.isExtra()); + }); + }); + describe(".text", () => { Object.entries({ '.parse(String)': (parser, src) => parser.parse(src), @@ -411,11 +691,172 @@ describe("Node", () => { ) }); - describe("VSCode", () => { + describe(".descendantCount", () => { + it("returns the number of descendants", () => { + parser.setLanguage(JSON); + const tree = parser.parse(JSON_EXAMPLE); + const valueNode = tree.rootNode; + const allNodes = getAllNodes(tree); + + assert.equal(valueNode.descendantCount, allNodes.length); + + const cursor = tree.walk(); + for (let i = 0; i < allNodes.length; i++) { + const node = allNodes[i]; + cursor.gotoDescendant(i) + assert.equal(cursor.currentNode, node, `index ${i}`); + } + + for (let i = allNodes.length - 1; i >= 0; i--) { + const node = allNodes[i]; + cursor.gotoDescendant(i) + assert.equal(cursor.currentNode, node, `rev index ${i}`); + } + }); + + it("tests a single node tree", () => { + parser.setLanguage(EmbeddedTemplate); + const tree = parser.parse("hello"); + + const nodes = getAllNodes(tree); + assert.equal(nodes.length, 2); + assert.equal(tree.rootNode.descendantCount, 2); + + const cursor = tree.walk(); + + cursor.gotoDescendant(0); + assert.equal(cursor.currentDepth, 0); + assert.equal(cursor.currentNode, nodes[0]); + + cursor.gotoDescendant(1); + assert.equal(cursor.currentDepth, 1); + assert.equal(cursor.currentNode, nodes[1]); + }); + }); + + describe(".descendantFor{Index, Position}", () => { + it("returns the descendant at the given index", () => { + parser.setLanguage(JSON); + const tree = parser.parse(JSON_EXAMPLE); + const arrayNode = tree.rootNode; + + // Leaf node exactly matches the given bounds - index query + const colonIndex = JSON_EXAMPLE.indexOf(":"); + let colonNode = arrayNode.descendantForIndex(colonIndex, colonIndex + 1); + assert.equal(colonNode.type, ":"); + assert.equal(colonNode.startIndex, colonIndex); + assert.equal(colonNode.endIndex, colonIndex + 1); + assert.deepEqual(colonNode.startPosition, { row: 6, column: 7 }); + assert.deepEqual(colonNode.endPosition, { row: 6, column: 8 }); + + // Leaf node exactly matches the given bounds - point query + colonNode = arrayNode.descendantForPosition({ row: 6, column: 7 }, { row: 6, column: 8 }); + assert.equal(colonNode.type, ":"); + assert.equal(colonNode.startIndex, colonIndex); + assert.equal(colonNode.endIndex, colonIndex + 1); + assert.deepEqual(colonNode.startPosition, { row: 6, column: 7 }); + assert.deepEqual(colonNode.endPosition, { row: 6, column: 8 }); + + // The given point is between two adjacent leaf nodes - index query + colonNode = arrayNode.descendantForIndex(colonIndex, colonIndex); + assert.equal(colonNode.type, ":"); + assert.equal(colonNode.startIndex, colonIndex); + assert.equal(colonNode.endIndex, colonIndex + 1); + assert.deepEqual(colonNode.startPosition, { row: 6, column: 7 }); + assert.deepEqual(colonNode.endPosition, { row: 6, column: 8 }); + + // The given point is between two adjacent leaf nodes - point query + colonNode = arrayNode.descendantForPosition({ row: 6, column: 7 }, { row: 6, column: 7 }); + assert.equal(colonNode.type, ":"); + assert.equal(colonNode.startIndex, colonIndex); + assert.equal(colonNode.endIndex, colonIndex + 1); + assert.deepEqual(colonNode.startPosition, { row: 6, column: 7 }); + assert.deepEqual(colonNode.endPosition, { row: 6, column: 8 }); + + // Leaf node starts at the lower bound, ends after the upper bound - index query + const stringIndex = JSON_EXAMPLE.indexOf('"x"'); + let stringNode = arrayNode.descendantForIndex(stringIndex, stringIndex + 2); + assert.equal(stringNode.type, "string"); + assert.equal(stringNode.startIndex, stringIndex); + assert.equal(stringNode.endIndex, stringIndex + 3); + assert.deepEqual(stringNode.startPosition, { row: 6, column: 4 }); + assert.deepEqual(stringNode.endPosition, { row: 6, column: 7 }); + + // Leaf node starts at the lower bound, ends after the upper bound - point query + stringNode = arrayNode.descendantForPosition({ row: 6, column: 4 }, { row: 6, column: 6 }); + assert.equal(stringNode.type, "string"); + assert.equal(stringNode.startIndex, stringIndex); + assert.equal(stringNode.endIndex, stringIndex + 3); + assert.deepEqual(stringNode.startPosition, { row: 6, column: 4 }); + assert.deepEqual(stringNode.endPosition, { row: 6, column: 7 }); + + // Leaf node starts before the lower bound, ends at the upper bound - index query + const nullIndex = JSON_EXAMPLE.indexOf("null"); + let nullNode = arrayNode.descendantForIndex(nullIndex + 1, nullIndex + 4); + assert.equal(nullNode.type, "null"); + assert.equal(nullNode.startIndex, nullIndex); + assert.equal(nullNode.endIndex, nullIndex + 4); + assert.deepEqual(nullNode.startPosition, { row: 6, column: 9 }); + assert.deepEqual(nullNode.endPosition, { row: 6, column: 13 }); + + // Leaf node starts before the lower bound, ends at the upper bound - point query + nullNode = arrayNode.descendantForPosition({ row: 6, column: 11 }, { row: 6, column: 13 }); + assert.equal(nullNode.type, "null"); + assert.equal(nullNode.startIndex, nullIndex); + assert.equal(nullNode.endIndex, nullIndex + 4); + assert.deepEqual(nullNode.startPosition, { row: 6, column: 9 }); + assert.deepEqual(nullNode.endPosition, { row: 6, column: 13 }); + + // The bounds span multiple leaf nodes - return the smallest node that does span it. + let pairNode = arrayNode.descendantForIndex(stringIndex + 2, stringIndex + 4); + assert.equal(pairNode.type, "pair"); + assert.equal(pairNode.startIndex, stringIndex); + assert.equal(pairNode.endIndex, stringIndex + 9); + assert.deepEqual(pairNode.startPosition, { row: 6, column: 4 }); + assert.deepEqual(pairNode.endPosition, { row: 6, column: 13 }); + + assert.equal(colonNode.parent, pairNode); + + // No leaf spans the given range - return the smallest node that does span it. + pairNode = arrayNode.descendantForPosition({ row: 6, column: 6 }, { row: 6, column: 8 }); + assert.equal(pairNode.type, "pair"); + assert.equal(pairNode.startIndex, stringIndex); + assert.equal(pairNode.endIndex, stringIndex + 9); + assert.deepEqual(pairNode.startPosition, { row: 6, column: 4 }); + assert.deepEqual(pairNode.endPosition, { row: 6, column: 13 }); + }); + }); + + describe("Numeric symbols respect simple aliases", () => { + it("should ensure numeric symbol ids for an alias match the normal id", () => { + parser.setLanguage(Python); + + // Example 1: + // Python argument lists can contain "splat" arguments, which are not allowed within + // other expressions. This includes `parenthesized_list_splat` nodes like `(*b)`. These + // `parenthesized_list_splat` nodes are aliased as `parenthesized_expression`. Their numeric + // `symbol`, aka `kind_id` should match that of a normal `parenthesized_expression`. + const tree = parser.parse("(a((*b)))"); + const root = tree.rootNode; + assert.equal( + root.toString(), + "(module (expression_statement (parenthesized_expression (call function: (identifier) arguments: (argument_list (parenthesized_expression (list_splat (identifier))))))))" + ); + + const outerExprNode = root.firstChild.firstChild; + assert.equal(outerExprNode.type, "parenthesized_expression"); + + const innerExprNode = outerExprNode.firstNamedChild.childForFieldName("arguments").firstNamedChild; + assert.equal(innerExprNode.type, "parenthesized_expression"); + assert.equal(innerExprNode.typeId, outerExprNode.typeId); + }); + }); + + + describe("Property accesses", () => { it("shouldn't segfault when accessing properties on the prototype", () => { const tree = parser.parse('2 + 2'); const nodePrototype = Object.getPrototypeOf(tree.rootNode); - // const nodePrototype = tree.rootNode.__proto__; const properties = [ "type", @@ -440,6 +881,7 @@ describe("Node", () => { "previousSibling", "previousNamedSibling", ]; + for (const property of properties) { assert.throws(() => { nodePrototype[property]; }, TypeError) } @@ -462,6 +904,7 @@ describe("Node", () => { "descendantForPosition", "closest", ]; + for (const method of methods) { assert.throws(nodePrototype[method], TypeError) } diff --git a/test/parser_test.js b/test/parser_test.js index 3ef50671..9cf3f95f 100644 --- a/test/parser_test.js +++ b/test/parser_test.js @@ -1,5 +1,7 @@ const Parser = require(".."); +const HTML = require('tree-sitter-html'); const JavaScript = require('tree-sitter-javascript'); +const JSON = require('tree-sitter-json'); const { assert } = require("chai"); describe("Parser", () => { @@ -92,6 +94,29 @@ describe("Parser", () => { }); }); + describe(".printDotGraphs", () => { + beforeEach(() => { + parser.setLanguage(JavaScript); + }); + + it("prints a dot graph to the output file", () => { + const hasZeroIndexedRow = s => s.indexOf("position: 0,") !== -1; + + const tmp = require("tmp"); + const debugGraphFile = tmp.fileSync({ postfix: ".dot" }); + parser.printDotGraphs(true, debugGraphFile.fd); + parser.parse("const zero = 0"); + + const fs = require('fs'); + const logReader = fs.readFileSync(debugGraphFile.name, 'utf8').split('\n'); + for (let line of logReader) { + assert.strictEqual(hasZeroIndexedRow(line), false, `Graph log output includes zero-indexed row: ${line}`); + } + + debugGraphFile.removeCallback(); + }); + }); + describe(".parse", () => { beforeEach(() => { parser.setLanguage(JavaScript); @@ -165,5 +190,440 @@ describe("Parser", () => { ); }) }) + + describe('invalid chars at eof', () => { + it('shows in the parse tree', () => { + parser.setLanguage(JSON); + const tree = parser.parse('\xdf'); + assert.equal(tree.rootNode.toString(), "(document (ERROR (UNEXPECTED 223)))"); + }); + }); + + describe('unexpected null characters', () => { + it('has a null character in the source', () => { + parser.setLanguage(JavaScript); + const tree = parser.parse('var \0 something;') + assert.equal( + tree.rootNode.toString(), + "(program (variable_declaration (ERROR (UNEXPECTED '\\0')) (variable_declarator name: (identifier))))" + ); + }); + }); + + describe('parsing with a timeout', () => { + it('stops after a certain number of microseconds', () => { + parser.setLanguage(JSON); + + // dont use Date.now(), we need microseconds + let startTime = performance.now() * 1000; + parser.setTimeoutMicros(1000); + let tree = parser.parse((offset, _) => offset === 0 ? " [" : ",0"); + assert.equal(tree, null); + assert.isBelow(performance.now() * 1000 - startTime, 2000); + + startTime = performance.now() * 1000; + parser.setTimeoutMicros(5000); + tree = parser.parse((offset, _) => offset === 0 ? " [" : ",0"); + assert.equal(tree, null); + assert.isAbove(performance.now() * 1000 - startTime, 100); + assert.isBelow(performance.now() * 1000 - startTime, 10000); + + parser.setTimeoutMicros(0); + tree = parser.parse((offset, _) => offset > 5000 ? "" : offset == 5000 ? "]" : ",0"); + assert.equal(tree.rootNode.firstChild.type, "array"); + }); + }); + + describe('parsing with a timeout and reset', () => { + it('stops after a certain number of microseconds and resets the parser', () => { + parser.setLanguage(JSON); + + parser.setTimeoutMicros(5); + let tree = parser.parse( + '["ok", 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]' + ); + assert.equal(tree, null); + + // Without calling reset, the parser continues from where it left off, so + // it does not see the changes to the beginning of the source code. + parser.setTimeoutMicros(0); + tree = parser.parse( + '[null, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]' + ); + assert.equal(tree.rootNode.firstNamedChild.firstNamedChild.type, "string"); + + parser.setTimeoutMicros(5); + tree = parser.parse( + '[\"ok\", 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]' + ); + assert.equal(tree, null); + + // By calling reset, we force the parser to start over from scratch so + // that it sees the changes to the beginning of the source code. + parser.setTimeoutMicros(0); + parser.reset(); + tree = parser.parse( + '[null, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]' + ); + assert.equal(tree.rootNode.firstNamedChild.firstNamedChild.type, "null"); + }); + }); + + describe('parsing with a timeout and implicit reset', () => { + it('stops after a certain number of microseconds and resets the parser', () => { + parser.setLanguage(JavaScript); + + parser.setTimeoutMicros(5); + let tree = parser.parse( + '[\"ok\", 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]' + ); + assert.equal(tree, null); + + // Changing the parser's language implicitly resets, discarding + // the previous partial parse. + parser.setLanguage(JSON); + parser.setTimeoutMicros(0); + tree = parser.parse( + '[null, 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]' + ); + assert.equal(tree.rootNode.firstNamedChild.firstNamedChild.type, "null"); + }); + }); + + describe('parsing with a timeout and no completion', () => { + it('stops after a certain number of microseconds and returns before completion', () => { + parser.setLanguage(JavaScript); + + parser.setTimeoutMicros(5); + let tree = parser.parse( + '[\"ok\", 1, 2, 3, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32]' + ); + assert.equal(tree, null); + }); + }); + + describe('one included range', () => { + it('parses the text within a range', () => { + parser.setLanguage(HTML); + const sourceCode = "hi"; + const htmlTree = parser.parse(sourceCode); + const scriptContentNode = htmlTree.rootNode.child(1).child(1); + assert.equal(scriptContentNode.type, "raw_text"); + + parser.setLanguage(JavaScript); + const jsTree = parser.parse( + sourceCode, + null, + { + includedRanges: [{ + startIndex: scriptContentNode.startIndex, + endIndex: scriptContentNode.endIndex, + startPosition: scriptContentNode.startPosition, + endPosition: scriptContentNode.endPosition + }] + } + ); + + assert.equal( + jsTree.rootNode.toString(), + "(program (expression_statement (call_expression " + + "function: (member_expression object: (identifier) property: (property_identifier)) " + + "arguments: (arguments (string (string_fragment))))))" + ); + assert.deepEqual(jsTree.rootNode.startPosition, { row: 0, column: sourceCode.indexOf('console') }); + }); + }); + + describe('multiple included ranges', () => { + it('parses the text within multiple ranges', () => { + parser.setLanguage(JavaScript); + const sourceCode = "html `
Hello, ${name.toUpperCase()}, it's ${now()}.
`"; + const jsTree = parser.parse(sourceCode); + const templateStringNode = jsTree.rootNode.descendantForIndex(sourceCode.indexOf('
'), sourceCode.indexOf('Hello')); + assert.equal(templateStringNode.type, "template_string"); + + const openQuoteNode = templateStringNode.child(0); + const interpolationNode1 = templateStringNode.child(1); + const interpolationNode2 = templateStringNode.child(2); + const closeQuoteNode = templateStringNode.child(3); + + parser.setLanguage(HTML); + const htmlRanges = [ + { + startIndex: openQuoteNode.endIndex, + startPosition: openQuoteNode.endPosition, + endIndex: interpolationNode1.startIndex, + endPosition: interpolationNode1.startPosition + }, + { + startIndex: interpolationNode1.endIndex, + startPosition: interpolationNode1.endPosition, + endIndex: interpolationNode2.startIndex, + endPosition: interpolationNode2.startPosition + }, + { + startIndex: interpolationNode2.endIndex, + startPosition: interpolationNode2.endPosition, + endIndex: closeQuoteNode.startIndex, + endPosition: closeQuoteNode.startPosition + }, + ]; + const htmlTree = parser.parse(sourceCode, null, { includedRanges: htmlRanges }); + + assert.equal( + htmlTree.rootNode.toString(), + "(fragment (element" + + " (start_tag (tag_name))" + + " (text)" + + " (element (start_tag (tag_name)) (end_tag (tag_name)))" + + " (text)" + + " (end_tag (tag_name))))" + ); + assert.deepEqual(htmlTree.getIncludedRanges(), htmlRanges); + + const divElementNode = htmlTree.rootNode.child(0); + const helloTextNode = divElementNode.child(1); + const bElementNode = divElementNode.child(2); + const bStartTagNode = bElementNode.child(0); + const bEndTagNode = bElementNode.child(1); + + assert.equal(helloTextNode.type, "text"); + assert.equal(helloTextNode.startIndex, sourceCode.indexOf('Hello')); + assert.equal(helloTextNode.endIndex, sourceCode.indexOf(' ')); + + assert.equal(bStartTagNode.type, "start_tag"); + assert.equal(bStartTagNode.startIndex, sourceCode.indexOf('')); + assert.equal(bStartTagNode.endIndex, sourceCode.indexOf('${now()}')); + + assert.equal(bEndTagNode.type, "end_tag"); + assert.equal(bEndTagNode.startIndex, sourceCode.indexOf('')); + assert.equal(bEndTagNode.endIndex, sourceCode.indexOf('.
')); + }); + }); + + describe('an included range containing mismatched positions', () => { + it('parses the text within the range', () => { + const sourceCode = "
test
{_ignore_this_part_}"; + + parser.setLanguage(HTML); + + const endIndex = sourceCode.indexOf('{_ignore_this_part_'); + + const rangeToParse = { + startIndex: 0, + startPosition: { row: 10, column: 12 }, + endIndex, + endPosition: { row: 10, column: 12 + endIndex }, + }; + + const htmlTree = parser.parse(sourceCode, null, { includedRanges: [rangeToParse] }); + + assert.deepEqual(htmlTree.getIncludedRanges()[0], rangeToParse); + + assert.equal( + htmlTree.rootNode.toString(), + "(fragment (element (start_tag (tag_name)) (text) (end_tag (tag_name))))" + ); + }); + }); + + describe('parsing error in invalid included ranges', () => { + it('throws an exception', () => { + const ranges = [ + { startIndex: 23, endIndex: 29, startPosition: { row: 0, column: 23 }, endPosition: { row: 0, column: 29 } }, + { startIndex: 0, endIndex: 5, startPosition: { row: 0, column: 0 }, endPosition: { row: 0, column: 5 } }, + { startIndex: 50, endIndex: 60, startPosition: { row: 0, column: 50 }, endPosition: { row: 0, column: 60 } }, + ]; + parser.setLanguage(JavaScript); + assert.throws(() => parser.parse('var a = 1;', null, { includedRanges: ranges }), /Overlapping ranges/); + }); + }); + + describe('test parsing with external scanner that uses included range boundaries', () => { + it('parses the text within the range', () => { + const sourceCode = 'a <%= b() %> c <% d() %>'; + const range1StartByte = sourceCode.indexOf(' b() '); + const range1EndByte = range1StartByte + ' b() '.length; + const range2StartByte = sourceCode.indexOf(' d() '); + const range2EndByte = range2StartByte + ' d() '.length; + + parser.setLanguage(JavaScript); + const tree = parser.parse(sourceCode, null, { + includedRanges: [ + { + startIndex: range1StartByte, + endIndex: range1EndByte, + startPosition: { row: 0, column: range1StartByte }, + endPosition: { row: 0, column: range1EndByte } + }, + { + startIndex: range2StartByte, + endIndex: range2EndByte, + startPosition: { row: 0, column: range2StartByte }, + endPosition: { row: 0, column: range2EndByte } + }, + ] + }); + + const root = tree.rootNode; + const statement1 = root.firstChild; + const statement2 = root.child(1); + + assert.equal( + root.toString(), + "(program" + + " (expression_statement (call_expression function: (identifier) arguments: (arguments)))" + + " (expression_statement (call_expression function: (identifier) arguments: (arguments))))" + ); + + assert.equal(statement1.startIndex, sourceCode.indexOf("b()")); + assert.equal(statement1.endIndex, sourceCode.indexOf(" %> c")); + assert.equal(statement2.startIndex, sourceCode.indexOf("d()")); + assert.equal(statement2.endIndex, sourceCode.length - " %>".length); + }); + }); + + describe('parsing with a newly excluded range', () => { + it('parses code after an edit', () => { + let sourceCode = '
<%= something %>
'; + + parser.setLanguage(HTML); + let firstTree = parser.parse(sourceCode); + + // Insert code at the beginning of the document. + const prefix = 'a very very long line of plain text. '; + firstTree.edit({ + startIndex: 0, + oldEndIndex: 0, + newEndIndex: prefix.length, + startPosition: { row: 0, column: 0 }, + oldEndPosition: { row: 0, column: 0 }, + newEndPosition: { row: 0, column: prefix.length }, + }); + sourceCode = prefix + sourceCode; + + // Parse the HTML again, this time *excluding* the template directive + // (which has moved since the previous parse). + const directiveStart = sourceCode.indexOf('<%='); + const directiveEnd = sourceCode.indexOf(''); + const sourceCodeEnd = sourceCode.length; + const tree = parser.parse(sourceCode, firstTree, { + includedRanges: [ + { + startIndex: 0, + endIndex: directiveStart, + startPosition: { row: 0, column: 0 }, + endPosition: { row: 0, column: directiveStart }, + }, + { + startIndex: directiveEnd, + endIndex: sourceCodeEnd, + startPosition: { row: 0, column: directiveEnd }, + endPosition: { row: 0, column: sourceCodeEnd }, + }, + ] + }); + + assert.equal( + tree.rootNode.toString(), + "(fragment (text) (element" + + " (start_tag (tag_name))" + + " (element (start_tag (tag_name)) (end_tag (tag_name)))" + + " (end_tag (tag_name))))" + ); + + assert.deepEqual( + tree.getChangedRanges(firstTree), + [ + // The first range that has changed syntax is the range of the newly-inserted text. + { + startIndex: 0, + endIndex: prefix.length, + startPosition: { row: 0, column: 0 }, + endPosition: { row: 0, column: prefix.length }, + }, + // Even though no edits were applied to the outer `div` element, + // its contents have changed syntax because a range of text that + // was previously included is now excluded. + { + startIndex: directiveStart, + endIndex: directiveEnd, + startPosition: { row: 0, column: directiveStart }, + endPosition: { row: 0, column: directiveEnd }, + }, + ] + ); + }); + }); + + describe('parsing with a newly included range', () => { + function simpleRange(startIndex, endIndex) { + return { + startIndex, + endIndex, + startPosition: { row: 0, column: startIndex }, + endPosition: { row: 0, column: endIndex }, + }; + } + + it('parses code after an edit', () => { + const sourceCode = '
<%= foo() %>
<%= bar() %><%= baz() %>'; + const range1Start = sourceCode.indexOf(' foo'); + const range2Start = sourceCode.indexOf(' bar'); + const range3Start = sourceCode.indexOf(' baz'); + const range1End = range1Start + 7; + const range2End = range2Start + 7; + const range3End = range3Start + 7; + + // Parse only the first code directive as JavaScript + parser.setLanguage(JavaScript); + const tree = parser.parse(sourceCode, null, { + includedRanges: [simpleRange(range1Start, range1End)] + }); + assert.equal( + tree.rootNode.toString(), + "(program" + + " (expression_statement (call_expression function: (identifier) arguments: (arguments))))" + ); + + // Parse both the first and third code directives as JavaScript, using the old tree as a + // reference. + const tree2 = parser.parse(sourceCode, tree, { + includedRanges: [ + simpleRange(range1Start, range1End), + simpleRange(range3Start, range3End) + ] + }); + assert.equal( + tree2.rootNode.toString(), + "(program" + + " (expression_statement (call_expression function: (identifier) arguments: (arguments)))" + + " (expression_statement (call_expression function: (identifier) arguments: (arguments))))" + ); + assert.deepEqual( + tree2.getChangedRanges(tree), + [simpleRange(range1End, range3End)] + ); + + const tree3 = parser.parse(sourceCode, tree, { + includedRanges: [ + simpleRange(range1Start, range1End), + simpleRange(range2Start, range2End), + simpleRange(range3Start, range3End), + ] + }); + assert.equal( + tree3.rootNode.toString(), + "(program" + + " (expression_statement (call_expression function: (identifier) arguments: (arguments)))" + + " (expression_statement (call_expression function: (identifier) arguments: (arguments)))" + + " (expression_statement (call_expression function: (identifier) arguments: (arguments))))" + ); + assert.deepEqual( + tree3.getChangedRanges(tree2), + [simpleRange(range2Start + 1, range2End - 1)] + ); + }); + }); }); }); diff --git a/test/query_test.js b/test/query_test.js index 1c579dd7..3989178d 100644 --- a/test/query_test.js +++ b/test/query_test.js @@ -1,6 +1,12 @@ const fs = require("fs"); const Parser = require(".."); +const C = require("tree-sitter-c"); +const Java = require("tree-sitter-java"); const JavaScript = require("tree-sitter-javascript"); +const JSON = require("tree-sitter-json"); +const Python = require("tree-sitter-python"); +const Ruby = require("tree-sitter-ruby"); +const Rust = require("tree-sitter-rust"); const { assert } = require("chai"); const {Query, QueryCursor} = Parser @@ -45,8 +51,7 @@ describe("Query", () => { const query = new Query(JavaScript, "(identifier) @element"); const matches = query.matches( tree.rootNode, - { row: 1, column: 1 }, - { row: 3, column: 1 } + { startPosition: { row: 1, column: 1 }, endPosition: { row: 3, column: 1 } }, ); assert.deepEqual(formatMatches(tree, matches), [ { pattern: 0, captures: [{ name: "element", text: "d" }] }, @@ -194,6 +199,747 @@ describe("Query", () => { ]); }); }); + + describe("match limit", () => { + it("has too many permutations to track", () => { + const query = new Query(JavaScript, ` + (array (identifier) @pre (identifier) @post) + `); + + const source = "[" + "hello, ".repeat(50) + "];"; + + const tree = parser.parse(source); + const matches = query.matches(tree.rootNode, { matchLimit: 32 }); + + // For this pathological query, some match permutations will be dropped. + // Just check that a subset of the results are returned, and crash or + // leak occurs. + assert.deepEqual( + formatMatches(tree, matches)[0], + { pattern: 0, captures: [{ name: "pre", text: "hello" }, { name: "post", text: "hello" }] }, + ) + assert.equal(query.matchLimit, 32); + assert.equal(query.didExceedMatchLimit(), true); + }); + + it("has alternatives", () => { + const query = new Query(JavaScript, ` + ( + (comment) @doc + ; not immediate + (class_declaration) @class + ) + + (call_expression + function: [ + (identifier) @function + (member_expression property: (property_identifier) @method) + ] + ) + `); + + const source = '/* hi */ a.b(); '.repeat(50); + + const tree = parser.parse(source); + const matches = query.matches(tree.rootNode, { matchLimit: 32 }); + + assert.deepEqual( + formatMatches(tree, matches), + Array(50).fill({ pattern: 1, captures: [{ name: "method", text: "b" }] }), + ); + assert.equal(query.matchLimit, 32); + assert.equal(query.didExceedMatchLimit(), true); + }); + }); + + describe(".disableCapture", () => { + it("disables a capture", () => { + const query = new Query(JavaScript, ` + (function_declaration + (identifier) @name1 @name2 @name3 + (statement_block) @body1 @body2) + `); + + const source = "function foo() { return 1; }"; + const tree = parser.parse(source); + + let matches = query.matches(tree.rootNode); + assert.deepEqual(formatMatches(tree, matches), [ + { + pattern: 0, + captures: [ + { name: "name1", text: "foo" }, + { name: "name2", text: "foo" }, + { name: "name3", text: "foo" }, + { name: "body1", text: "{ return 1; }" }, + { name: "body2", text: "{ return 1; }" }, + ], + }, + ]); + + // disabling captures still works when there are multiple captures on a + // single node. + query.disableCapture("name2"); + matches = query.matches(tree.rootNode); + assert.deepEqual(formatMatches(tree, matches), [ + { + pattern: 0, + captures: [ + { name: "name1", text: "foo" }, + { name: "name3", text: "foo" }, + { name: "body1", text: "{ return 1; }" }, + { name: "body2", text: "{ return 1; }" }, + ], + }, + ]); + }); + }); + + describe(".disablePattern", () => { + it("disables a pattern", () => { + const query = new Query(JavaScript, ` + (function_declaration + name: (identifier) @name) + (function_declaration + body: (statement_block) @body) + (class_declaration + name: (identifier) @name) + (class_declaration + body: (class_body) @body) + `); + + // disable the patterns that match names + query.disablePattern(0); + query.disablePattern(2); + + const source = "class A { constructor() {} } function b() { return 1; }"; + const tree = parser.parse(source); + + const matches = query.matches(tree.rootNode); + + assert.deepEqual(formatMatches(tree, matches), [ + { + pattern: 3, + captures: [ + { name: "body", text: "{ constructor() {} }" }, + ], + }, + { + pattern: 1, + captures: [ + { name: "body", text: "{ return 1; }" }, + ], + }, + ]); + }); + }); + + describe(".startIndexForPattern", () => { + it("returns the index where the given pattern starts in the query's source", () => { + const patterns1 = ` + "+" @operator + "-" @operator + "*" @operator + "=" @operator + "=>" @operator + `.trimStart(); + + const patterns2 = ` + (identifier) @a + (string) @b + `.trimStart(); + + const patterns3 = ` + ((identifier) @b (#match? @b i)) + (function_declaration name: (identifier) @c) + (method_definition name: (property_identifier) @d) + `.trimStart(); + + const source = patterns1 + patterns2 + patterns3; + + const query = new Query(JavaScript, source); + + assert.equal(query.startIndexForPattern(0), 0); + assert.equal(query.startIndexForPattern(5), patterns1.length); + assert.equal(query.startIndexForPattern(7), patterns1.length + patterns2.length); + }); + }); + + describe(".isPatternGuaranteedAtStep", () => { + it("returns true if the given pattern is guaranteed to match at the given step", () => { + const rows = [ + { + description: "no guaranteed steps", + language: Python, + pattern: `(expression_statement (string))`, + resultsBySubstring: [["expression_statement", false], ["string", false]], + }, + { + description: "all guaranteed steps", + language: JavaScript, + pattern: `(object "{" "}")`, + resultsBySubstring: [["object", false], ["{", true], ["}", true]], + }, + { + description: "a fallible step that is optional", + language: JavaScript, + pattern: `(object "{" (identifier)? @foo "}")`, + resultsBySubstring: [ + ["object", false], + ["{", true], + ["(identifier)?", false], + ["}", true], + ], + }, + { + description: "multiple fallible steps that are optional", + language: JavaScript, + pattern: `(object "{" (identifier)? @id1 ("," (identifier) @id2)? "}")`, + resultsBySubstring: [ + ["object", false], + ["{", true], + ["(identifier)? @id1", false], + ["\",\"", false], + ["}", true], + ], + }, + { + description: "guaranteed step after fallibe step", + language: JavaScript, + pattern: `(pair (property_identifier) ":")`, + resultsBySubstring: [["pair", false], ["property_identifier", false], [":", true]], + }, + { + description: "fallible step in between two guaranteed steps", + language: JavaScript, + pattern: `(ternary_expression + condition: (_) + "?" + consequence: (call_expression) + ":" + alternative: (_))`, + resultsBySubstring: [ + ["condition:", false], + ["\"?\"", false], + ["consequence:", false], + ["\":\"", true], + ["alternative:", true], + ], + }, + { + description: "one guaranteed step after a repetition", + language: JavaScript, + pattern: `(object "{" (_) "}")`, + resultsBySubstring: [["object", false], ["{", false], ["(_)", false], ["}", true]], + }, + { + description: "guaranteed steps after multiple repetitions", + language: JSON, + pattern: `(object "{" (pair) "," (pair) "," (_) "}")`, + resultsBySubstring: [ + ["object", false], + ["{", false], + ["(pair) \",\" (pair)", false], + ["(pair) \",\" (_)", false], + ["\",\" (_)", false], + ["(_)", true], + ["}", true], + ], + }, + { + description: "a guaranteed step with a field", + language: JavaScript, + pattern: `(binary_expression left: (expression) right: (_))`, + resultsBySubstring: [ + ["binary_expression", false], + ["(expression)", false], + ["(_)", true], + ], + }, + { + description: "multiple guaranteed steps with fields", + language: JavaScript, + pattern: `(function_declaration name: (identifier) body: (statement_block))`, + resultsBySubstring: [ + ["function_declaration", false], + ["identifier", true], + ["statement_block", true], + ], + }, + { + description: "nesting, one guaranteed step", + language: JavaScript, + pattern: ` + (function_declaration + name: (identifier) + body: (statement_block "{" (expression_statement) "}"))`, + resultsBySubstring: [ + ["function_declaration", false], + ["identifier", false], + ["statement_block", false], + ["{", false], + ["expression_statement", false], + ["}", true], + ], + }, + { + description: "a guaranteed step after some deeply nested hidden nodes", + language: Ruby, + pattern: ` + (singleton_class + value: (constant) + "end") + `, + resultsBySubstring: [ + ["singleton_class", false], + ["constant", false], + ["end", true], + ], + }, + { + description: "nesting, no guaranteed steps", + language: JavaScript, + pattern: ` + (call_expression + function: (member_expression + property: (property_identifier) @template-tag) + arguments: (template_string)) @template-call + `, + resultsBySubstring: [["property_identifier", false], ["template_string", false]], + }, + { + description: "a guaranteed step after a nested node", + language: JavaScript, + pattern: ` + (subscript_expression + object: (member_expression + object: (identifier) @obj + property: (property_identifier) @prop) + "[") + `, + resultsBySubstring: [ + ["identifier", false], + ["property_identifier", false], + ["[", true], + ], + }, + { + description: "a step that is fallible due to a predicate", + language: JavaScript, + pattern: ` + (subscript_expression + object: (member_expression + object: (identifier) @obj + property: (property_identifier) @prop) + "[" + (#match? @prop "foo")) + `, + resultsBySubstring: [ + ["identifier", false], + ["property_identifier", false], + ["[", true], + ], + }, + { + description: "alternation where one branch has guaranteed steps", + language: JavaScript, + pattern: ` + [ + (unary_expression (identifier)) + (call_expression + function: (_) + arguments: (_)) + (binary_expression right:(call_expression)) + ] + `, + resultsBySubstring: [ + ["identifier", false], + ["right:", false], + ["function:", true], + ["arguments:", true], + ], + }, + { + description: "guaranteed step at the end of an aliased parent node", + language: Ruby, + pattern: ` + (method_parameters "(" (identifier) @id")") + `, + resultsBySubstring: [["\"(\"", false], ["(identifier)", false], ["\")\"", true]], + }, + { + description: "long, but not too long to analyze", + language: JavaScript, + pattern: ` + (object "{" (pair) (pair) (pair) (pair) "}") + `, + resultsBySubstring: [ + ["\"{\"", false], + ["(pair)", false], + ["(pair) \"}\"", false], + ["\"}\"", true], + ], + }, + { + description: "too long to analyze", + language: JavaScript, + pattern: ` + (object "{" (pair) (pair) (pair) (pair) (pair) (pair) (pair) (pair) (pair) (pair) (pair) (pair) "}") + `, + resultsBySubstring: [ + ["\"{\"", false], + ["(pair)", false], + ["(pair) \"}\"", false], + ["\"}\"", false], + ], + }, + { + description: "hidden nodes that have several fields", + language: Java, + pattern: ` + (method_declaration name: (identifier)) + `, + resultsBySubstring: [["name:", true]], + }, + { + description: "top-level non-terminal extra nodes", + language: Ruby, + pattern: ` + (heredoc_body + (interpolation) + (heredoc_end) @end) + `, + resultsBySubstring: [ + ["(heredoc_body", false], + ["(interpolation)", false], + ["(heredoc_end)", true], + ], + }, + { + description: "multiple extra nodes", + language: Rust, + pattern: ` + (call_expression + (line_comment) @a + (line_comment) @b + (arguments)) + `, + resultsBySubstring: [ + ["(line_comment) @a", false], + ["(line_comment) @b", false], + ["(arguments)", true], + ], + }, + ]; + + for (const row of rows) { + const query = new Query(row.language, row.pattern); + for (const [substring, isDefinite] of row.resultsBySubstring) { + const offset = row.pattern.indexOf(substring); + assert.equal( + query.isPatternGuaranteedAtStep(offset), + isDefinite, + `Description: ${row.description}, Pattern: ${row.pattern}, Substring: ${substring}, expected isDefinite to be: ${isDefinite}`, + ); + } + } + }); + }); + + describe(".isPatternRooted", () => { + it("returns true if the given pattern has a single root node", () => { + const rows = [ + { + description: "simple token", + pattern: "(identifier)", + isRooted: true, + }, + { + description: "simple non-terminal", + pattern: "(function_definition name: (identifier))", + isRooted: true, + }, + { + description: "alternative of many tokens", + pattern: '["if" "def" (identifier) (comment)]', + isRooted: true, + }, + { + description: "alternative of many non-terminals", + pattern: ` + [ + (function_definition name: (identifier)) + (class_definition name: (identifier)) + (block) + ] + `, + isRooted: true, + }, + { + description: "two siblings", + pattern: '("{" "}")', + isRooted: false, + }, + { + description: "top-level repetition", + pattern: "(comment)*", + isRooted: false, + }, + { + description: "alternative where one option has two siblings", + pattern: ` + [ + (block) + (class_definition) + ("(" ")") + (function_definition) + ] + `, + isRooted: false, + }, + { + description: "alternative where one option has a top-level repetition", + pattern: ` + [ + (block) + (class_definition) + (comment)* + (function_definition) + ] + `, + isRooted: false, + }, + ]; + + parser.setLanguage(Python); + for (const row of rows) { + const query = new Query(Python, row.pattern); + assert.equal( + query.isPatternRooted(0), + row.isRooted, + `Description: ${row.description}, Pattern: ${row.pattern}`, + ); + } + }); + }); + + describe(".isPatternNonLocal", () => { + it("returns true if the given pattern has a single root node", () => { + const rows = [ + { + description: "simple token", + pattern: "(identifier)", + language: Python, + isNonLocal: false, + }, + { + description: "siblings that can occur in an argument list", + pattern: "((identifier) (identifier))", + language: Python, + isNonLocal: true, + }, + { + description: "siblings that can occur in a statement block", + pattern: "((return_statement) (return_statement))", + language: Python, + isNonLocal: true, + }, + { + description: "siblings that can occur in a source file", + pattern: "((function_definition) (class_definition))", + language: Python, + isNonLocal: true, + }, + { + description: "siblings that can't occur in any repetition", + pattern: `("{" "}")`, + language: Python, + isNonLocal: false, + }, + { + description: "siblings that can't occur in any repetition, wildcard root", + pattern: `(_ "{" "}") @foo`, + language: JavaScript, + isNonLocal: false, + }, + { + description: "siblings that can occur in a class body, wildcard root", + pattern: "(_ (method_definition) (method_definition)) @foo", + language: JavaScript, + isNonLocal: true, + }, + { + description: "top-level repetitions that can occur in a class body", + pattern: "(method_definition)+ @foo", + language: JavaScript, + isNonLocal: true, + }, + { + description: "top-level repetitions that can occur in a statement block", + pattern: "(return_statement)+ @foo", + language: JavaScript, + isNonLocal: true, + }, + { + description: "rooted pattern that can occur in a statement block", + pattern: "(return_statement) @foo", + language: JavaScript, + isNonLocal: false, + }, + ]; + + for (const row of rows) { + const query = new Query(row.language, row.pattern); + assert.equal( + query.isPatternNonLocal(0), + row.isNonLocal, + `Description: ${row.description}, Pattern: ${row.pattern}`, + ); + } + }); + }); + + describe("max start depth", () => { + it("will not explore child nodes beyond the given depth", () => { + const source = ` +if (a1 && a2) { + if (b1 && b2) { } + if (c) { } +} +if (d) { + if (e1 && e2) { } + if (f) { } +} +`; + + const rows = [ + { + description: "depth 0: match translation unit", + depth: 0, + pattern: ` + (translation_unit) @capture + `, + matches: [ + [0, [["capture", "if (a1 && a2) {\n if (b1 && b2) { }\n if (c) { }\n}\nif (d) {\n if (e1 && e2) { }\n if (f) { }\n}\n"]]], + ] + }, + { + description: "depth 0: match none", + depth: 0, + pattern: ` + (if_statement) @capture + `, + matches: [] + }, + { + description: "depth 1: match 2 if statements at the top level", + depth: 1, + pattern: ` + (if_statement) @capture + `, + matches: [ + [0, [["capture", "if (a1 && a2) {\n if (b1 && b2) { }\n if (c) { }\n}"]]], + [0, [["capture", "if (d) {\n if (e1 && e2) { }\n if (f) { }\n}"]]], + ] + }, + { + description: "depth 1 with deep pattern: match the only the first if statement", + depth: 1, + pattern: ` + (if_statement + condition: (parenthesized_expression + (binary_expression) + ) + ) @capture + `, + matches: [ + [0, [["capture", "if (a1 && a2) {\n if (b1 && b2) { }\n if (c) { }\n}"]]], + ] + }, + { + description: "depth 3 with deep pattern: match all if statements with a binexpr condition", + depth: 3, + pattern: ` + (if_statement + condition: (parenthesized_expression + (binary_expression) + ) + ) @capture + `, + matches: [ + [0, [["capture", "if (a1 && a2) {\n if (b1 && b2) { }\n if (c) { }\n}"]]], + [0, [["capture", "if (b1 && b2) { }"]]], + [0, [["capture", "if (e1 && e2) { }"]]], + ] + }, + ]; + + parser.setLanguage(C); + const tree = parser.parse(source); + + for (const row of rows) { + const query = new Query(C, row.pattern); + const matches = query.matches(tree.rootNode, { maxStartDepth: row.depth }); + const expected = row.matches.map(([pattern, captures]) => ({ + pattern, + captures: captures.map(([name, text]) => ({ name, text })), + })); + assert.deepEqual(formatMatches(tree, matches), expected, row.description); + } + }); + + it("tests more", () => { + const source = ` +{ + { } + { + { } + } +} +` + const rows = [ + { + depth: 0, + matches: [ + [0, [["capture", "{\n { }\n {\n { }\n }\n}"]]], + ] + }, + { + depth: 1, + matches: [ + [0, [["capture", "{\n { }\n {\n { }\n }\n}"]]], + [0, [["capture", "{ }"]]], + [0, [["capture", "{\n { }\n }"]]], + ] + }, + { + depth: 2, + matches: [ + [0, [["capture", "{\n { }\n {\n { }\n }\n}"]]], + [0, [["capture", "{ }"]]], + [0, [["capture", "{\n { }\n }"]]], + [0, [["capture", "{ }"]]], + ] + }, + ]; + + parser.setLanguage(C); + const tree = parser.parse(source); + const query = new Query(C, "(compound_statement) @capture"); + const matches = query.matches(tree.rootNode); + const node = matches[0].captures[0].node; + assert.equal(node.type, "compound_statement"); + + for (const row of rows) { + const matches = query.matches(node, { maxStartDepth: row.depth }); + const expected = row.matches.map(([pattern, captures]) => ({ + pattern, + captures: captures.map(([name, text]) => ({ name, text })), + })); + assert.deepEqual(formatMatches(tree, matches), expected); + } + }); + }); }); function formatMatches(tree, matches) { diff --git a/test/tree_test.js b/test/tree_test.js index 579ed06a..e7f56879 100644 --- a/test/tree_test.js +++ b/test/tree_test.js @@ -1,6 +1,7 @@ const Parser = require(".."); const JavaScript = require('tree-sitter-javascript'); const { assert } = require("chai"); +const { constants } = require("perf_hooks"); describe("Tree", () => { let parser; @@ -10,6 +11,38 @@ describe("Tree", () => { parser.setLanguage(JavaScript) }); + describe('.rootNodeWithOffset', () => { + it('returns the root node of the tree, offset by the given byte offset', () => { + const tree = parser.parse(' if (a) b'); + const node = tree.rootNodeWithOffset(6, {row: 2, column: 2}); + assert.equal(node.startIndex, 8); + assert.equal(node.endIndex, 16); + assert.deepEqual(node.startPosition, {row: 2, column: 4}); + assert.deepEqual(node.endPosition, {row: 2, column: 12}); + + const child = node.firstChild.child(2); + assert.equal(child.type, 'expression_statement'); + assert.equal(child.startIndex, 15); + assert.equal(child.endIndex, 16); + assert.deepEqual(child.startPosition, {row: 2, column: 11}); + assert.deepEqual(child.endPosition, {row: 2, column: 12}); + + const cursor = node.walk(); + cursor.gotoFirstChild(); + cursor.gotoFirstChild(); + cursor.gotoNextSibling(); + assertCursorState(cursor, { + nodeType: 'parenthesized_expression', + nodeIsNamed: true, + nodeIsMissing: false, + startPosition: {row: 2, column: 7}, + endPosition: {row: 2, column: 10}, + startIndex: 11, + endIndex: 14 + }); + }); + }); + describe('.edit', () => { let input, edit diff --git a/tree-sitter.d.ts b/tree-sitter.d.ts index 0a54488d..0c959ac8 100644 --- a/tree-sitter.d.ts +++ b/tree-sitter.d.ts @@ -1,11 +1,15 @@ declare module "tree-sitter" { class Parser { parse(input: string | Parser.Input | Parser.InputReader, oldTree?: Parser.Tree, options?: { bufferSize?: number, includedRanges?: Parser.Range[] }): Parser.Tree; + getIncludedRanges(): Parser.Range[]; + getTimeoutMicros(): number; + setTimeoutMicros(timeout: number): void; getLanguage(): any; setLanguage(language: any): void; getLogger(): Parser.Logger; setLogger(logFunc: Parser.Logger): void; - printDotGraphs(enabled: boolean): void; + printDotGraphs(enabled: boolean, fd?: number): void; + reset(): void; } namespace Parser { @@ -47,8 +51,11 @@ declare module "tree-sitter" { export interface SyntaxNode { tree: Tree; + id: number; + typeId: number; + grammarId: number; type: string; - typeId: string; + grammarName: string; isNamed: boolean; text: string; startPosition: Point; @@ -68,13 +75,23 @@ declare module "tree-sitter" { nextNamedSibling: SyntaxNode | null; previousSibling: SyntaxNode | null; previousNamedSibling: SyntaxNode | null; + parseState: number; + nextParseState: number; + descendantCount: number; hasChanges(): boolean; hasError(): boolean; isMissing(): boolean; + isExtra(): boolean; + isError(): boolean; toString(): string; child(index: number): SyntaxNode | null; namedChild(index: number): SyntaxNode | null; + childForFieldName(fieldName: string): SyntaxNode | null; + childForFieldId(fieldId: number): SyntaxNode | null; + fieldNameForChild(childIndex: number): string | null; + childrenForFieldName(fieldName: string): Array; + childrenForFieldId(fieldId: number): Array; firstChildForIndex(index: number): SyntaxNode | null; firstNamedChildForIndex(index: number): SyntaxNode | null; @@ -103,22 +120,32 @@ declare module "tree-sitter" { endIndex: number; readonly currentNode: SyntaxNode; readonly currentFieldName: string; + readonly currentFieldId: number; + readonly currentDepth: number; + readonly currentDescendantIndex: number; reset(node: SyntaxNode): void + resetTo(other: TreeCursor): void; gotoParent(): boolean; gotoFirstChild(): boolean; - gotoFirstChildForIndex(index: number): boolean; + gotoLastChild(): boolean; + gotoFirstChildForIndex(goalIndex: number): boolean; + gotoFirstChildForPosition(goalPosition: Point): boolean; gotoNextSibling(): boolean; + gotoPreviousSibling(): boolean; + gotoDescendant(goalDescendantIndex: number): boolean; } export interface Tree { readonly rootNode: SyntaxNode; + rootNodeWithOffset(offsetBytes: number, offsetExtent: Point): SyntaxNode; edit(delta: Edit): Tree; walk(): TreeCursor; getChangedRanges(other: Tree): Range[]; + getIncludedRanges(): Range[]; getEditedRange(other: Tree): Range; - printDotGraph(): void; + printDotGraph(fd?: number): void; } export interface QueryMatch { @@ -140,11 +167,52 @@ declare module "tree-sitter" { readonly setProperties: any[]; readonly assertedProperties: any[]; readonly refutedProperties: any[]; + readonly matchLimit: number; + + constructor( + language: any, + source: string | Buffer, + ); + + matches( + rootNode: SyntaxNode, + options?: { + startPosition?: Point; + endPosition?: Point; + startIndex?: number; + endIndex?: number; + matchLimit?: number; + maxStartDepth?: number; + } + ): QueryMatch[]; + captures( + rootNode: SyntaxNode, + options?: { + startPosition?: Point; + endPosition?: Point; + startIndex?: number; + endIndex?: number; + matchLimit?: number; + maxStartDepth?: number; + } + ): QueryCapture[]; + disableCapture(captureName: string): void; + disablePattern(patternIndex: number): void; + isPatternGuaranteedAtStep(byteOffset: number): boolean; + isPatternRooted(patternIndex: number): boolean; + isPatternNonLocal(patternIndex: number): boolean; + startIndexForPattern(patternIndex: number): number; + didExceedMatchLimit(): boolean; + } - constructor(language: any, source: string | Buffer); + export class LookaheadIterator { + readonly currentSymbol: number; + readonly currentSymbolName: string; - matches(rootNode: SyntaxNode, startPosition?: Point, endPosition?: Point): QueryMatch[]; - captures(rootNode: SyntaxNode, startPosition?: Point, endPosition?: Point): QueryCapture[]; + reset(language: any, state: number): boolean; + resetState(state: number): boolean; + next(): number; + iterNames(): string[]; } } diff --git a/vendor/tree-sitter b/vendor/tree-sitter index 4b933268..524bf7e2 160000 --- a/vendor/tree-sitter +++ b/vendor/tree-sitter @@ -1 +1 @@ -Subproject commit 4b933268980b258c70d064b03045877b4a4799b7 +Subproject commit 524bf7e2c664d4a5dbd0c20d4d10f1e58f99e8ce