diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c5351db2..1e066c8f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -41,6 +41,7 @@ and adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). - Axum routes now link to their handler when rustfmt wraps the `.route(` call so the handler sits on a later line, and when the handler's name also appears elsewhere on the line, as in `delete(handlers::user::delete)` or `.route("/login", post(handlers::user::login))`; Actix's `web::resource(…)` and `.route(…)` registrations get the same fix. A call inside a closure handler is no longer mistaken for a route, and an Actix resource no longer takes the next route's handler as its own. Thanks @mg-mg-mg for the report and @danusha2345. (#2326) - In JavaScript and TypeScript, the functions written inside a named object literal are now symbols of their own even when the object isn't exported: a plain `const api = { load() {…} }`, an object declared inside an IIFE or a function, and a namespace hung on the page or on another object, like `window.App = { init() {…} }`, `App.utils = {…}` or `dw_page = {…}`. Calls such as `api.load()`, `window.App.init()`, `App.utils.pad()`, a sibling's `this.render()`, and `App.init()` from another script on the page now reach them, the calls made inside a member belong to that member instead of the object, and a bare `init()` is no longer taken for `App.init`, so script-tag apps no longer lose most of their code from `callers` and impact. A member's qualified name now carries its object, exported or not (`api::load`, `window.App::init`), so asking for `App.init` finds it. Re-index JavaScript and TypeScript projects after upgrading. Thanks @tkhoaaa for the report and @danusha2345. (#2300) - In JavaScript and TypeScript files with Windows (CRLF) line endings, as a Windows checkout usually has them, a store exported on a later line, like `const useStore = create(…)` followed by `export default useStore;`, now has its actions indexed, as it already did with Unix line endings. Before, those actions were missing from the graph on Windows, so their callers and impact came back empty. Re-index JavaScript and TypeScript projects after upgrading. +- In VB.NET, a name that begins with a keyword, like `SharedCache`, `Dimension`, `PublicKey` or `FriendlyName`, is now indexed whole, and a method like `Sub NewItem()` is no longer taken for a constructor. Before, the keyword was cut off (`Cache`, `ension`, `Key`) or the declaration was lost, and a line in a method that starts with such a name, like `Constants.Reload()` or `ConstVBV.Value = False`, was misread as a declaration, so reads of and calls to these names didn't link. A `Private Protected` member now counts as private instead of public. Re-index VB.NET projects after upgrading. ## [1.6.2] - 2026-10-03 diff --git a/__tests__/vbnet-keyword-prefixed-names.test.ts b/__tests__/vbnet-keyword-prefixed-names.test.ts new file mode 100644 index 000000000..271e1dd64 --- /dev/null +++ b/__tests__/vbnet-keyword-prefixed-names.test.ts @@ -0,0 +1,242 @@ +/** + * VB.NET names that begin with a keyword. + * + * The vendored grammar lexed the member modifiers (`Public`, `Shared`, `Dim`, + * `Const`, …) as one token that outranked identifiers, so the lexer stopped at + * the end of a modifier spelled at the start of a name: `Public SharedCache As + * Integer` indexed a field `Cache`, `Public Dimension` a field `ension`, + * `Private FriendlyNameValue` (staxrip) a field `lyNameValue`, and `Public + * Shared Shared1` lost the field to a parse error. Inside a method `Dim` and + * `Const` did the same: `ConstVBV.Value = False` (staxrip) parsed as a + * declaration `Const VBV`, and `ConstructPath()` as `Const ructPath()`. And + * `Sub NewItem()` lexed as the constructor `Sub New` followed by `Item`. + * + * Two things the old lexing got right only by accident are pinned too: the + * attribute lines above a top-level declaration belong to it, and `Option` + * lines may follow a comment banner. + */ +import { describe, it, expect, afterAll, beforeAll } from 'vitest'; +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { CodeGraph } from '../src'; +import { extractFromSource } from '../src/extraction'; +import { getParser, initGrammars, loadGrammarsForLanguages } from '../src/extraction/grammars'; + +// The issue's class, verbatim. +const HOLDER = `Public Class Holder + Public Shared Shared1 As Integer + Public Shared SharedCache As Integer + Public Dimension As Integer + Public Endpoint As String + Public PublicKey As String + Private Property1 As Integer + Public Shared Function SharedHelper() As Integer + Return 1 + End Function + Public Sub EndSession() + End Sub + Public Normal As Integer +End Class +`; + +const MODIFIERS = [ + 'Public', 'Private', 'Protected', 'Friend', 'ReadOnly', 'WriteOnly', 'Shared', 'Shadows', 'MustInherit', + 'NotInheritable', 'Overrides', 'NotOverridable', 'Overridable', 'Overloads', 'WithEvents', 'Widening', + 'Narrowing', 'Partial', 'Async', 'Iterator', 'Dim', 'Const', 'Static', 'MustOverride', +]; + +function namesOf(source: string, kind: string): string[] { + return extractFromSource('Probe.vb', source).nodes.filter((n) => n.kind === kind).map((n) => n.name); +} + +beforeAll(async () => { + await initGrammars(); + await loadGrammarsForLanguages(['vbnet']); +}); + +describe('VB.NET names that begin with a keyword', () => { + it('index the fields of the issue under their whole names', () => { + expect(namesOf(HOLDER, 'field')) + .toEqual(['Shared1', 'SharedCache', 'Dimension', 'Endpoint', 'PublicKey', 'Property1', 'Normal']); + expect(namesOf(HOLDER, 'method')).toEqual(['SharedHelper', 'EndSession']); + }); + + it('keep every member modifier out of a name it begins, in any case', () => { + const source = [ + 'Public Class Fields', + ...MODIFIERS.flatMap((m) => [ + ` Private ${m}Field As Integer`, + ` PUBLIC SHARED ${m.toUpperCase()}UPPER As Integer`, + ` friend ${m.toLowerCase()}lower As Integer`, + ]), + 'End Class', + '', + ].join('\n'); + expect(namesOf(source, 'field')).toEqual( + MODIFIERS.flatMap((m) => [`${m}Field`, `${m.toUpperCase()}UPPER`, `${m.toLowerCase()}lower`]), + ); + }); + + it('still read the modifiers themselves', () => { + const source = `Public Class Settings + Public Shared ReadOnly Limit As Integer = 1 + Public Shared SharedCache As Integer + Protected Friend Inherited As Integer + Private Protected Derived As Integer + Friend FriendlyName As String + Private Const ConstantRate As Integer = 2 +End Class +`; + const fields = new Map(extractFromSource('Probe.vb', source).nodes + .filter((n) => n.kind === 'field' || n.kind === 'constant') + .map((n) => [n.name, n])); + expect([...fields.keys()]).toEqual(['Limit', 'SharedCache', 'Inherited', 'Derived', 'FriendlyName', 'ConstantRate']); + expect(fields.get('SharedCache')?.isStatic).toBe(true); + expect(fields.get('Inherited')?.visibility).toBe('protected'); + // `Private Protected` was one token that matched no visibility, so it read as public. + expect(fields.get('Derived')?.visibility).toBe('private'); + expect(fields.get('FriendlyName')?.visibility).toBe('internal'); + }); + + it('tell Sub New from a method whose name begins with New', () => { + const source = `Public Class Menu + Public Sub New() + End Sub + Shared Sub New() + End Sub + Private Sub NewFromDefaultsToolStripMenuItem_Click() Handles NewFromDefaultsToolStripMenuItem.Click + End Sub + Public Sub NewItem() + End Sub +End Class +`; + expect(namesOf(source, 'method')).toEqual(['New', 'New', 'NewFromDefaultsToolStripMenuItem_Click', 'NewItem']); + }); + + it('keep a statement that begins with Dim or Const a statement', () => { + const source = `Public Class Encoder + Private ConstVBV As New BoolParam() + Sub Apply() + ConstVBV.Value = False + ConstVBV.Reset() + ConstructPath() + DimSomething() + Constants.Reload() + Dim constructor As Object = Nothing + constructor = Nothing + End Sub +End Class +`; + const result = extractFromSource('Probe.vb', source); + expect(result.nodes.filter((n) => n.kind === 'field').map((n) => n.name)).toEqual(['ConstVBV']); + const refs = result.unresolvedReferences + .filter((r) => r.referenceKind === 'calls' || r.referenceKind === 'references') + .map((r) => `${r.referenceKind} ${r.referenceName}`); + expect(refs).toEqual([ + 'references ConstVBV.Value', + 'calls ConstVBV.Reset', + 'calls ConstructPath', + 'calls DimSomething', + 'calls Constants.Reload', + ]); + }); +}); + +describe('VB.NET top-level lines', () => { + it('give a declaration every attribute line above it', () => { + const source = `Imports System + + + +Public Class Settings +End Class + + +Class Plain +End Class +`; + const classes = extractFromSource('Probe.vb', source).nodes.filter((n) => n.kind === 'class'); + expect(classes.map((n) => [n.name, n.startLine])).toEqual([['Settings', 3], ['Plain', 8]]); + }); + + it('parse Option lines after a comment banner', () => { + const source = `'------------------------------------------------------------------------------ +' +' This code was generated by a tool. +' +'------------------------------------------------------------------------------ + +Option Strict On +Option Explicit On + +Namespace My.Resources + Friend Module Resources + End Module +End Namespace +`; + const tree = getParser('vbnet')!.parse(source)!; + try { + expect(tree.rootNode.hasError).toBe(false); + expect(tree.rootNode.descendantsOfType('option_statement')).toHaveLength(2); + } finally { + tree.delete(); + } + }); +}); + +describe('VB.NET references to keyword-prefixed names', () => { + let root = ''; + let cg: CodeGraph; + const files: Record = { + 'Holder.vb': HOLDER.replace(' Public Normal As Integer\n', ` Public Normal As Integer + Public Sub NewItem() + End Sub +`), + 'Constants.vb': `Public Module Constants + Public Sub Reload() + End Sub +End Module +`, + 'User.vb': `Public Class User + Public Sub Run(x As Holder) + Dim a As Integer = Holder.Dimension + Holder.SharedCache = 2 + Dim c As Integer = Holder.Shared1 + Holder.SharedHelper() + Dim h As New Holder() + h.NewItem() + Constants.Reload() + End Sub +End Class +`, + }; + + beforeAll(async () => { + root = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-vb-keyword-names-')); + for (const [rel, content] of Object.entries(files)) fs.writeFileSync(path.join(root, rel), content); + cg = await CodeGraph.init(root, { index: true }); + }); + + afterAll(() => { + cg?.close(); + if (root) fs.rmSync(root, { recursive: true, force: true }); + }); + + function linkedFrom(qualifiedName: string, kind: 'references' | 'calls'): string[] { + const found = cg.getNodesInFiles(Object.keys(files)).filter((n) => n.qualifiedName === qualifiedName); + expect(found.map((n) => n.qualifiedName)).toEqual([qualifiedName]); + return cg.getIncomingEdgesTo([found[0]!.id], [kind]).map((e) => cg.getNode(e.source)!.qualifiedName); + } + + it('link reads and writes through the class to the field', () => { + expect(linkedFrom('Holder::Dimension', 'references')).toEqual(['User::Run']); + expect(linkedFrom('Holder::SharedCache', 'references')).toEqual(['User::Run']); + expect(linkedFrom('Holder::Shared1', 'references')).toEqual(['User::Run']); + }); + + it('link calls to methods whose names begin with a keyword', () => { + expect(linkedFrom('Holder::SharedHelper', 'calls')).toEqual(['User::Run']); + expect(linkedFrom('Holder::NewItem', 'calls')).toEqual(['User::Run']); + expect(linkedFrom('Constants::Reload', 'calls')).toEqual(['User::Run']); + }); +}); diff --git a/docs/grammars/tree-sitter-vbnet.md b/docs/grammars/tree-sitter-vbnet.md index 3de667d35..4aa84e257 100644 --- a/docs/grammars/tree-sitter-vbnet.md +++ b/docs/grammars/tree-sitter-vbnet.md @@ -82,6 +82,31 @@ on those repos plus SCrawler and PCL: 12. **LINQ queries** — query expressions no longer require a trailing `Select`/`Group` clause, `Aggregate`-led queries, and `Distinct`/`Skip`/`Take` clauses. +13. **Names that begin with a keyword** — upstream lexed every member + modifier as one `token(prec(10, choice(…)))` and `Sub New` as one + `token(prec(100, …))`. A token that outranks the `prec(-1)` identifier + ends where its text ends, so `Public SharedCache` lexed as `Shared` + + `Cache`, `Public Dimension` as `Dim` + `ension`, and `Sub NewItem()` as a + constructor followed by `Item`. `Dim` and `Const` also appeared inside + that token, which kept the local-declaration `Dim`/`Const` out of keyword + extraction too, so `Constants.X = 1` in a method lexed as `Const ants.X`. + The modifiers and `Sub New` are now plain keywords, which the `word` + lexer matches only after it has read the whole identifier. `Protected + Friend` and `Private Protected` parse as two modifiers, and `prec(1)` on + `member_modifier` keeps a line that could start either a member or a + local declaration read as a member, as before. + The high-precedence tokens had also let the lexer fold a newline into the + keyword after it. Two parses depended on that by accident, so they are + now part of the grammar. `option_statements` may begin with newlines (an + `Option` line under a comment banner, as in every `My Project` + designer file). Upstream's top-level `file_attribute_section` is gone, so + every attribute line above a top-level declaration belongs to that + declaration, as one inside a class or namespace already did. + Measured on all five corpora below: clean parses rose on staxrip (138 → + 141 of 145) and fell nowhere. Apart from the fixed names and the + positions of the newlines no longer folded into a keyword, the trees + changed only where `Protected Friend` became two modifiers and where an + attribute line now attaches to its declaration. ### External scanner (`src/scanner.c`, new) @@ -130,6 +155,16 @@ Known remaining gap (localized ERROR regions, deliberately unpatched): `word:` stays and column-0 labels keep a localized error; indented labels parse fine. Worth an upstream tree-sitter investigation eventually. +Before changing a keyword rule, run `tree-sitter generate --log` and read the +`Keywords - exclude …` lines. A keyword-shaped token that is left out of +keyword extraction is lexed with its own precedence, and if that precedence is +above the identifier's it splits every identifier that starts with it. Those +lines name the tokens left out and the reason: a token that matches the same +string as another one, or that conflicts with a token, such as `"\n"`. Four +remain: `Inherits`, plain `Implements` and both spellings of `IsNot`. Valid VB +never starts a name where one of them can appear, so they split nothing in +valid code. + ## Rebuild ```bash @@ -156,6 +191,14 @@ npx tree-sitter generate # src/scanner.c from the patch is picked u npx tree-sitter build --wasm -o tree-sitter-vbnet.wasm # needs emscripten or Docker ``` +The CLI's Docker fallback uses emscripten 4.0.4. A local emsdk at the same +version works too, including on Windows (`emsdk install 4.0.4`, +`emsdk activate 4.0.4`, then `emsdk_env`). Built that way from this patch, +the vendored wasm's sha256 is +`5a8aa2911e28e43e22b077ae56c8143ccfe52485bb6719152b60e2e66a1a9e53`. The +wasm this one replaced did not rebuild byte for byte with that toolchain, but +the rebuild parsed every file in SCrawler and staxrip to the same tree. + Upstream's checked-in `test/corpus` expectations predate its own grammar.js (every corpus test fails at the pinned commit, before any patching), so the five-repo parse-health sweep above — plus 16 construct repros and the diff --git a/docs/grammars/tree-sitter-vbnet.patch b/docs/grammars/tree-sitter-vbnet.patch index 8caba699f..37918d81e 100644 --- a/docs/grammars/tree-sitter-vbnet.patch +++ b/docs/grammars/tree-sitter-vbnet.patch @@ -1,5 +1,5 @@ diff --git a/grammar.js b/grammar.js -index 21c8523..00bf2b1 100644 +index 21c8523..c9c1d85 100644 --- a/grammar.js +++ b/grammar.js @@ -21,10 +21,14 @@ const PRECEDENCE = { @@ -53,14 +53,46 @@ index 21c8523..00bf2b1 100644 [ $.class_declaration, $.structure_declaration, -@@ -89,7 +88,6 @@ module.exports = grammar({ +@@ -89,30 +88,29 @@ module.exports = grammar({ ], ], - word: ($) => $._identifier_token, rules: { ++ // An attribute list on the line above a top-level declaration belongs to ++ // it: VB continues the line after the closing `>`. With no standalone ++ // attribute-line rule, the newline after the list is an extra here, as it ++ // already is inside a class or namespace. source_file: ($) => + seq( + optional($.option_statements), + repeat(choice( + prec(10, $.imports_statement), + prec(10, $.global_attribute_section), +- prec(10, $.file_attribute_section), + $._declaration, + $._statement, + $._block_terminator + )) + ), + +- file_attribute_section: ($) => +- seq( +- $.attribute_list, +- $._terminator +- ), +- +- +- option_statements: ($) => repeat1($.option_statement), ++ // Files often open with a comment banner, so newlines may come before the ++ // first `Option` line. Only this path can be followed by `Option`, so it ++ // needs no precedence against a top-level empty line. ++ option_statements: ($) => ++ seq(optional($._block_terminator), repeat1($.option_statement)), + + option_statement: ($) => + seq( @@ -157,13 +155,14 @@ module.exports = grammar({ ), @@ -79,15 +111,87 @@ index 21c8523..00bf2b1 100644 identifier: ($) => choice( -@@ -194,7 +193,6 @@ module.exports = grammar({ - ci("MustInherit"), - ci("NotInheritable"), - ci("Overrides"), +@@ -174,41 +173,45 @@ module.exports = grammar({ + _implements_keyword: ($) => token(prec(10, ci("Implements"))), + _inherits_keyword: ($) => token(prec(10, ci("Inherits"))), + +- _sub_new: ($) => token(prec(100, seq(ci("Sub"), /\s+/, ci("New")))), +- ++ // `Sub New` as two keywords rather than one token(prec(100, ...)). The ++ // single token ended at `New`, so `Sub NewItem()` lexed as a constructor ++ // followed by a stray `Item`. The keyword lexer reads `NewItem` whole. ++ _sub_new: ($) => seq(ci("Sub"), ci("New")), ++ ++ // Each modifier is its own keyword, not one token(prec(10, choice(...))). ++ // That token outranked the prec(-1) identifier, so the lexer stopped at the ++ // end of a modifier spelled at the start of a name: `Public SharedCache` ++ // lexed as Shared + Cache, `Public Dimension` as Dim + ension. Keywords go ++ // through the `word` lexer, which reads the whole identifier first. ++ // `Protected Friend` and `Private Protected` are two modifiers. `Dim` and ++ // `Const` are now the same tokens as local_declaration_modifier's, so ++ // `Constants.X = 1` no longer lexes as `Const ants.X = 1`; prec(1) keeps ++ // the old reading (a member modifier) where a line could start either. + member_modifier: ($) => +- token( +- prec( +- 10, +- choice( +- ci("Public"), +- ci("Private"), +- ci("Protected"), +- ci("Friend"), +- ci("Protected Friend"), +- ci("Private Protected"), +- ci("ReadOnly"), +- ci("WriteOnly"), +- ci("Shared"), +- ci("Shadows"), +- ci("MustInherit"), +- ci("NotInheritable"), +- ci("Overrides"), - ci("MustOverride"), - ci("NotOverridable"), - ci("Overridable"), - ci("Overloads"), -@@ -225,43 +223,6 @@ module.exports = grammar({ +- ci("NotOverridable"), +- ci("Overridable"), +- ci("Overloads"), +- ci("WithEvents"), +- ci("Widening"), +- ci("Narrowing"), +- ci("Partial"), +- ci("Async"), +- ci("Iterator"), +- ci("Dim"), +- ci("Const") +- ) +- ) +- ), ++ prec(1, choice( ++ ci("Public"), ++ ci("Private"), ++ ci("Protected"), ++ ci("Friend"), ++ ci("ReadOnly"), ++ ci("WriteOnly"), ++ ci("Shared"), ++ ci("Shadows"), ++ ci("MustInherit"), ++ ci("NotInheritable"), ++ ci("Overrides"), ++ ci("NotOverridable"), ++ ci("Overridable"), ++ ci("Overloads"), ++ ci("WithEvents"), ++ ci("Widening"), ++ ci("Narrowing"), ++ ci("Partial"), ++ ci("Async"), ++ ci("Iterator"), ++ ci("Dim"), ++ ci("Const") ++ )), + + _method_body_statement: ($) => + choice( +@@ -225,43 +228,6 @@ module.exports = grammar({ $._statement // Fallback to regular statements ), @@ -131,7 +235,7 @@ index 21c8523..00bf2b1 100644 local_declaration_modifier: ($) => choice(ci("Dim"), ci("Const"), ci("Static")), -@@ -300,6 +261,8 @@ module.exports = grammar({ +@@ -300,6 +266,8 @@ module.exports = grammar({ _primary_expression: ($) => choice( $._literal, @@ -140,7 +244,7 @@ index 21c8523..00bf2b1 100644 $.identifier, $.parenthesized_expression, $.generic_invocation_expression, // Add this before regular invocation -@@ -320,17 +283,26 @@ module.exports = grammar({ +@@ -320,17 +288,26 @@ module.exports = grammar({ ), generic_invocation_expression: ($) => @@ -170,7 +274,7 @@ index 21c8523..00bf2b1 100644 member_access_expression: ($) => prec.left( PRECEDENCE.MEMBER_ACCESS, -@@ -350,20 +322,23 @@ module.exports = grammar({ +@@ -350,20 +327,23 @@ module.exports = grammar({ ) ), @@ -199,7 +303,7 @@ index 21c8523..00bf2b1 100644 ")" ) ), -@@ -471,16 +446,53 @@ module.exports = grammar({ +@@ -471,16 +451,53 @@ module.exports = grammar({ ), lambda_expression: ($) => @@ -258,7 +362,7 @@ index 21c8523..00bf2b1 100644 typeof_is_expression: ($) => prec.left( -@@ -488,7 +500,7 @@ module.exports = grammar({ +@@ -488,7 +505,7 @@ module.exports = grammar({ seq( ci("TypeOf"), field("expression", $._expression), @@ -267,7 +371,7 @@ index 21c8523..00bf2b1 100644 field("type", $._type) ) ), -@@ -504,29 +516,12 @@ module.exports = grammar({ +@@ -504,29 +521,12 @@ module.exports = grammar({ "initializer", choice( $.object_initializer, @@ -298,7 +402,7 @@ index 21c8523..00bf2b1 100644 object_initializer: ($) => -@@ -551,6 +546,8 @@ module.exports = grammar({ +@@ -551,6 +551,8 @@ module.exports = grammar({ collection_initializer: ($) => seq(ci("From"), "{", commaSep($._expression), "}"), @@ -307,7 +411,7 @@ index 21c8523..00bf2b1 100644 array_creation_expression: ($) => seq( -@@ -646,6 +643,9 @@ module.exports = grammar({ +@@ -646,6 +648,9 @@ module.exports = grammar({ seq(field("object", $._expression), "!", field("member", $.identifier)), _literal: ($) => choice( @@ -317,7 +421,7 @@ index 21c8523..00bf2b1 100644 $.string_literal, $.character_literal, $.integer_literal, -@@ -654,16 +654,49 @@ module.exports = grammar({ +@@ -654,16 +659,49 @@ module.exports = grammar({ $.nothing_literal, $.date_literal ), @@ -371,7 +475,7 @@ index 21c8523..00bf2b1 100644 ) ), boolean_literal: ($) => choice(ci("True"), ci("False")), -@@ -672,7 +705,7 @@ module.exports = grammar({ +@@ -672,7 +710,7 @@ module.exports = grammar({ // Names and types _name_reference: ($) => @@ -380,7 +484,7 @@ index 21c8523..00bf2b1 100644 simple_name: ($) => $.identifier, -@@ -682,7 +715,7 @@ module.exports = grammar({ +@@ -682,7 +720,7 @@ module.exports = grammar({ seq( field("qualifier", $._name_reference), ".", @@ -389,7 +493,7 @@ index 21c8523..00bf2b1 100644 ) ), global_qualified_name: ($) => -@@ -694,6 +727,7 @@ module.exports = grammar({ +@@ -694,6 +732,7 @@ module.exports = grammar({ $.simple_name, $.qualified_name, $.generic_name, @@ -397,7 +501,7 @@ index 21c8523..00bf2b1 100644 $.predefined_type, $.array_type, $.tuple_type -@@ -720,7 +754,8 @@ module.exports = grammar({ +@@ -720,7 +759,8 @@ module.exports = grammar({ ), generic_name: ($) => seq(field("name", $.identifier), $.type_argument_list), @@ -407,7 +511,7 @@ index 21c8523..00bf2b1 100644 array_type: ($) => seq(field("element_type", $._type), $.array_rank_specifier), array_rank_specifier: ($) => seq("(", repeat(","), ")"), -@@ -742,10 +777,14 @@ module.exports = grammar({ +@@ -742,10 +782,14 @@ module.exports = grammar({ field("type", $._type) ), @@ -426,7 +530,7 @@ index 21c8523..00bf2b1 100644 implements_member_clause: ($) => seq(ci("Implements"), commaSep1($._name_reference)), -@@ -856,7 +895,7 @@ module.exports = grammar({ +@@ -856,7 +900,7 @@ module.exports = grammar({ enum_member_declaration: ($) => seq( @@ -435,7 +539,7 @@ index 21c8523..00bf2b1 100644 field("name", $.identifier), optional(seq("=", field("value", $._expression))), $._terminator -@@ -882,6 +921,10 @@ module.exports = grammar({ +@@ -882,6 +926,10 @@ module.exports = grammar({ prec(10, $.inherits_statement), prec(10, $.implements_statement), prec(100, $.constructor_declaration), @@ -446,7 +550,7 @@ index 21c8523..00bf2b1 100644 // Explicitly add statements that can appear at class level prec(200, $.try_statement), // Very high precedence prec(200, $.select_statement), // Very high precedence -@@ -936,33 +979,21 @@ module.exports = grammar({ +@@ -936,33 +984,21 @@ module.exports = grammar({ variable_declarator: ($) => seq( field("name", $.identifier), @@ -483,7 +587,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), commaSep1($.variable_declarator), $._terminator -@@ -970,7 +1001,8 @@ module.exports = grammar({ +@@ -970,7 +1006,8 @@ module.exports = grammar({ field_variable_declarator: ($) => seq( @@ -493,7 +597,7 @@ index 21c8523..00bf2b1 100644 optional($.array_rank_specifier), optional($.as_clause), optional(seq("=", field("initializer", $._expression))) -@@ -978,12 +1010,13 @@ module.exports = grammar({ +@@ -978,12 +1015,13 @@ module.exports = grammar({ property_declaration: ($) => seq( @@ -509,7 +613,7 @@ index 21c8523..00bf2b1 100644 optional($.implements_member_clause), optional(seq("=", field("initializer", $._expression))), choice( -@@ -1001,7 +1034,7 @@ module.exports = grammar({ +@@ -1001,7 +1039,7 @@ module.exports = grammar({ property_getter: ($) => seq( @@ -518,7 +622,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), ci("Get"), $._block_terminator, -@@ -1013,7 +1046,7 @@ module.exports = grammar({ +@@ -1013,7 +1051,7 @@ module.exports = grammar({ property_setter: ($) => seq( @@ -527,7 +631,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), ci("Set"), optional(seq("(", field("parameter", $.parameter), ")")), -@@ -1028,7 +1061,7 @@ module.exports = grammar({ +@@ -1028,7 +1066,7 @@ module.exports = grammar({ prec( 1, seq( @@ -536,7 +640,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), choice( seq( -@@ -1063,7 +1096,7 @@ module.exports = grammar({ +@@ -1063,7 +1101,7 @@ module.exports = grammar({ prec( 100, seq( @@ -545,7 +649,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), $._sub_new, optional(field("parameters", $.parameter_list)), -@@ -1087,7 +1120,7 @@ module.exports = grammar({ +@@ -1087,7 +1125,7 @@ module.exports = grammar({ event_declaration: ($) => seq( @@ -554,7 +658,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), // Changed ci("Event"), field("name", $.identifier), -@@ -1098,7 +1131,7 @@ module.exports = grammar({ +@@ -1098,7 +1136,7 @@ module.exports = grammar({ operator_declaration: ($) => seq( @@ -563,7 +667,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), // Changed ci("Operator"), field( -@@ -1152,7 +1185,7 @@ module.exports = grammar({ +@@ -1152,7 +1190,7 @@ module.exports = grammar({ interface_method_declaration: ($) => seq( @@ -572,7 +676,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), // Changed choice(ci("Sub"), ci("Function")), field("name", $.identifier), -@@ -1164,7 +1197,7 @@ module.exports = grammar({ +@@ -1164,7 +1202,7 @@ module.exports = grammar({ interface_property_declaration: ($) => seq( @@ -581,7 +685,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), // Changed ci("Property"), field("name", $.identifier), -@@ -1175,7 +1208,7 @@ module.exports = grammar({ +@@ -1175,7 +1213,7 @@ module.exports = grammar({ interface_event_declaration: ($) => seq( @@ -590,7 +694,7 @@ index 21c8523..00bf2b1 100644 optional(field("modifiers", repeat1($.member_modifier))), // Changed ci("Event"), field("name", $.identifier), -@@ -1209,7 +1242,7 @@ module.exports = grammar({ +@@ -1209,7 +1247,7 @@ module.exports = grammar({ parameter: ($) => seq( @@ -599,7 +703,7 @@ index 21c8523..00bf2b1 100644 optional( field( "modifiers", -@@ -1273,7 +1306,7 @@ module.exports = grammar({ +@@ -1273,7 +1311,7 @@ module.exports = grammar({ declaration_statement: ($) => prec(1, seq( // Add precedence @@ -608,7 +712,7 @@ index 21c8523..00bf2b1 100644 field("modifiers", choice( $.local_declaration_modifier, repeat1($.local_declaration_modifier) -@@ -1282,6 +1315,65 @@ module.exports = grammar({ +@@ -1282,6 +1320,65 @@ module.exports = grammar({ $._terminator )), @@ -674,7 +778,7 @@ index 21c8523..00bf2b1 100644 expression_statement: ($) => seq($._expression, $._terminator), assignment_statement: ($) => -@@ -1301,21 +1393,96 @@ module.exports = grammar({ +@@ -1301,21 +1398,96 @@ module.exports = grammar({ $._terminator ) ), @@ -775,7 +879,7 @@ index 21c8523..00bf2b1 100644 $._block_terminator, field("then_branch", repeat($._statement)), repeat($.elseif_clause), -@@ -1331,7 +1498,7 @@ module.exports = grammar({ +@@ -1331,7 +1503,7 @@ module.exports = grammar({ seq( ci("ElseIf"), field("condition", $._expression), @@ -784,7 +888,7 @@ index 21c8523..00bf2b1 100644 $._block_terminator, field("body", repeat($._statement)) ), -@@ -1445,7 +1612,7 @@ module.exports = grammar({ +@@ -1445,7 +1617,7 @@ module.exports = grammar({ using_statement: ($) => seq( ci("Using"), @@ -793,7 +897,7 @@ index 21c8523..00bf2b1 100644 $._block_terminator, field("body", repeat($._statement)), ci("End"), -@@ -1590,14 +1757,25 @@ module.exports = grammar({ +@@ -1590,14 +1762,25 @@ module.exports = grammar({ $._terminator ), // LINQ Query expressions query_expression: ($) => @@ -826,7 +930,7 @@ index 21c8523..00bf2b1 100644 ), query_body_clause: ($) => choice( -@@ -1606,48 +1784,62 @@ module.exports = grammar({ +@@ -1606,48 +1789,62 @@ module.exports = grammar({ $.where_clause, $.join_clause, $.order_by_clause, @@ -914,7 +1018,7 @@ index 21c8523..00bf2b1 100644 aggregate_function: ($) => seq( field( -@@ -1668,42 +1860,44 @@ module.exports = grammar({ +@@ -1668,42 +1865,44 @@ module.exports = grammar({ select_or_group_clause: ($) => choice($.select_clause, $.group_by_clause), select_clause: ($) => seq(ci("Select"), commaSep1($.select_element)), select_element: ($) => @@ -973,7 +1077,7 @@ index 21c8523..00bf2b1 100644 // Comments comment: ($) => token(prec(100, choice( -@@ -1714,7 +1908,7 @@ module.exports = grammar({ +@@ -1714,7 +1913,7 @@ module.exports = grammar({ ))), // Preprocessor Directives diff --git a/src/extraction/languages/vbnet.ts b/src/extraction/languages/vbnet.ts index e8502b2e3..35f634953 100644 --- a/src/extraction/languages/vbnet.ts +++ b/src/extraction/languages/vbnet.ts @@ -87,9 +87,12 @@ export const vbnetExtractor: LanguageExtractor = { // this, calls inside every Sub/Function would be skipped. resolveBody: (node: SyntaxNode) => node, getReturnType: extractVbnetReturnType, + // `Protected Friend` and `Private Protected` are two modifiers each, so they + // read as protected and private, like C#'s `protected internal` and + // `private protected`. getVisibility: (node) => { if (hasModifier(node, /^private$/i)) return 'private'; - if (hasModifier(node, /^protected(\s+friend)?$/i)) return 'protected'; + if (hasModifier(node, /^protected$/i)) return 'protected'; if (hasModifier(node, /^friend$/i)) return 'internal'; return 'public'; // VB members default to Public in practice }, diff --git a/src/extraction/wasm/tree-sitter-vbnet.wasm b/src/extraction/wasm/tree-sitter-vbnet.wasm index 365a6c603..c09cc4746 100755 Binary files a/src/extraction/wasm/tree-sitter-vbnet.wasm and b/src/extraction/wasm/tree-sitter-vbnet.wasm differ