From 8312afaef01300f7bfe8474491696b9da1ec7f8e Mon Sep 17 00:00:00 2001 From: Hannah Date: Sat, 19 Sep 2026 15:15:24 -0400 Subject: [PATCH 1/8] feat: parallelize initial indexing --- .gitignore | 4 + extension.toml | 6 +- languages/objectscript/highlights.scm | 43 +- languages/objectscript/injections.scm | 35 +- languages/objectscript_routine/highlights.scm | 28 +- languages/objectscript_udl/highlights.scm | 22 +- languages/objectscript_udl/injections.scm | 49 +- objectscript-lsp/Cargo.toml | 7 +- objectscript-lsp/README.md | 13 +- .../crates/objectscript-core/Cargo.toml | 11 +- .../benches/update_document.rs | 4 +- .../examples/cold_workspace_bench.rs | 278 ++++++++ .../examples/update_bench.rs | 4 +- .../crates/objectscript-core/src/common.rs | 33 +- .../objectscript-core/src/global_semantic.rs | 134 +--- .../crates/objectscript-core/src/method.rs | 147 +++- .../objectscript-core/src/parse_structures.rs | 51 +- .../objectscript-core/src/scope_tree.rs | 49 +- .../crates/objectscript-core/src/variable.rs | 8 +- .../crates/objectscript-core/src/workspace.rs | 648 ++++++++++++++---- .../gotodef/relative-method-call/hk.cls | 1 + .../relative-method-call/hksubclass.cls | 4 +- .../test-dotted-block-actual.mac | 0 .../test-nested-refactor-actual.mac | 0 .../test-refactor-do-actual.mac | 0 .../test-dotted-block-expected.mac | 0 .../test-nested-refactor-expected.mac | 0 .../test-refactor-do-expected.mac | 0 objectscript-lsp/src/backend_testing.rs | 160 ++--- objectscript-lsp/src/common.rs | 144 +++- objectscript-lsp/src/lsp.rs | 6 +- objectscript-lsp/src/main.rs | 68 +- objectscript-lsp/src/server.rs | 201 ++---- objectscript-lsp/src/test.rs | 104 ++- 34 files changed, 1579 insertions(+), 683 deletions(-) create mode 100644 objectscript-lsp/crates/objectscript-core/examples/cold_workspace_bench.rs rename objectscript-lsp/objectscript-tests/{dotted-block => refactor-actual}/test-dotted-block-actual.mac (100%) rename objectscript-lsp/objectscript-tests/{nested_dots => refactor-actual}/test-nested-refactor-actual.mac (100%) rename objectscript-lsp/objectscript-tests/{routines => refactor-actual}/test-refactor-do-actual.mac (100%) rename objectscript-lsp/objectscript-tests/{dotted-block => refactor-expected}/test-dotted-block-expected.mac (100%) rename objectscript-lsp/objectscript-tests/{nested_dots => refactor-expected}/test-nested-refactor-expected.mac (100%) rename objectscript-lsp/objectscript-tests/{routines => refactor-expected}/test-refactor-do-expected.mac (100%) diff --git a/.gitignore b/.gitignore index f7a375a..33a2e7f 100644 --- a/.gitignore +++ b/.gitignore @@ -10,7 +10,11 @@ Cargo.lock .tabnine/ vscode/ objectscript-lsp/objectscript-tests/local/ +objectscript-lsp/objectscript-tests/failing/ objectscript-lsp/documentation/code-changes-AI objectscript-lsp/documentation/local objectscript-lsp/claude-code-objectscript-lsp/ objectscript-lsp/.claude-plugin/ +.zed/ +objectscript-lsp/objectscript-tests/refactor-expected/test-large-dotted-statements-expected.mac +objectscript-lsp/objectscript-tests/refactor-actual/test-large-dotted-statements-actual.mac diff --git a/extension.toml b/extension.toml index cb96e41..cbb490b 100644 --- a/extension.toml +++ b/extension.toml @@ -8,17 +8,17 @@ repository = "https://github.com/intersystems/zed-objectscript" [grammars.objectscript_udl] repository = "https://github.com/intersystems/tree-sitter-objectscript" -commit = "58432d03bfba25efad3059a52d0916d714739b09" +commit = "48a06b6401af62cd238d0c0f4a75dbd9bc803c28" path = "udl" [grammars.objectscript_routine] repository = "https://github.com/intersystems/tree-sitter-objectscript" -commit = "58432d03bfba25efad3059a52d0916d714739b09" +commit = "48a06b6401af62cd238d0c0f4a75dbd9bc803c28" path = "objectscript_routine" [grammars.objectscript] repository = "https://github.com/intersystems/tree-sitter-objectscript" -commit = "58432d03bfba25efad3059a52d0916d714739b09" +commit = "48a06b6401af62cd238d0c0f4a75dbd9bc803c28" path = "objectscript" [grammars.xml] diff --git a/languages/objectscript/highlights.scm b/languages/objectscript/highlights.scm index 7107b02..92739c8 100644 --- a/languages/objectscript/highlights.scm +++ b/languages/objectscript/highlights.scm @@ -20,7 +20,9 @@ [ (keyword_super) (keyword_pound_pound_class) -] @keyword + (keyword_quote_directive) + (keyword_pound_expression) +] @property (system_defined_function) @constant.builtin @@ -78,6 +80,7 @@ (ssvn) (system_defined_variable) "$$" + (keyword_this) ] @variable.special [ @@ -86,7 +89,15 @@ ] @variant (method_arg) @variable.parameter - +[(line_comment_1) +(line_comment_2) +(line_comment_3) +(line_comment_4) +(block_comment) +(inline_comment) +(argumentless_inline_comment) +] @comment +(documatic_line) @comment.doc ; I didn't include ( or ) in this, because they are often grouped ; as part of a sequence that gets turned into a single token, so they ; don't get matched, and one ends up getting colored differently than the other. @@ -139,16 +150,15 @@ "?" ] @operator -; === END EXPR === -; === BEGIN CORE === (macro_arg) @variant (macro_value) @constant.builtin -(macro_def) @preproc +(macro_def) @property [ (keyword_pound_define) + (keyword_sqlcompile) (keyword_pound_def1arg) (keyword_pound_if) (keyword_pound_elseif) @@ -162,7 +172,8 @@ (keyword_pound_delay) (locktype) (tag_end_if) -] @preproc + (pound_execute) +] @property [ (keyword_as) @@ -193,6 +204,7 @@ (keyword_ztrap) (keyword_zz) (keyword_print) + (keyword_select) (keyword_zprint) (keyword_set) (keyword_write) @@ -220,6 +232,7 @@ (keyword_zremove) (command_keyword) (keyword_zload) + (keyword_zsave) (keyword_for) (keyword_while) (keyword_continue) @@ -234,6 +247,7 @@ (keyword_throw) (keyword_try) (keyword_catch) + (keyword_zedit) ] @keyword [ @@ -252,6 +266,11 @@ (html_marker_reversed) ] @punctuation.special +[ + (pound_if_special_case_else) + (pound_if_special_case) +] @comment + (tag) @function "--" @operator @@ -284,8 +303,7 @@ "." @constant.builtin) ; === END CORE === -; === BEGIN LOCAL === -(iris_username) @preproc +(iris_username) @property [ (keyword_import) @@ -293,6 +311,7 @@ (keyword_includegenerator) (keyword_method) (keyword_classmethod) + (keyword_clientmethod) (keyword_extends) (keyword_property) (keyword_relationship) @@ -329,7 +348,6 @@ (property_keyword) ] @type.builtin - [ (query_name) (trigger_name) @@ -341,19 +359,14 @@ (xdata_name) (storage_name) (xml_identifier) - (index_property) (column_name) ] @variant [ - (return_type) (keyword_list) - (parameter_type) - (index_type) - (index_property_type) (typename) ] @type.builtin -; === END UDL === + ; routine (routine_type) @type.builtin diff --git a/languages/objectscript/injections.scm b/languages/objectscript/injections.scm index 94c8c91..887d020 100644 --- a/languages/objectscript/injections.scm +++ b/languages/objectscript/injections.scm @@ -27,8 +27,11 @@ ; Keywords, one of type language = "python", none of type codemode ; External method body injection based on [ Language = ... ] (method_definition - (python_method_body_content) @injection.content + (method_keyword_external_language + (typename) @_lang) + (external_method_body_content) @injection.content (#set! injection.include-children "true") + (#any-of? @_lang "python" "Python" "PYTHON") (#set! injection.language "python")) (method_definition @@ -48,8 +51,11 @@ (#set! injection.language "ispl")) (trigger - (python_method_body_content) @injection.content + (method_keyword_external_language + (typename) @_lang) + (external_method_body_content) @injection.content (#set! injection.include-children "true") + (#any-of? @_lang "python" "Python" "PYTHON") (#set! injection.language "python")) (trigger @@ -87,6 +93,14 @@ (#any-of? @_mt "text/markdown" "\"text/markdown\"") (#set! injection.language "markdown")) +(xdata + (xdata_keyword_mimetype + (typename) @_mt) + (external_method_body_content) @injection.content + (#set! injection.include-children "true") + (#any-of? @_mt "text/x-python" "\"text/x-python\"" "application/python" "\"application/python\"") + (#set! injection.language "python")) + (xdata (xdata_keyword_mimetype (typename) @_mt) @@ -127,11 +141,12 @@ (#any-of? @_mt "text/css" "\"text/css\"") (#set! injection.language "css")) -; ----------------------------------------- -; XDATA default (no MimeType): XML fallback -; ----------------------------------------- -(xdata - (external_method_body_content) @injection.content +; -------------------------------------------- +; XDATA default (no MimeType header): XML fallback +; -------------------------------------------- +((xdata + (external_method_body_content) @injection.content) @_xdata + (#not-match? @_xdata "^[^{}]*[Mm][Ii][Mm][Ee][Tt][Yy][Pp][Ee][ \t\r\n]*=") (#set! injection.include-children "true") (#set! injection.language "xml")) @@ -141,7 +156,7 @@ (#set! injection.language "xml") (#set! injection.include-children "true")) - ([ +([ (line_comment_1) (line_comment_2) (line_comment_3) @@ -150,7 +165,7 @@ (inline_comment) (argumentless_inline_comment) (documatic_line) - ] @injection.content - (#set! injection.language "comment")) +] @injection.content + (#set! injection.language "comment")) ; === END LOCAL === diff --git a/languages/objectscript_routine/highlights.scm b/languages/objectscript_routine/highlights.scm index bed8b8d..dd8116d 100644 --- a/languages/objectscript_routine/highlights.scm +++ b/languages/objectscript_routine/highlights.scm @@ -22,7 +22,9 @@ [ (keyword_super) (keyword_pound_pound_class) -] @keyword + (keyword_quote_directive) + (keyword_pound_expression) +] @property (system_defined_function) @constant.builtin @@ -55,7 +57,7 @@ [ (routine_name) (class_name) -] @type +] @enum [ (macro_function) @@ -77,6 +79,7 @@ [ (ssvn) (system_defined_variable) + (keyword_this) "$$" ] @variable.special @@ -86,6 +89,15 @@ ] @variant (method_arg) @variable.parameter +[(line_comment_1) +(line_comment_2) +(line_comment_3) +(line_comment_4) +(block_comment) +(inline_comment) +(argumentless_inline_comment) +] @comment +(documatic_line) @comment.doc ; I didn't include ( or ) in this, because they are often grouped ; as part of a sequence that gets turned into a single token, so they @@ -145,7 +157,7 @@ (macro_value) @constant.builtin -(macro_def) @preproc +(macro_def) @property [ (keyword_pound_define) @@ -157,12 +169,14 @@ (keyword_pound_ifdef) (keyword_pound_ifndef) (keyword_dim) + (keyword_sqlcompile) (keyword_pound_import) (keyword_pound_include) (keyword_pound_delay) (locktype) + (pound_execute) (tag_end_if) -] @preproc +] @property [ (keyword_as) @@ -195,6 +209,7 @@ (keyword_print) (keyword_zprint) (keyword_set) + (keyword_select) (keyword_write) (keyword_zwrite) (keyword_do) @@ -220,6 +235,7 @@ (keyword_zremove) (command_keyword) (keyword_zload) + (keyword_zsave) (keyword_for) (keyword_while) (keyword_continue) @@ -234,7 +250,8 @@ (keyword_throw) (keyword_try) (keyword_catch) -] @type.builtin + (keyword_zedit) +] @keyword [ (keyword_embedded_html) @@ -258,6 +275,7 @@ ] @comment (tag) @function +(typename) @type.builtin "--" @operator diff --git a/languages/objectscript_udl/highlights.scm b/languages/objectscript_udl/highlights.scm index 0fb74ab..1b839fb 100644 --- a/languages/objectscript_udl/highlights.scm +++ b/languages/objectscript_udl/highlights.scm @@ -20,7 +20,9 @@ [ (keyword_super) (keyword_pound_pound_class) -] @keyword + (keyword_quote_directive) + (keyword_pound_expression) +] @property (system_defined_function) @constant.builtin @@ -78,6 +80,7 @@ (ssvn) (system_defined_variable) "$$" + (keyword_this) ] @variable.special [ @@ -153,10 +156,11 @@ (macro_value) @constant.builtin -(macro_def) @preproc +(macro_def) @property [ (keyword_pound_define) + (keyword_sqlcompile) (keyword_pound_def1arg) (keyword_pound_if) (keyword_pound_elseif) @@ -170,7 +174,8 @@ (keyword_pound_delay) (locktype) (tag_end_if) -] @preproc + (pound_execute) +] @property [ (keyword_as) @@ -201,6 +206,7 @@ (keyword_ztrap) (keyword_zz) (keyword_print) + (keyword_select) (keyword_zprint) (keyword_set) (keyword_write) @@ -228,6 +234,7 @@ (keyword_zremove) (command_keyword) (keyword_zload) + (keyword_zsave) (keyword_for) (keyword_while) (keyword_continue) @@ -242,6 +249,7 @@ (keyword_throw) (keyword_try) (keyword_catch) + (keyword_zedit) ] @keyword [ @@ -298,7 +306,7 @@ ; === END CORE === ; === BEGIN LOCAL === -(iris_username) @preproc +(iris_username) @property (keyword_class) @attribute @@ -308,6 +316,7 @@ (keyword_includegenerator) (keyword_method) (keyword_classmethod) + (keyword_clientmethod) (keyword_extends) (keyword_property) (keyword_relationship) @@ -355,16 +364,11 @@ (xdata_name) (storage_name) (xml_identifier) - (index_property) (column_name) ] @variant [ - (return_type) (keyword_list) - (parameter_type) - (index_type) - (index_property_type) (typename) ] @type.builtin diff --git a/languages/objectscript_udl/injections.scm b/languages/objectscript_udl/injections.scm index 4dc5b18..887d020 100644 --- a/languages/objectscript_udl/injections.scm +++ b/languages/objectscript_udl/injections.scm @@ -27,8 +27,11 @@ ; Keywords, one of type language = "python", none of type codemode ; External method body injection based on [ Language = ... ] (method_definition - (python_method_body_content) @injection.content + (method_keyword_external_language + (typename) @_lang) + (external_method_body_content) @injection.content (#set! injection.include-children "true") + (#any-of? @_lang "python" "Python" "PYTHON") (#set! injection.language "python")) (method_definition @@ -48,8 +51,11 @@ (#set! injection.language "ispl")) (trigger - (python_method_body_content) @injection.content + (method_keyword_external_language + (typename) @_lang) + (external_method_body_content) @injection.content (#set! injection.include-children "true") + (#any-of? @_lang "python" "Python" "PYTHON") (#set! injection.language "python")) (trigger @@ -87,6 +93,14 @@ (#any-of? @_mt "text/markdown" "\"text/markdown\"") (#set! injection.language "markdown")) +(xdata + (xdata_keyword_mimetype + (typename) @_mt) + (external_method_body_content) @injection.content + (#set! injection.include-children "true") + (#any-of? @_mt "text/x-python" "\"text/x-python\"" "application/python" "\"application/python\"") + (#set! injection.language "python")) + (xdata (xdata_keyword_mimetype (typename) @_mt) @@ -127,11 +141,12 @@ (#any-of? @_mt "text/css" "\"text/css\"") (#set! injection.language "css")) -; ----------------------------------------- -; XDATA default (no MimeType): XML fallback -; ----------------------------------------- -(xdata - (external_method_body_content) @injection.content +; -------------------------------------------- +; XDATA default (no MimeType header): XML fallback +; -------------------------------------------- +((xdata + (external_method_body_content) @injection.content) @_xdata + (#not-match? @_xdata "^[^{}]*[Mm][Ii][Mm][Ee][Tt][Yy][Pp][Ee][ \t\r\n]*=") (#set! injection.include-children "true") (#set! injection.language "xml")) @@ -141,16 +156,16 @@ (#set! injection.language "xml") (#set! injection.include-children "true")) -; === END LOCAL === - ([ -(line_comment_1) -(line_comment_2) -(line_comment_3) -(line_comment_4) -(block_comment) -(inline_comment) -(argumentless_inline_comment) -(documatic_line) + (line_comment_1) + (line_comment_2) + (line_comment_3) + (line_comment_4) + (block_comment) + (inline_comment) + (argumentless_inline_comment) + (documatic_line) ] @injection.content (#set! injection.language "comment")) + +; === END LOCAL === diff --git a/objectscript-lsp/Cargo.toml b/objectscript-lsp/Cargo.toml index 8bf2118..482440d 100644 --- a/objectscript-lsp/Cargo.toml +++ b/objectscript-lsp/Cargo.toml @@ -13,11 +13,12 @@ objectscript-core = { path = "crates/objectscript-core" } tower-lsp = "0.20.0" tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "fs"] } tree-sitter = "0.26.6" -tree-sitter-objectscript = "1.9.16" -tree-sitter-objectscript-routine = "1.9.16" -tree-sitter-objectscript-playground = "1.9.16" +tree-sitter-objectscript = "1.9.20" +tree-sitter-objectscript-routine = "1.9.20" +tree-sitter-objectscript-playground = "1.9.20" tree-sitter-xml = "0.7.0" serde = "1.0.228" serde_json = "1.0.145" parking_lot = "0.12.5" walkdir = "2.5.0" +rayon = "1.12.0" diff --git a/objectscript-lsp/README.md b/objectscript-lsp/README.md index 36daade..06c3cda 100644 --- a/objectscript-lsp/README.md +++ b/objectscript-lsp/README.md @@ -7,7 +7,8 @@ We built this language server to provide editor-independent ObjectScript semanti ## Current Features - Workspace indexing for `.cls`, `.inc`, `.rtn`, `.mac`, and `.int` -- Everything is rebuilt incrementally. +- Cold workspace construction uses a bulk declaration/linking pipeline; live documents are + rebuilt incrementally. - Multi-workspace support through LSP workspace folders, with deepest-parent routing per document - Go-to-definition for ObjectScript variables, orefs, methods, properties, classes, parameters with ProcedureBlock-aware private/public resolution - Go-to-implementation for inherited and overridden methods and classes @@ -31,6 +32,10 @@ We built this language server to provide editor-independent ObjectScript semanti - Public symbols live in `GlobalSemanticModel` - Private symbols are tracked through `LocalSemanticModel` and `ScopeTree` - XML documents are tracked for diagnostics, but they do not enter the class/routine semantic rebuild pipeline +- The bulk indexing phases, ownership model, SYS classification, and performance changes are + documented in [documentation/bulk-workspace-indexing.md](documentation/bulk-workspace-indexing.md). +- Remaining measured bottlenecks and optimization priorities are documented in + [documentation/indexing-optimization-roadmap.md](documentation/indexing-optimization-roadmap.md). ## Workspace Layout @@ -76,9 +81,9 @@ In this case, the `DependencyGraph` is used to determine all possible paths to t ## Grammar Baseline - `tree-sitter = 0.26.6` -- `tree-sitter-objectscript = 1.9.16` -- `tree-sitter-objectscript-routine = 1.9.16` -- `tree-sitter-objectscript-playground = 1.9.16` +- `tree-sitter-objectscript = 1.9.20` +- `tree-sitter-objectscript-routine = 1.9.20` +- `tree-sitter-objectscript-playground = 1.9.20` - `tree-sitter-xml = 0.7.0` ## Roadmap diff --git a/objectscript-lsp/crates/objectscript-core/Cargo.toml b/objectscript-lsp/crates/objectscript-core/Cargo.toml index 0daa3bc..fe8503d 100644 --- a/objectscript-lsp/crates/objectscript-core/Cargo.toml +++ b/objectscript-lsp/crates/objectscript-core/Cargo.toml @@ -1,10 +1,10 @@ [package] name = "objectscript-core" -version = "0.1.1" +version = "0.1.8" edition = "2024" authors = ["Hannah Kimura "] license = "MIT" -description = "Language Server for ObjectScript" +description = "Internal Representation of an ObjectScript Workspace" [features] update-bench = [] @@ -18,9 +18,9 @@ required-features = ["update-bench"] tower-lsp = "0.20.0" tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "fs"] } tree-sitter = "0.26.6" -tree-sitter-objectscript = "1.9.16" -tree-sitter-objectscript-routine = "1.9.16" -tree-sitter-objectscript-playground = "1.9.16" +tree-sitter-objectscript = "1.9.20" +tree-sitter-objectscript-routine = "1.9.20" +tree-sitter-objectscript-playground = "1.9.20" tree-sitter-xml = "0.7.0" serde = "1.0.228" serde_json = "1.0.145" @@ -29,6 +29,7 @@ walkdir = "2.5.0" regex = "1.12.3" petgraph = "0.8.3" evalexpr = "13.1.0" +rayon = "1.12.0" [dev-dependencies] criterion = { version = "0.5", features = ["html_reports"] } diff --git a/objectscript-lsp/crates/objectscript-core/benches/update_document.rs b/objectscript-lsp/crates/objectscript-core/benches/update_document.rs index a2979d8..f6783da 100644 --- a/objectscript-lsp/crates/objectscript-core/benches/update_document.rs +++ b/objectscript-lsp/crates/objectscript-core/benches/update_document.rs @@ -8,7 +8,7 @@ use objectscript_core::parse_structures::{ClassId, FileType}; use objectscript_core::workspace::{ ProjectData, full_update_document_call_count, reset_full_update_document_call_count, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::env; use std::fs; use std::hint::black_box; @@ -204,6 +204,8 @@ fn build_project_data(prepared: &PreparedEdit) -> ProjectData { inheritance_diagonstics: HashMap::new(), method_reference_diagnostics: HashMap::new(), other_class_diagnostics: HashMap::new(), + sys_classes: HashSet::new(), + sys_classes_overwritten: HashSet::new(), }; let (class_range, class_name, _class_name_def) = get_member_name_and_range_from_root( diff --git a/objectscript-lsp/crates/objectscript-core/examples/cold_workspace_bench.rs b/objectscript-lsp/crates/objectscript-core/examples/cold_workspace_bench.rs new file mode 100644 index 0000000..a2d7403 --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/examples/cold_workspace_bench.rs @@ -0,0 +1,278 @@ +//! CH-009 cold-workspace construction benchmark. +//! +//! This is deliberately an example rather than production instrumentation: all +//! clocks and input generation disappear from normal library builds. + +use objectscript_core::common::{get_member_name_and_range_from_root, initial_build_scope_tree}; +use objectscript_core::config::Config; +use objectscript_core::dependency_tracker::{DependencyGraph, Dependents}; +use objectscript_core::document::Document; +use objectscript_core::global_semantic::GlobalSemanticModel; +use objectscript_core::override_index::OverrideIndex; +use objectscript_core::parse_structures::{ClassId, FileType}; +use objectscript_core::scope_tree::ScopeTree; +use objectscript_core::workspace::{BulkIndexDocument, ProjectData}; +use std::collections::{HashMap, HashSet}; +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; +use std::time::{Duration, Instant}; +use tower_lsp::lsp_types::Url; +use tree_sitter::{Parser, Range, Tree}; +use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; +use tree_sitter_objectscript_routine::LANGUAGE_OBJECTSCRIPT_ROUTINE; +use walkdir::WalkDir; + +#[derive(Clone)] +struct Source { + path: PathBuf, + content: String, + file_type: FileType, +} + +#[derive(Clone)] +struct Parsed { + url: Url, + content: String, + tree: Tree, + file_type: FileType, + class_name: String, + class_range: Range, +} + +#[derive(Default)] +struct Timings { + parse: Duration, + declaration: Duration, + finalization: Duration, + total: Duration, +} + +fn main() { + let sources = match env::args_os().nth(1) { + Some(path) => load_workspace(Path::new(&path)), + None => generated_workspace(), + }; + assert!( + !sources.is_empty(), + "workspace contains no supported source files" + ); + + println!( + "CH-009 cold workspace benchmark: {} documents, {} bytes", + sources.len(), + sources + .iter() + .map(|source| source.content.len()) + .sum::() + ); + let parse_started = Instant::now(); + let parsed = parse_sources(&sources); + let parse = parse_started.elapsed(); + print_timings("sequential add_document", run_sequential(&parsed, parse)); + print_timings("BulkWorkspaceIndex", run_bulk(&parsed, parse)); +} + +fn run_sequential(parsed: &[Parsed], parse: Duration) -> Timings { + let declaration = Instant::now(); + let mut data = empty_project_data(); + for input in parsed { + let class_id = (input.file_type != FileType::Xml) + .then(|| ClassId(data.global_semantic_model.next_id())); + data.add_document( + input.url.clone(), + &input.content, + &input.tree, + input.file_type, + class_id, + input.class_name.clone(), + None, + input.class_range, + ); + } + let declaration = declaration.elapsed(); + Timings { + parse, + declaration, + finalization: Duration::ZERO, + total: parse + declaration, + } +} + +fn run_bulk(parsed: &[Parsed], parse: Duration) -> Timings { + let declaration = Instant::now(); + let mut data = empty_project_data(); + let mut bulk = data.begin_bulk_index(); + for input in parsed { + let scope_tree = if input.file_type == FileType::Xml { + ScopeTree::new() + } else { + initial_build_scope_tree( + &input.tree, + &input.content, + input.file_type == FileType::Routine, + ) + }; + bulk.register(BulkIndexDocument { + url: input.url.clone(), + class_range: input.class_range, + document: Document::new( + input.content.clone(), + input.tree.clone(), + input.file_type, + input.class_name.clone(), + None, + scope_tree, + None, + ), + }); + } + let declaration = declaration.elapsed(); + let finalization = Instant::now(); + bulk.finalize(); + let finalization = finalization.elapsed(); + Timings { + parse, + declaration, + finalization, + total: parse + declaration + finalization, + } +} + +fn parse_sources(sources: &[Source]) -> Vec { + let mut cls_parser = parser(FileType::Cls); + let mut routine_parser = parser(FileType::Routine); + sources + .iter() + .map(|source| { + let parser = match source.file_type { + FileType::Cls => &mut cls_parser, + FileType::Routine => &mut routine_parser, + FileType::Xml => unreachable!("XML is not included in this benchmark"), + }; + let tree = parser + .parse(&source.content, None) + .unwrap_or_else(|| panic!("failed to parse {:?}", source.path)); + let is_routine = source.file_type == FileType::Routine; + let (class_range, class_name, _) = + get_member_name_and_range_from_root(&source.content, tree.root_node(), is_routine) + .unwrap_or_else(|| { + panic!("source has no class/routine name: {:?}", source.path) + }); + let absolute = source + .path + .canonicalize() + .unwrap_or_else(|_| source.path.clone()); + let url = Url::from_file_path(&absolute) + .unwrap_or_else(|_| panic!("cannot create file URL for {:?}", source.path)); + Parsed { + url, + content: source.content.clone(), + tree, + file_type: source.file_type, + class_name, + class_range, + } + }) + .collect() +} + +fn parser(file_type: FileType) -> Parser { + let mut parser = Parser::new(); + let language = match file_type { + FileType::Cls => LANGUAGE_OBJECTSCRIPT_UDL, + FileType::Routine => LANGUAGE_OBJECTSCRIPT_ROUTINE, + FileType::Xml => unreachable!(), + }; + parser.set_language(&language.into()).expect("load grammar"); + parser +} + +fn generated_workspace() -> Vec { + let mut sources = Vec::new(); + // Reverse inheritance order intentionally makes the old incremental path + // exercise its unresolved-reference repair work. + for number in (0..40).rev() { + let parent = if number == 0 { + String::new() + } else { + format!(" Extends Bench.Class{}", number - 1) + }; + let content = format!( + "Class Bench.Class{number}{parent}\n{{\nProperty Value As %Integer;\nMethod Work(arg As %Integer) As %Integer\n{{\n Set local = arg + {number}\n Quit local\n}}\n}}\n" + ); + sources.push(Source { + path: env::temp_dir().join(format!("ch009/Bench.Class{number}.cls")), + content, + file_type: FileType::Cls, + }); + } + for number in 0..10 { + sources.push(Source { + path: env::temp_dir().join(format!("ch009/BenchRoutine{number}.mac")), + content: format!( + "ROUTINE BenchRoutine{number}\n\nMain(arg)\n Set local=arg+{number}\n Quit local\n\nSecond\n Do Main(1)\n Quit\n" + ), + file_type: FileType::Routine, + }); + } + sources +} + +fn load_workspace(root: &Path) -> Vec { + WalkDir::new(root) + .sort_by_file_name() + .into_iter() + .filter_map(Result::ok) + .filter(|entry| entry.file_type().is_file()) + .filter_map(|entry| { + let extension = entry.path().extension()?.to_str()?.to_ascii_lowercase(); + let file_type = match extension.as_str() { + "cls" => FileType::Cls, + "mac" | "int" | "inc" | "rtn" => FileType::Routine, + _ => return None, + }; + Some(Source { + path: entry.path().to_owned(), + content: fs::read_to_string(entry.path()) + .unwrap_or_else(|error| panic!("failed to read {:?}: {error}", entry.path())), + file_type, + }) + }) + .collect() +} + +fn print_timings(label: &str, timings: Timings) { + println!("\n{label}"); + println!(" parse: {:>10.3} ms", millis(timings.parse)); + println!(" declaration: {:>10.3} ms", millis(timings.declaration)); + println!(" finalization: {:>10.3} ms", millis(timings.finalization)); + println!(" total: {:>10.3} ms", millis(timings.total)); +} + +fn millis(duration: Duration) -> f64 { + duration.as_secs_f64() * 1_000.0 +} + +fn empty_project_data() -> ProjectData { + ProjectData { + config: Config::default(), + documents: HashMap::new(), + global_semantic_model: GlobalSemanticModel::new(), + classes: HashMap::new(), + method_defs: HashMap::new(), + pub_var_defs: HashMap::new(), + parameter_defs: HashMap::new(), + property_defs: HashMap::new(), + override_index: OverrideIndex::new(), + dependent_class_index: Dependents::new(), + dependency_graph: DependencyGraph::new(), + unresolved_inheritance_references: HashMap::new(), + unresolved_method_references: HashMap::new(), + inheritance_diagonstics: HashMap::new(), + method_reference_diagnostics: HashMap::new(), + other_class_diagnostics: HashMap::new(), + sys_classes: HashSet::new(), + sys_classes_overwritten: HashSet::new(), + } +} diff --git a/objectscript-lsp/crates/objectscript-core/examples/update_bench.rs b/objectscript-lsp/crates/objectscript-core/examples/update_bench.rs index ceb5976..8f26a0b 100644 --- a/objectscript-lsp/crates/objectscript-core/examples/update_bench.rs +++ b/objectscript-lsp/crates/objectscript-core/examples/update_bench.rs @@ -7,7 +7,7 @@ use objectscript_core::parse_structures::{ClassId, FileType}; use objectscript_core::workspace::{ ProjectData, full_update_document_call_count, reset_full_update_document_call_count, }; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::env; use std::hint::black_box; use std::time::{Duration, Instant}; @@ -189,6 +189,8 @@ fn build_project_data(prepared: &PreparedEdit) -> ProjectData { inheritance_diagonstics: HashMap::new(), method_reference_diagnostics: HashMap::new(), other_class_diagnostics: HashMap::new(), + sys_classes: HashSet::new(), + sys_classes_overwritten: HashSet::new(), }; let (class_range, class_name, new_class_name_def) = get_member_name_and_range_from_root( diff --git a/objectscript-lsp/crates/objectscript-core/src/common.rs b/objectscript-lsp/crates/objectscript-core/src/common.rs index 071256f..3572692 100644 --- a/objectscript-lsp/crates/objectscript-core/src/common.rs +++ b/objectscript-lsp/crates/objectscript-core/src/common.rs @@ -1,4 +1,4 @@ -use crate::parse_structures::{ClassId, MemberType, MethodType, ReturnType}; +use crate::parse_structures::{MemberType, MethodType, ReturnType}; use crate::refactor::count_leading_dots_in_line; use crate::scope_structures::ScopeId; use crate::scope_tree::ScopeTree; @@ -589,7 +589,8 @@ pub fn get_keyword_and_value(keyword: &str) -> (bool, String, Vec<&str>) { let mut keyword_name = "".to_string(); let mut keyword_value: Vec<&str> = Vec::new(); // splits string by spaces or equal sign - let regex = Regex::new(r"[^\s=,()]+").unwrap(); + static KEYWORD_PART: OnceLock = OnceLock::new(); + let regex = KEYWORD_PART.get_or_init(|| Regex::new(r"[^\s=,()]+").unwrap()); let mut count = 0; let values: Vec<&str> = regex.find_iter(keyword).map(|m| m.as_str()).collect(); for value in values { @@ -610,13 +611,8 @@ pub fn get_keyword_and_value(keyword: &str) -> (bool, String, Vec<&str>) { /// /// Creates a new `ScopeTree` rooted at `class_symbol_id`, then walks the syntax tree and adds /// scopes for nodes considered "scope nodes" (see `cls_is_scope_node`). -pub fn initial_build_scope_tree( - tree: &Tree, - class_symbol_id: ClassId, - content: &str, - is_rtn: bool, -) -> ScopeTree { - let mut scope_tree = ScopeTree::new(Some(class_symbol_id)); +pub fn initial_build_scope_tree(tree: &Tree, content: &str, is_rtn: bool) -> ScopeTree { + let mut scope_tree = ScopeTree::new(); let mut scope_stack = vec![scope_tree.root]; let root = tree.root_node(); @@ -938,9 +934,7 @@ pub fn get_subroutine_info( statement_type.named_child((statement_type.named_child_count() - 1) as u32) { match tag_keyword.kind() { - "keyword_methodimpl" => { - eprintln!("TODO: Verify if there is anything to be done for methodimpl keyword"); - } + "keyword_methodimpl" => {} "keyword_private" => { is_public = false; } @@ -996,10 +990,6 @@ pub fn rtn_is_scope_node(node: Node, content: &str) -> (bool, Option, bo let mut sib = node.parent().and_then(|p| p.prev_named_sibling()); while let Some(sibling) = sib { let Some(command) = sibling.named_child(0) else { - eprintln!( - "Sibling node {:?} for tag statement {:?} did not have a child at index 0, skipping (rtn_is_scope_node)", - sibling, node - ); sib = sibling.prev_named_sibling(); continue; }; @@ -1012,10 +1002,6 @@ pub fn rtn_is_scope_node(node: Node, content: &str) -> (bool, Option, bo return (true, None, true); } let Some(command) = last_sib.named_child(0) else { - eprintln!( - "Sibling node {:?} for tag statement {:?} did not have a child at index 0, skipping (rtn_is_scope_node)", - last_sib, node - ); sib = last_sib.prev_named_sibling(); continue; }; @@ -1087,10 +1073,6 @@ pub fn rtn_is_scope_node(node: Node, content: &str) -> (bool, Option, bo let mut curr_sib = parent.prev_named_sibling(); while let Some(sibling) = curr_sib { let Some(command) = sibling.named_child(0) else { - eprintln!( - "Sibling node {:?} did not have a child at index 0, skipping (rtn_is_scope_node)", - sibling.kind() - ); curr_sib = sibling.prev_named_sibling(); continue; }; @@ -1164,6 +1146,9 @@ pub fn get_routine_scope_node_range(node: Node, content: &str) -> (Point, Point) } dotted_statement_scope_end_point = sib.end_position(); next_sibling = sib.next_named_sibling(); + } else { + // non-dotted sibling, so scope ends here. + break; } } return ( diff --git a/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs b/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs index 262e423..d38b722 100644 --- a/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs +++ b/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs @@ -2,8 +2,8 @@ use crate::common::generic_exit_statements; use crate::dependency_tracker::Dependents; use crate::local_semantic::LocalSemanticModel; use crate::parse_structures::{ - Class, ClassId, DfsState, Language, Method, MethodRef, Parameter, ParameterRef, Property, - PropertyRef, PublicVarId, Variable, VariableRef, + Class, ClassId, DfsState, Method, MethodRef, Parameter, ParameterRef, Property, PropertyRef, + PublicVarId, Variable, VariableDefType, VariableRef, }; use crate::scope_structures::{ ClassGlobalSymbol, MethodSymbol, ParameterSymbol, PropertySymbol, ScopeId, @@ -124,19 +124,18 @@ impl GlobalSemanticModel { private_variable_symbols: &Vec, // the private variable symbols in a scope ) -> Option<(Range, String)> { let mut variable_definition: Option = None; - let mut oref_class = None; - let mut potential_variable_indices = HashSet::new(); + let mut oref_class: Option = None; if let Some(variables) = self .variables .get(&method_ref) .and_then(|scopes| scopes.get(&scope_id)) { + let mut potential_variable_indices = HashSet::new(); for (i, variable) in variables.iter().enumerate() { - if variable.is_oref - && &variable.name == variable_name - && let Some(oref_cls) = &variable.cls + if &variable.name == variable_name + && let VariableDefType::OrefDef(oref_cls) = &variable.variable_type { - potential_variable_indices.insert((i, oref_cls)); + potential_variable_indices.insert((i, oref_cls.clone())); } } for (i, oref_cls) in &potential_variable_indices { @@ -150,11 +149,11 @@ impl GlobalSemanticModel { if let Some(curr_var_def) = variable_definition { if variable_def.location.start_byte > curr_var_def.start_byte { variable_definition = Some(variable_def.location); - oref_class = Some(*oref_cls); + oref_class = Some(oref_cls.clone()); } } else { variable_definition = Some(variable_def.location); - oref_class = Some(*oref_cls); + oref_class = Some(oref_cls.clone()); } } } @@ -167,12 +166,12 @@ impl GlobalSemanticModel { .get(&method_ref) .and_then(|scopes| scopes.get(&scope_id)) { + let mut potential_variable_indices = HashSet::new(); for (i, variable) in variables.iter().enumerate() { - if variable.is_oref - && &variable.name == variable_name - && let Some(oref_cls) = &variable.cls + if &variable.name == variable_name + && let VariableDefType::OrefDef(oref_cls) = &variable.variable_type { - potential_variable_indices.insert((i, oref_cls)); + potential_variable_indices.insert((i, oref_cls.clone())); } } for (i, oref_cls) in potential_variable_indices { @@ -691,113 +690,6 @@ impl GlobalSemanticModel { }); } - /// Computes effective class keyword values (procedure block + default language) from inheritance. - /// - /// Fills only missing (`None`) values using the primary parent (leftmost) transitively, with - /// cycle protection via DFS state/memoization. - pub fn class_keyword_inheritance(&mut self, name_to_id: &HashMap) { - #[derive(Clone)] - struct Snap { - declared_pb: Option, - declared_lang: Option, - declared_is_final: Option, - primary_parent: Option, - } - - let class_ids: Vec = self.classes.keys().copied().collect(); - - let id_to_idx: HashMap = class_ids - .iter() - .enumerate() - .map(|(i, &id)| (id, i)) - .collect(); - - let snaps: Vec = class_ids - .iter() - .map(|id| { - let c = &self.classes[id]; - Snap { - declared_pb: c.is_procedure_block, - declared_lang: c.default_language.clone(), - declared_is_final: c.is_final.clone(), - primary_parent: c - .inherited_classes - .get(0) - .and_then(|(name, _)| name_to_id.get(name)) - .copied(), - } - }) - .collect(); - - let n = snaps.len(); - let mut memo: Vec, Option, Option)>> = vec![None; n]; - let mut state: Vec = vec![DfsState::Unvisited; n]; - - fn dfs( - idx: usize, - snaps: &Vec, - id_to_idx: &HashMap, - memo: &mut Vec, Option, Option)>>, - state: &mut Vec, - ) -> (Option, Option, Option) { - if let Some(v) = memo[idx].clone() { - return v; - } - - if state[idx] == DfsState::Visiting { - let s = &snaps[idx]; - return (s.declared_pb, s.declared_lang.clone(), s.declared_is_final); - } - - state[idx] = DfsState::Visiting; - - let s = &snaps[idx]; - - // start with declared values - let mut pb = s.declared_pb; - let mut lang = s.declared_lang.clone(); - let mut is_final = s.declared_is_final; - - // fill missing from primary parent transitively - if pb.is_none() || lang.is_none() { - if let Some(parent_id) = s.primary_parent { - if let Some(&parent_idx) = id_to_idx.get(&parent_id) { - let (ppb, plang, pfinal) = dfs(parent_idx, snaps, id_to_idx, memo, state); - if pb.is_none() { - pb = ppb; - } - if lang.is_none() { - lang = plang; - } - if is_final.is_none() { - is_final = pfinal; - } - } - } - } - - state[idx] = DfsState::Done; - memo[idx] = Some((pb, lang.clone(), is_final)); - (pb, lang, is_final) - } - - // ---- Phase B: apply (only fill None) ---- - for i in 0..n { - let (eff_pb, eff_lang, _eff_final) = dfs(i, &snaps, &id_to_idx, &mut memo, &mut state); - let class_id = class_ids[i]; - let Some(cls) = self.classes.get_mut(&class_id) else { - continue; - }; - - if cls.is_procedure_block.is_none() { - cls.is_procedure_block = eff_pb; - } - if cls.default_language.is_none() { - cls.default_language = eff_lang; - } - } - } - /// Build a reverse inheritance index for each class. /// /// For every class, this returns the transitive set of subclasses that depend on it via diff --git a/objectscript-lsp/crates/objectscript-core/src/method.rs b/objectscript-lsp/crates/objectscript-core/src/method.rs index f08f7e2..069f656 100644 --- a/objectscript-lsp/crates/objectscript-core/src/method.rs +++ b/objectscript-lsp/crates/objectscript-core/src/method.rs @@ -4,6 +4,7 @@ use crate::common::{ }; use crate::parse_structures::{ CodeMode, Language, Method, MethodType, TypeName, UnresolvedMethodRef, Variable, + VariableDefType, }; use crate::scope_structures::ScopeId; @@ -188,10 +189,12 @@ impl Method { scope_tree: &ScopeTree, variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, method_range: Range, + class_name: &str, ) { { let query = routine_set_variables_query(); let mut cursor = QueryCursor::new(); + cursor.set_byte_range(method_range.start_byte..method_range.end_byte); let mut iter = cursor.matches(query, node, content.as_bytes()); while let Some(query_match) = iter.next() { let set_target_node = query_match.captures[0].node; @@ -200,6 +203,7 @@ impl Method { } let mut var_defs = Vec::new(); let mut var_deps = Vec::new(); + let mut var_type = VariableDefType::VariableDef; let var_value = query_match.captures[1].node; let children; if set_target_node.kind() == "set_target_list" { @@ -242,24 +246,37 @@ impl Method { var_defs.push((lvn_id, var_range)); } } + "instance_variable" => { + if let Some(instance_var) = + get_string_at_byte_range(content, set_target_child.byte_range()) + && let Some(property_name_outer) = set_target_child.named_child(0) + && let Some(property_name_node) = property_name_outer.named_child(0) + && let Some(property_name) = get_string_at_byte_range( + content, + property_name_node.byte_range(), + ) + { + var_type = VariableDefType::PropertyDef(( + class_name.to_string(), + property_name, + )); + var_defs.push((instance_var, var_range)); + } + } _ => { - eprintln!( - "Warning: set target case: {:?} not yet implemented, skipping.", - set_target_child.kind() - ); + // Other set targets do not define variables tracked here. } } } - let (is_oref, curr_class) = - find_var_dependencies(var_value, content, &mut var_deps); + if matches!(var_type, VariableDefType::VariableDef) { + let (is_oref, curr_class) = + find_var_dependencies(var_value, content, &mut var_deps); + if is_oref && let Some(curr_class) = curr_class { + var_type = VariableDefType::OrefDef(curr_class); + } + } for (variable_name, var_range) in &var_defs { - let var = Variable::new( - variable_name.clone(), - None, - true, - is_oref, - curr_class.clone(), - ); + let var = Variable::new(variable_name.clone(), None, true, var_type.clone()); if let Some(scope_id) = scope_tree .find_current_scope_for_range(var_range.start_point, var_range.end_point) { @@ -278,6 +295,8 @@ impl Method { variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, class_is_procedure_block: Option, is_class_method: bool, + method_range: Range, + class_name: &str, ) { { let query = if is_class_method { @@ -286,11 +305,19 @@ impl Method { routine_set_variables_query() }; let mut cursor = QueryCursor::new(); + if !is_class_method { + cursor.set_byte_range(method_range.start_byte..method_range.end_byte); + } let mut iter = cursor.matches(query, node, content.as_bytes()); while let Some(query_match) = iter.next() { let mut var_defs = Vec::new(); let mut var_deps = Vec::new(); + let mut var_type = VariableDefType::VariableDef; let set_target_node = query_match.captures[0].node; + if !is_class_method && !range_within_range(&set_target_node.range(), &method_range) + { + continue; + } let var_value = query_match.captures[1].node; let children; if set_target_node.kind() == "set_target_list" { @@ -333,16 +360,35 @@ impl Method { var_defs.push((lvn_id, var_range)); } } + "instance_variable" => { + if let Some(instance_var) = + get_string_at_byte_range(content, set_target_child.byte_range()) + && let Some(property_name_outer) = set_target_child.named_child(0) + && let Some(property_name_node) = property_name_outer.named_child(0) + && let Some(property_name) = get_string_at_byte_range( + content, + property_name_node.byte_range(), + ) + { + var_type = VariableDefType::PropertyDef(( + class_name.to_string(), + property_name, + )); + var_defs.push((instance_var, var_range)); + } + } _ => { - eprintln!( - "Warning: set target case: {:?} not yet implemented, skipping.", - set_target_child.kind() - ); + continue; } } } - let (is_oref, curr_class) = - find_var_dependencies(var_value, content, &mut var_deps); + if matches!(var_type, VariableDefType::VariableDef) { + let (is_oref, curr_class) = + find_var_dependencies(var_value, content, &mut var_deps); + if is_oref && let Some(curr_class) = curr_class { + var_type = VariableDefType::OrefDef(curr_class); + } + } for (variable_name, var_range) in &var_defs { let variable_is_public = if !is_class_method { if self.public_variables_declared.contains(variable_name) { @@ -360,8 +406,7 @@ impl Method { variable_name.clone(), None, variable_is_public, - is_oref, - curr_class.clone(), + var_type.clone(), ); if let Some(scope_id) = scope_tree .find_current_scope_for_range(var_range.start_point, var_range.end_point) @@ -381,13 +426,18 @@ impl Method { scope_tree: &ScopeTree, variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, is_procedure: bool, // false if subroutine + method_range: Range, ) { { let query = routine_argument_query(); let mut cursor = QueryCursor::new(); + cursor.set_byte_range(method_range.start_byte..method_range.end_byte); let mut iter = cursor.matches(query, tag_node, content.as_bytes()); while let Some(query_match) = iter.next() { let method_arg = query_match.captures[0].node; + if !range_within_range(&method_arg.range(), &method_range) { + continue; + } if let Some(method_arg_type) = method_arg.named_child(0) { let Some(variable_name_node) = method_arg_type.named_child(0) else { eprintln!( @@ -407,7 +457,12 @@ impl Method { } else { false }; - let var = Variable::new(var_name, None, variable_is_public, false, None); + let var = Variable::new( + var_name, + None, + variable_is_public, + VariableDefType::VariableDef, + ); if let Some(scope_id) = scope_tree.find_current_scope_for_range( var_range.start_point, var_range.end_point, @@ -510,8 +565,12 @@ impl Method { .unwrap_or(class_is_procedure_block.unwrap_or(true)) == false || self.public_variables_declared.contains(var_name); - let var = - Variable::new(var_name.clone(), arg_type, variable_is_public, false, None); + let var = Variable::new( + var_name.clone(), + arg_type, + variable_is_public, + VariableDefType::VariableDef, + ); if let Some(scope_id) = scope_tree .find_current_scope_for_range(var_range.start_point, var_range.end_point) { @@ -542,6 +601,9 @@ impl Method { routine_method_dependency_query() }; let mut cursor = QueryCursor::new(); + if !is_class_method { + cursor.set_byte_range(method_range.start_byte..method_range.end_byte); + } let mut iter = cursor.matches(query, node, content.as_bytes()); let classmethod_idx = query.capture_index_for_name("classmethodcall"); let systemfunc_idx = query.capture_index_for_name("systemfunc"); @@ -632,12 +694,14 @@ impl Method { method_name_node.byte_range(), ) { - unresolved_method_refs.insert(UnresolvedMethodRef { - class: class_name.to_string(), - method: method_name, - offset: None, - method_call_range: matched_node.range(), - }); + if method_name_node.kind() == "string_literal" { + unresolved_method_refs.insert(UnresolvedMethodRef { + class: class_name.to_string(), + method: method_name, + offset: None, + method_call_range: matched_node.range(), + }); + } } } else { if let Some(classname_method_arg) = matched_node.named_child(0) @@ -660,12 +724,14 @@ impl Method { method_name_node.byte_range(), ) { - unresolved_method_refs.insert(UnresolvedMethodRef { - class: classname_var, - method: method_name, - offset: None, - method_call_range: matched_node.range(), - }); + if method_name_node.kind() == "string_literal" { + unresolved_method_refs.insert(UnresolvedMethodRef { + class: classname_var, + method: method_name, + offset: None, + method_call_range: matched_node.range(), + }); + } } } } else if func_name.eq_ignore_ascii_case("$system") { @@ -778,6 +844,7 @@ impl Method { scope_tree, &mut variables_in_method, false, + method_range, ); self.build_subroutine_set_variables( node, @@ -785,6 +852,7 @@ impl Method { scope_tree, &mut variables_in_method, method_range, + class_name, ); let (unresolved_method_refs, unresolved_oref_method_refs) = self.get_method_dependencies(node, content, false, class_name, method_range); @@ -804,6 +872,7 @@ impl Method { scope_tree, &mut variables_in_method, false, + method_range, ); self.build_subroutine_set_variables( node, @@ -811,6 +880,7 @@ impl Method { scope_tree, &mut variables_in_method, method_range, + class_name, ); let (unresolved_method_refs, unresolved_oref_method_refs) = self.get_method_dependencies(node, content, false, class_name, method_range); @@ -832,6 +902,8 @@ impl Method { &mut variables_in_method, class_is_procedure_block, false, + method_range, + class_name, ); self.build_routine_method_arguments( node, @@ -839,6 +911,7 @@ impl Method { scope_tree, &mut variables_in_method, true, + method_range, ); let (unresolved_method_refs, unresolved_oref_method_refs) = self.get_method_dependencies(node, content, false, class_name, method_range); @@ -867,6 +940,8 @@ impl Method { &mut variables_in_method, class_is_procedure_block, true, + method_range, + class_name, ); let (unresolved_method_refs, unresolved_oref_method_refs) = self.get_method_dependencies(node, content, true, class_name, method_range); diff --git a/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs b/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs index caaee23..7cf536c 100644 --- a/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs +++ b/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs @@ -3,7 +3,10 @@ use std::collections::{HashMap, HashSet}; use std::hash::Hash; use std::hash::Hasher; use tower_lsp::lsp_types::Range as LspRange; -use tree_sitter::Range; +use tree_sitter::{Parser, Range}; +use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; +use tree_sitter_objectscript_routine::LANGUAGE_OBJECTSCRIPT_ROUTINE; +use tree_sitter_xml::LANGUAGE_XML; /// Stores the Index into `GlobalSemanticModel::classes`. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct ClassId(pub usize); @@ -62,6 +65,31 @@ pub enum MemberType { DottedStatementTag, } +pub struct IndexParsers { + pub cls: Parser, + pub routine: Parser, + pub xml: Parser, +} + +impl IndexParsers { + pub fn new() -> Self { + let mut cls = Parser::new(); + cls.set_language(&LANGUAGE_OBJECTSCRIPT_UDL.into()) + .expect("failed to load ObjectScript UDL grammar"); + + let mut routine = Parser::new(); + routine + .set_language(&LANGUAGE_OBJECTSCRIPT_ROUTINE.into()) + .expect("failed to load ObjectScript routine grammar"); + + let mut xml = Parser::new(); + xml.set_language(&LANGUAGE_XML.into()) + .expect("failed to load XML grammar"); + + Self { cls, routine, xml } + } +} + /// DFS visitation state. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum DfsState { @@ -271,10 +299,23 @@ pub struct Variable { pub arg_type: Option, /// Whether variable is public or not. pub is_public: bool, - /// True if variable is an instance of a class, false otherwise. - pub is_oref: bool, - /// None if not an oref. If an oref, String representing class it points to. - pub cls: Option, + /// The Type of Variable Definition. + pub variable_type: VariableDefType, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct OrefChainExpr { + pub class_ref: String, + pub property_ref: Option, + pub parameter_ref: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum VariableDefType { + OrefDef(String), + OrefChainExpr(OrefChainExpr), + VariableDef, + PropertyDef((String, String)), } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/objectscript-lsp/crates/objectscript-core/src/scope_tree.rs b/objectscript-lsp/crates/objectscript-core/src/scope_tree.rs index 4b3f53e..eb330d1 100644 --- a/objectscript-lsp/crates/objectscript-core/src/scope_tree.rs +++ b/objectscript-lsp/crates/objectscript-core/src/scope_tree.rs @@ -1,5 +1,5 @@ use crate::common::{generic_exit_statements, point_in_range}; -use crate::parse_structures::{ClassId, MethodRef, PropertyRef, VariableRef}; +use crate::parse_structures::{MethodRef, PropertyRef, VariableRef}; use crate::scope_structures::*; use std::collections::{HashMap, HashSet}; use tower_lsp::lsp_types::Url; @@ -151,8 +151,6 @@ pub struct ScopeTree { pub private_method_defs: HashMap, /// Stores PropertyRef -> Property Symbol for all private properties in the document. pub private_property_defs: HashMap, - /// The Id corresponding to the class definition symbol for this document when this is a class file. - pub class_def: Option, } impl Clone for ScopeTree { @@ -164,14 +162,13 @@ impl Clone for ScopeTree { next_scope_id: self.next_scope_id, private_method_defs: self.private_method_defs.clone(), private_property_defs: self.private_property_defs.clone(), - class_def: self.class_def, } } } impl ScopeTree { /// Creates a new scope tree with a root scope spanning the entire document. - pub fn new(class_def: Option) -> Self { + pub fn new() -> Self { let root_id = ScopeId(0); let root_scope = Scope::new( Point { row: 0, column: 0 }, @@ -191,7 +188,6 @@ impl ScopeTree { next_scope_id: 1, private_method_defs: HashMap::new(), private_property_defs: HashMap::new(), - class_def, } } @@ -361,19 +357,19 @@ impl ScopeTree { true } - /// Inserts a private variable symbol into the scope containing its start point. - pub fn new_variable_symbol( + /// Inserts a private variable symbol into an already-resolved scope. + pub fn new_variable_symbol_at_scope( &mut self, + scope_id: ScopeId, name: String, range: Range, var_deps: Vec, variable_reference: VariableRef, ) { - let Some(scope) = self.get_mut_scope(range.start_point) else { - eprintln!("Error: couldn't get scope for variable {:?}", name); + let Some(scope) = self.scopes.get_mut(&scope_id) else { return; }; - scope.new_variable_symbol(name, range, var_deps, variable_reference) + scope.new_variable_symbol(name, range, var_deps, variable_reference); } /// Register a private method definition symbol in this document. @@ -416,26 +412,6 @@ impl ScopeTree { self.private_property_defs .insert(property_ref, property_symbol); } - /// Get a mutable reference to the innermost scope containing `point`. - /// - /// Logs a warning and returns `None` if no containing scope is found. - fn get_mut_scope(&mut self, point: Point) -> Option<&mut Scope> { - let Some(scope_id) = self.find_current_scope(point) else { - eprintln!("Warning: Scope Id not found for Point {:?}", point); - return None; - }; - - let scopes = self.scopes.clone(); - let Some(scope) = self.scopes.get_mut(&scope_id) else { - eprintln!( - "Warning: Scope not found, Scope Id {:?} DNE in scopes hashmap: \n {:?} \n\n", - scope_id, scopes - ); - return None; - }; - Some(scope) - } - /// Get an immutable reference to the innermost scope containing `point`. /// /// Logs a warning and returns `None` if no containing scope is found. @@ -482,18 +458,17 @@ impl ScopeTree { children } - /// Record a public variable symbol in the scope that contains `range.start_point`. - pub fn new_public_var_symbol( + /// Records a public variable symbol in an already-resolved scope. + pub fn new_public_var_symbol_at_scope( &mut self, + scope_id: ScopeId, name: String, - range: Range, variable_reference: VariableRef, ) { - let Some(scope) = self.get_mut_scope(range.start_point) else { - generic_exit_statements("Scope", "new_public_var_symbol"); + let Some(scope) = self.scopes.get_mut(&scope_id) else { return; }; - scope.new_symbol_pub_variable(name.clone(), variable_reference); + scope.new_symbol_pub_variable(name, variable_reference); } /// Returns the method name associated with the scope containing the given position. diff --git a/objectscript-lsp/crates/objectscript-core/src/variable.rs b/objectscript-lsp/crates/objectscript-core/src/variable.rs index 00a1066..6f6dd03 100644 --- a/objectscript-lsp/crates/objectscript-core/src/variable.rs +++ b/objectscript-lsp/crates/objectscript-core/src/variable.rs @@ -1,4 +1,4 @@ -use crate::parse_structures::{TypeName, Variable}; +use crate::parse_structures::{TypeName, Variable, VariableDefType}; impl Variable { /// Construct a `Variable` with an optional declared argument type and inferred expression types. @@ -9,15 +9,13 @@ impl Variable { var_name: String, arg_type: Option, is_public: bool, - is_oref: bool, - cls: Option, + variable_def_type: VariableDefType, ) -> Self { Self { name: var_name, arg_type, is_public, - is_oref, - cls, + variable_type: variable_def_type, } } } diff --git a/objectscript-lsp/crates/objectscript-core/src/workspace.rs b/objectscript-lsp/crates/objectscript-core/src/workspace.rs index dd923d0..3b98685 100644 --- a/objectscript-lsp/crates/objectscript-core/src/workspace.rs +++ b/objectscript-lsp/crates/objectscript-core/src/workspace.rs @@ -9,8 +9,8 @@ use crate::global_semantic::GlobalSemanticModel; use crate::local_semantic::LocalSemanticModel; use crate::override_index::OverrideIndex; use crate::parse_structures::{ - Class, ClassId, FileType, MethodRef, MethodType, ParameterRef, PropertyRef, RefactorLevel, - UnresolvedMethodRef, VariableRef, + Class, ClassId, FileType, Method, MethodRef, MethodType, Parameter, ParameterRef, Property, + PropertyRef, RefactorLevel, UnresolvedMethodRef, Variable, VariableDefType, VariableRef, }; use crate::refactor::{ refactor_conditionals_in_document, refactor_for_statements, refactor_legacy_do_statements, @@ -19,6 +19,7 @@ use crate::scope_structures::ScopeId; use crate::scope_tree::ScopeTree; use parking_lot::{Mutex, RwLock}; use petgraph::visit::EdgeRef; +use rayon::prelude::*; use std::collections::{HashMap, HashSet}; use std::fmt::Debug; use std::path::PathBuf; @@ -137,6 +138,452 @@ pub struct ProjectData { pub inheritance_diagonstics: HashMap>, pub method_reference_diagnostics: HashMap<(String, String), HashMap>, pub other_class_diagnostics: HashMap>, + /// Names of classes loaded from the system-class workspace. + pub sys_classes: HashSet, + /// System classes replaced by a class from the user workspace. + pub sys_classes_overwritten: HashSet, +} + +/// A parsed document waiting to be committed as part of a workspace index. +#[derive(Debug)] +pub struct BulkIndexDocument { + pub url: Url, + pub class_range: Range, + pub document: Document, +} + +/// Result of registering a document with a [`BulkWorkspaceIndex`]. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct BulkRegistration { + pub duplicate_document: bool, + pub duplicate_class: bool, +} + +/// Explicit, short-lived workspace indexing transaction. +/// +/// Registration records the complete declaration set without changing live +/// project state. `finalize` commits it in one operation, allowing unresolved +/// inheritance and method references to be repaired after later declarations +/// become visible. Live document updates continue to use `add_document` and +/// `incremental_update_document` directly. +pub struct BulkWorkspaceIndex<'a> { + data: &'a mut ProjectData, + documents: Vec, + urls: HashSet, + class_names: HashSet, +} + +type PendingVariable = (Variable, Range, Vec, ScopeId); + +struct PendingBulkClass { + url: Url, + class_id: ClassId, + class_name: String, + class_range: Range, + document: Document, + class: Class, + inherited_classes: Vec<(String, LspRange)>, + properties: HashMap, + parameters: HashMap, + methods: HashMap, + diagnostics: Vec, +} + +struct PreparedMethod { + method: Method, + range: Range, + method_ref: MethodRef, + variables: Vec, + unresolved: HashSet, + unresolved_orefs: HashSet<(String, String, Range, String)>, +} + +impl<'a> BulkWorkspaceIndex<'a> { + pub fn register(&mut self, mut input: BulkIndexDocument) -> BulkRegistration { + let duplicate_document = + self.data.documents.contains_key(&input.url) || !self.urls.insert(input.url.clone()); + + let duplicate_class = input.document.file_type != FileType::Xml + && (self.data.classes.contains_key(&input.document.class_name) + || !self.class_names.insert(input.document.class_name.clone())); + + if !duplicate_document { + if input.document.file_type != FileType::Xml { + input.document.class_id = Some(ClassId(self.data.global_semantic_model.next_id())); + } + + self.documents.push(input); + } + + BulkRegistration { + duplicate_document, + duplicate_class, + } + } + + pub fn finalize(self) { + let total_started = std::time::Instant::now(); + let data = self.data; + let prepare_started = std::time::Instant::now(); + let (xml_documents, class_documents): (Vec<_>, Vec<_>) = self + .documents + .into_iter() + .partition(|input| input.document.file_type == FileType::Xml); + + // Each worker owns its document and all products of class/method analysis. + // In particular, no worker observes ProjectData, so registration and every + // shared semantic/index mutation remain serial. + let pending: Vec = class_documents + .into_par_iter() + .map(|input| { + let BulkIndexDocument { + url, + class_range, + document, + } = input; + let class_id = document + .class_id + .expect("register must assign an id to every non-XML document"); + let class_name = document.class_name.clone(); + let is_rtn = document.file_type == FileType::Routine; + let starting_node = if is_rtn { + document.tree.root_node() + } else { + find_class_definition(document.tree.root_node()) + .expect("registered class document must contain a class definition") + }; + let mut class = Class::new(class_name.clone(), is_rtn); + let (_, _, methods, properties, parameters, inherited_classes, _, diagnostics) = + class.build_class( + starting_node, + &document.content, + is_rtn, + &class_id, + class_range, + &class_name, + ); + class.build_imports(&document.tree, &document.content); + let class_is_final = class.is_final; + let class_is_procedure_block = class.is_procedure_block; + let methods = methods + .into_iter() + .filter_map( + |(name, (mut method, range, method_ref, public_variables))| { + let method_type = method.method_type; + let node = match method_type { + MethodType::ClassMethod + | MethodType::InstanceMethod + | MethodType::Procedure(_) => { + document.tree.root_node().named_descendant_for_byte_range( + range.start_byte, + range.end_byte, + ) + } + _ => Some(document.tree.root_node()), + }?; + let (_, _, variables, unresolved, unresolved_orefs) = method + .rebuild_method( + node, + &document.content, + &document.scope_tree, + method_type, + range, + public_variables, + class_is_final, + None, + class_is_procedure_block, + &class_name, + ); + Some(( + name, + PreparedMethod { + method, + range, + method_ref, + variables, + unresolved, + unresolved_orefs, + }, + )) + }, + ) + .collect(); + PendingBulkClass { + url, + class_id, + class_name, + class_range, + document, + class, + inherited_classes, + properties, + parameters, + methods, + diagnostics, + } + }) + .collect(); + eprintln!( + "[index] prepared {} classes/routines in {:.3?}", + pending.len(), + prepare_started.elapsed() + ); + + let commit_started = std::time::Instant::now(); + for input in xml_documents { + data.documents.insert(input.url, input.document); + } + let mut pending_calls = Vec::with_capacity(pending.len()); + let mut pending_inheritance = Vec::with_capacity(pending.len()); + for mut prepared in pending { + let class_name = prepared.class_name.clone(); + let url = prepared.url.clone(); + let class_id = prepared.class_id; + if let Some(existing_class_id) = data.classes.get(&class_name).copied() + && let Some(existing_symbol) = data + .global_semantic_model + .get_class_symbol(&existing_class_id) + && existing_symbol.url != url + && let Some(existing_document) = data.documents.get(&existing_symbol.url) + { + let existing_url = existing_symbol.url.clone(); + let existing_content = existing_document.content.clone(); + data.fully_remove_old_class_members(&existing_class_id, &existing_content, false); + data.global_semantic_model.remove_class(&existing_class_id); + data.documents.remove(&existing_url); + if data.sys_classes.contains(&class_name) { + data.sys_classes_overwritten.insert(class_name.clone()); + } + } + data.global_semantic_model + .new_local_semantic(class_id, LocalSemanticModel::new()); + data.classes.insert(class_name.clone(), class_id); + data.global_semantic_model.new_class( + prepared.class, + class_id, + prepared.class_range, + url.clone(), + ); + data.other_class_diagnostics + .insert(url.clone(), prepared.diagnostics); + + for (name, (property, range, property_ref)) in prepared.properties { + if !property.is_public { + prepared.document.scope_tree.new_property_symbol( + name.clone(), + range, + property_ref, + url.clone(), + ); + } + data.global_semantic_model + .new_property(property, property_ref, range, url.clone()); + data.property_defs + .entry(class_name.clone()) + .or_default() + .insert(name, property_ref); + } + for (name, (parameter, range, parameter_ref)) in prepared.parameters { + data.global_semantic_model.new_parameter( + parameter, + parameter_ref, + range, + url.clone(), + ); + data.parameter_defs + .entry(class_name.clone()) + .or_default() + .insert(name, parameter_ref); + } + let mut calls = Vec::new(); + let mut oref_calls = Vec::new(); + for (name, mut method) in prepared.methods { + data.method_defs + .entry(class_name.clone()) + .or_default() + .insert(name.clone(), method.method_ref); + data.dependency_graph.get_or_add_node(method.method_ref); + if !method.method.is_public { + prepared.document.scope_tree.new_method_symbol( + name.clone(), + method.range, + method.method_ref, + url.clone(), + ); + } + for (variable, range, dependencies, scope_id) in method.variables { + let name = variable.name.clone(); + let is_public = variable.is_public; + let variable_ref = data.global_semantic_model.new_variable( + variable, + method.method_ref, + scope_id, + dependencies.clone(), + range, + url.clone(), + ); + method + .method + .variables + .entry(name.clone()) + .or_default() + .push((variable_ref, scope_id)); + if is_public { + prepared.document.scope_tree.new_public_var_symbol_at_scope( + scope_id, + name.clone(), + variable_ref, + ); + data.pub_var_defs + .entry(name) + .or_default() + .entry(method.method_ref) + .or_default() + .entry(scope_id) + .or_default() + .push(variable_ref); + } else { + prepared.document.scope_tree.new_variable_symbol_at_scope( + scope_id, + name, + range, + dependencies, + variable_ref, + ); + } + } + data.global_semantic_model.new_method( + method.method, + method.method_ref, + method.range, + url.clone(), + ); + calls.push((method.method_ref, method.unresolved)); + oref_calls.push((method.method_ref, method.unresolved_orefs)); + } + data.documents.insert(url.clone(), prepared.document); + pending_calls.push((url.clone(), class_id, calls, oref_calls)); + prepared.inherited_classes.shrink_to_fit(); + // Retain only inheritance inputs needed by the serial graph phase. + // This avoids keeping trees/content alive twice. + pending_inheritance.push((url, class_id, class_name, prepared.inherited_classes)); + } + eprintln!( + "[index] committed prepared results in {:.3?}", + commit_started.elapsed() + ); + + // Establish the complete inheritance graph. No override/member + // table is built until the graph is complete. + let inheritance_started = std::time::Instant::now(); + eprintln!( + "[index] building inheritance for {} classes/routines", + pending_inheritance.len() + ); + let affected: HashSet = data.classes.values().copied().collect(); + for (url, class_id, class_name, inherited_classes) in &pending_inheritance { + // Any previously unresolved references to this newly registered class + // are resolved by the complete graph build below. + data.unresolved_inheritance_references.remove(class_name); + data.inheritance_diagonstics.remove(class_name); + + // Preserve diagnostics for parents that are still genuinely absent. + let missing_parents: Vec<(String, LspRange)> = inherited_classes + .iter() + .filter(|(name, _)| !data.classes.contains_key(name)) + .cloned() + .collect(); + let mut unused_affected = HashSet::new(); + data.add_dependent_class_to_inherited_class( + *class_id, + url.clone(), + &missing_parents, + &mut unused_affected, + ); + } + data.dependent_class_index = data.global_semantic_model.build_dependents(&data.classes); + eprintln!( + "[index] built inheritance for {} classes/routines in {:.3?}", + pending_inheritance.len(), + inheritance_started.elapsed() + ); + + // Phase 2B is internally topologically sorted parent-before-child. + let overrides_started = std::time::Instant::now(); + eprintln!( + "[index] building override index for {} affected classes/routines", + affected.len() + ); + data.rebuild_override_index_for_classes_and_apply(&affected); + eprintln!( + "[index] built override index for {} affected classes/routines in {:.3?}", + affected.len(), + overrides_started.elapsed() + ); + + // Build every ordinary caller -> callee edge before resolving oref calls. + // Public oref resolution walks incoming call edges, so interleaving these + // passes would make its result depend on document iteration order. + let ordinary_calls_started = std::time::Instant::now(); + eprintln!("[index] resolving ordinary method calls"); + for (url, class_id, calls, _) in &mut pending_calls { + let Some(document) = data.documents.remove(url) else { + continue; + }; + for (method_ref, unresolved) in std::mem::take(calls) { + data.resolve_method_references( + &document.content, + &unresolved, + method_ref, + *class_id, + ); + } + data.documents.insert(url.clone(), document); + } + eprintln!( + "[index] resolved ordinary method calls in {:.3?}", + ordinary_calls_started.elapsed() + ); + + let oref_calls_started = std::time::Instant::now(); + eprintln!("[index] resolving oref method calls"); + for (url, _, _, oref_calls) in pending_calls { + let Some(document) = data.documents.remove(&url) else { + continue; + }; + for (method_ref, unresolved) in oref_calls { + for (oref_name, method_name, range, current_method_name) in unresolved { + let (resolved, still_unresolved) = data.resolve_oref_methods( + method_ref, + &oref_name, + &method_name, + range, + ¤t_method_name, + &document.scope_tree, + ); + for referenced in resolved { + data.dependency_graph + .add_edge(method_ref, referenced, range); + } + for (key, value) in still_unresolved { + data.unresolved_method_references + .entry(key) + .or_default() + .extend(value); + } + } + } + data.documents.insert(url, document); + } + eprintln!( + "[index] resolved oref method calls in {:.3?}", + oref_calls_started.elapsed() + ); + eprintln!( + "[index] finalized bulk workspace in {:.3?}", + total_started.elapsed() + ); + } } /// Concurrency wrapper for a workspace’s state and parsers. @@ -155,6 +602,24 @@ pub struct ProjectState { } impl ProjectData { + /// Classify every class currently loaded in this project as a SYS class. + /// + /// Call this after appending a SYS source root and before appending the + /// customer workspace. SYS classes keep the same full semantic model as + /// customer classes; this set is only dependency-classification metadata. + pub fn mark_current_classes_as_sys(&mut self) { + self.sys_classes = self.classes.keys().cloned().collect(); + } + + pub fn begin_bulk_index(&mut self) -> BulkWorkspaceIndex<'_> { + BulkWorkspaceIndex { + data: self, + documents: Vec::new(), + urls: HashSet::new(), + class_names: HashSet::new(), + } + } + pub fn clear_diagnostics_for_url(&mut self, url: &Url) { self.other_class_diagnostics.remove(url); @@ -193,40 +658,6 @@ impl ProjectData { )) } - /// Add a document only if it is not already present. - /// Returns true if the document was present, false otherwise. - pub fn add_document_if_absent( - &mut self, - url: Url, - code: String, - tree: &Tree, - filetype: FileType, - class_name: String, - class_range: Range, - version: Option, - ) -> bool { - if self.documents.contains_key(&url) { - eprintln!("Document already exists for file at :{:?}", url.path()); - return true; - } - let class_id = if filetype == FileType::Xml { - None - } else { - Some(ClassId(self.global_semantic_model.next_id())) - }; - self.add_document( - url, - code.as_str(), - tree, - filetype, - class_id, - class_name, - version, - class_range, - ); - false - } - /// Refactor a document. Refactoring options are in `RefactorLevel`, and include /// `Refactor Legacy Dotted Do Statements`, `Refactor Legacy If/Else Statements` /// `Refactor Legacy For Statements`, or do all three actions. @@ -488,7 +919,12 @@ impl ProjectData { }) } - fn fully_remove_old_class_members(&mut self, class_id: &ClassId, content: &str) { + fn fully_remove_old_class_members( + &mut self, + class_id: &ClassId, + content: &str, + rebuild_overrides: bool, + ) { let classes: HashSet = self.classes.values().copied().collect(); let (class_name, inherited_classes) = if let Some(class) = self.global_semantic_model.get_class(&class_id) { @@ -538,6 +974,11 @@ impl ProjectData { .or_insert(HashSet::new()) .extend(method_caller_refs); } + self.dependency_graph.remove_node(stale_method_ref); + self.pub_var_defs.retain(|_, methods| { + methods.remove(&stale_method_ref); + !methods.is_empty() + }); self.global_semantic_model.remove_method(&stale_method_ref); } } @@ -558,7 +999,9 @@ impl ProjectData { class.clear(class_name, false); } - self.rebuild_override_index_for_classes_and_apply(&classes); + if rebuild_overrides { + self.rebuild_override_index_for_classes_and_apply(&classes); + } } pub fn full_update_document( @@ -583,13 +1026,13 @@ impl ProjectData { filetype, "XML".to_string(), None, - ScopeTree::new(None), + ScopeTree::new(), version, ); self.documents.insert(url, document); return; } else if filetype == FileType::Routine || filetype == FileType::Cls { - self.fully_remove_old_class_members(&class_id, content); + self.fully_remove_old_class_members(&class_id, content, true); self.global_semantic_model.remove_class(&class_id); self.documents.remove(&url); self.add_document( @@ -634,7 +1077,7 @@ impl ProjectData { filetype, "XML".to_string(), None, - ScopeTree::new(None), + ScopeTree::new(), version, ); self.documents.insert(url, document); @@ -644,7 +1087,6 @@ impl ProjectData { return; }; if self.documents.contains_key(&url) { - eprintln!("Error: Document already exists"); return; } let is_rtn = if filetype == FileType::Routine { @@ -652,7 +1094,7 @@ impl ProjectData { } else { false }; - let scope_tree = initial_build_scope_tree(&tree, class_id, content, is_rtn); + let scope_tree = initial_build_scope_tree(&tree, content, is_rtn); let mut document = Document::new( content.to_string(), tree.clone(), @@ -697,9 +1139,6 @@ impl ProjectData { .new_class(class, class_id, class_range, url.clone()); self.other_class_diagnostics .insert(url.clone(), class_diagnostics); - // inherits is_procedure_block, is_final, language from leftmost inherited class if applicable - self.rebuild_keyword_inheritance_for_class(&class_id); - // NOTE: this must be checked after the keyword inheritance is completed. let (class_is_final, class_is_procedure_block) = { if let Some(class) = self.global_semantic_model.get_class(&class_id) { (class.is_final, class.is_procedure_block) @@ -833,9 +1272,9 @@ impl ProjectData { .push((variable_ref, variable_scope_id)); if variable_is_public { - document.scope_tree.new_public_var_symbol( + document.scope_tree.new_public_var_symbol_at_scope( + variable_scope_id, variable_name.clone(), - variable_range, variable_ref, ); self.pub_var_defs @@ -847,7 +1286,8 @@ impl ProjectData { .or_insert(Vec::new()) .push(variable_ref); } else { - document.scope_tree.new_variable_symbol( + document.scope_tree.new_variable_symbol_at_scope( + variable_scope_id, variable_name, variable_range, variable_dependencies, @@ -855,7 +1295,6 @@ impl ProjectData { ); } } - self.dependency_graph.get_or_add_node(method_ref); self.method_defs .entry(class_name.clone()) .or_insert_with(HashMap::new) @@ -1731,7 +2170,7 @@ impl ProjectData { document.content = content.to_string(); document.class_name = new_class_name; document.class_id = None; - document.scope_tree = ScopeTree::new(None); + document.scope_tree = ScopeTree::new(); return; } else if file_type == FileType::Routine || file_type == FileType::Cls { let is_rtn = if file_type == FileType::Routine { @@ -1907,7 +2346,7 @@ impl ProjectData { self.classes.insert(new_class_name.clone(), old_class_id); // rebuild scope tree - let mut scope_tree = initial_build_scope_tree(&tree, old_class_id, content, is_rtn); + let mut scope_tree = initial_build_scope_tree(&tree, content, is_rtn); // copy over the old variable defs from the old scope tree into the new rebuilt scope tree let old_class_member_scopes = old_scope_tree.get_root_children_scopes(); for old_scope in old_class_member_scopes { @@ -1916,9 +2355,6 @@ impl ProjectData { scope_tree.private_method_defs = old_scope_tree.private_method_defs; // NOTE: property defs are never copied over because they are fully rebuilt - // rebuild keywords for class (is_procedure, is_final, language) - self.rebuild_keyword_inheritance_for_class(&old_class_id); - // NOTE: this must be checked after the keyword inheritance is completed. let (new_class_is_final, new_class_is_procedure_block) = { if let Some(class) = self.global_semantic_model.get_class(&old_class_id) { (class.is_final, class.is_procedure_block) @@ -2090,9 +2526,9 @@ impl ProjectData { .push((variable_ref, variable_scope_id)); if variable_is_public { - scope_tree.new_public_var_symbol( + scope_tree.new_public_var_symbol_at_scope( + variable_scope_id, variable_name.clone(), - variable_range, variable_ref, ); self.pub_var_defs @@ -2104,7 +2540,8 @@ impl ProjectData { .or_insert(Vec::new()) .push(variable_ref); } else { - scope_tree.new_variable_symbol( + scope_tree.new_variable_symbol_at_scope( + variable_scope_id, variable_name, variable_range, variable_dependencies, @@ -2495,9 +2932,9 @@ impl ProjectData { } if variable_is_public { - scope_tree.new_public_var_symbol( + scope_tree.new_public_var_symbol_at_scope( + variable_scope_id, variable_name.clone(), - variable_range, variable_ref, ); self.pub_var_defs @@ -2509,7 +2946,8 @@ impl ProjectData { .or_insert(Vec::new()) .push(variable_ref); } else { - scope_tree.new_variable_symbol( + scope_tree.new_variable_symbol_at_scope( + variable_scope_id, variable_name, variable_range, variable_dependencies, @@ -2802,12 +3240,12 @@ impl ProjectData { .get(&ancestor_ref) .and_then(|s| s.get(sid)) .and_then(|vars| vars.get(var_id.0)) - && var.is_oref - && let Some(ref oref_cls) = var.cls + && let VariableDefType::OrefDef(oref_cls) = + &var.variable_type { if let Some(referenced_method_ref) = self .method_defs - .get(oref_cls) + .get(oref_cls.as_str()) .and_then(|methods| methods.get(oref_method_name)) { all_possible_oref_methods @@ -2846,67 +3284,33 @@ impl ProjectData { return (all_possible_oref_methods, unresolved_method_references); } - /// Rebuild keyword inheritance (is_procedure_block, default_language, is_final) - /// for a single class by walking up the primary parent chain. - pub fn rebuild_keyword_inheritance_for_class(&mut self, class_id: &ClassId) { - let Some(class) = self.global_semantic_model.get_class(class_id) else { - return; - }; - if class.is_procedure_block.is_some() - && class.default_language.is_some() - && class.is_final.is_some() - { - return; - } - - let mut pb = class.is_procedure_block; - let mut lang = class.default_language.clone(); - let mut is_final = class.is_final; - let mut current_parents = class.inherited_classes.clone(); - let mut visited = HashSet::new(); - - while pb.is_none() || lang.is_none() || is_final.is_none() { - let Some((parent_name, _)) = current_parents.get(0) else { - break; - }; - if !visited.insert(parent_name.clone()) { - break; - } - let Some(&parent_id) = self.classes.get(parent_name) else { - break; - }; - let Some(parent) = self.global_semantic_model.get_class(&parent_id) else { - break; - }; - if pb.is_none() { - pb = parent.is_procedure_block; - } - if lang.is_none() { - lang = parent.default_language.clone(); - } - if is_final.is_none() { - is_final = parent.is_final; - } - current_parents = parent.inherited_classes.clone(); - } - - if let Some(class) = self.global_semantic_model.get_mut_class(class_id) { - if class.is_procedure_block.is_none() { - class.is_procedure_block = pb; - } - if class.default_language.is_none() { - class.default_language = lang; - } - if class.is_final.is_none() { - class.is_final = is_final; - } - } - } - fn rebuild_override_index_for_classes_and_apply( &mut self, affected_classes: &HashSet, ) { + // Reset accessible-definition maps to declarations owned by each class. + // The override rebuild below adds the currently inherited entries back. + // Without this reset, members removed from a replaced/updated parent can + // remain as stale inherited definitions in subclasses. + let own_definitions: Vec<_> = affected_classes + .iter() + .filter_map(|class_id| { + self.global_semantic_model.get_class(class_id).map(|class| { + ( + class.name.clone(), + class.methods.clone(), + class.properties.clone(), + class.parameters.clone(), + ) + }) + }) + .collect(); + for (class_name, methods, properties, parameters) in own_definitions { + self.method_defs.insert(class_name.clone(), methods); + self.property_defs.insert(class_name.clone(), properties); + self.parameter_defs.insert(class_name, parameters); + } + let (extended_methods, extended_properties, extended_parameters) = self.build_override_index_for_classes(affected_classes); @@ -4628,6 +5032,8 @@ impl ProjectState { inheritance_diagonstics: HashMap::new(), method_reference_diagnostics: HashMap::new(), other_class_diagnostics: HashMap::new(), + sys_classes: HashSet::new(), + sys_classes_overwritten: HashSet::new(), }), } } diff --git a/objectscript-lsp/objectscript-tests/gotodef/relative-method-call/hk.cls b/objectscript-lsp/objectscript-tests/gotodef/relative-method-call/hk.cls index 88c853c..d91d493 100644 --- a/objectscript-lsp/objectscript-tests/gotodef/relative-method-call/hk.cls +++ b/objectscript-lsp/objectscript-tests/gotodef/relative-method-call/hk.cls @@ -1,4 +1,5 @@ Class hk { + Relationship manyProp As User.hksubclass [ Cardinality = many, Inverse = oneProp ]; ClassMethod print2() [Private] { w 2 } diff --git a/objectscript-lsp/objectscript-tests/gotodef/relative-method-call/hksubclass.cls b/objectscript-lsp/objectscript-tests/gotodef/relative-method-call/hksubclass.cls index 009c8e4..8682dd4 100644 --- a/objectscript-lsp/objectscript-tests/gotodef/relative-method-call/hksubclass.cls +++ b/objectscript-lsp/objectscript-tests/gotodef/relative-method-call/hksubclass.cls @@ -1,6 +1,8 @@ Class hksubclass Extends (hk, %RegisteredObject) { - Property x; + Relationship oneProp As User.hk [ Cardinality = one, Inverse = manyProp, Required ]; + Property x As %Boolean [InitialExpression = 1]; ClassMethod callInheritedMethod() { d ..print2() + if 1 { d } } } diff --git a/objectscript-lsp/objectscript-tests/dotted-block/test-dotted-block-actual.mac b/objectscript-lsp/objectscript-tests/refactor-actual/test-dotted-block-actual.mac similarity index 100% rename from objectscript-lsp/objectscript-tests/dotted-block/test-dotted-block-actual.mac rename to objectscript-lsp/objectscript-tests/refactor-actual/test-dotted-block-actual.mac diff --git a/objectscript-lsp/objectscript-tests/nested_dots/test-nested-refactor-actual.mac b/objectscript-lsp/objectscript-tests/refactor-actual/test-nested-refactor-actual.mac similarity index 100% rename from objectscript-lsp/objectscript-tests/nested_dots/test-nested-refactor-actual.mac rename to objectscript-lsp/objectscript-tests/refactor-actual/test-nested-refactor-actual.mac diff --git a/objectscript-lsp/objectscript-tests/routines/test-refactor-do-actual.mac b/objectscript-lsp/objectscript-tests/refactor-actual/test-refactor-do-actual.mac similarity index 100% rename from objectscript-lsp/objectscript-tests/routines/test-refactor-do-actual.mac rename to objectscript-lsp/objectscript-tests/refactor-actual/test-refactor-do-actual.mac diff --git a/objectscript-lsp/objectscript-tests/dotted-block/test-dotted-block-expected.mac b/objectscript-lsp/objectscript-tests/refactor-expected/test-dotted-block-expected.mac similarity index 100% rename from objectscript-lsp/objectscript-tests/dotted-block/test-dotted-block-expected.mac rename to objectscript-lsp/objectscript-tests/refactor-expected/test-dotted-block-expected.mac diff --git a/objectscript-lsp/objectscript-tests/nested_dots/test-nested-refactor-expected.mac b/objectscript-lsp/objectscript-tests/refactor-expected/test-nested-refactor-expected.mac similarity index 100% rename from objectscript-lsp/objectscript-tests/nested_dots/test-nested-refactor-expected.mac rename to objectscript-lsp/objectscript-tests/refactor-expected/test-nested-refactor-expected.mac diff --git a/objectscript-lsp/objectscript-tests/routines/test-refactor-do-expected.mac b/objectscript-lsp/objectscript-tests/refactor-expected/test-refactor-do-expected.mac similarity index 100% rename from objectscript-lsp/objectscript-tests/routines/test-refactor-do-expected.mac rename to objectscript-lsp/objectscript-tests/refactor-expected/test-refactor-do-expected.mac diff --git a/objectscript-lsp/src/backend_testing.rs b/objectscript-lsp/src/backend_testing.rs index e8fd820..bdddf9e 100644 --- a/objectscript-lsp/src/backend_testing.rs +++ b/objectscript-lsp/src/backend_testing.rs @@ -1,16 +1,16 @@ -use objectscript_core::common::{get_member_name_and_range_from_root, ts_range_to_lsp_range}; -use objectscript_core::parse_structures::FileType; -use objectscript_core::workspace::ProjectState; +use crate::common::{IndexingIssue, PreparedFile, get_paths, prepare_document}; +use objectscript_core::common::ts_range_to_lsp_range; + +use objectscript_core::parse_structures::IndexParsers; +use objectscript_core::workspace::{BulkIndexDocument, ProjectState}; use parking_lot::RwLock; +use rayon::iter::Either; +use rayon::prelude::*; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Url}; -use tree_sitter::Parser; -use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; -use tree_sitter_objectscript_routine::LANGUAGE_OBJECTSCRIPT_ROUTINE; -use walkdir::WalkDir; - +use tree_sitter::Range; /// Test harness that mirrors the real Backend for integration testing without a live LSP client. #[derive(Debug)] pub(crate) struct BackendTester { @@ -35,6 +35,7 @@ impl BackendTester { self.projects.read().get(uri).cloned() } + #[cfg(test)] fn find_parent_workspace(&self, uri: Url) -> Option { let doc_path: PathBuf = uri.to_file_path().ok()?; @@ -56,12 +57,14 @@ impl BackendTester { } /// Resolve the project that contains the given document URI. + #[cfg(test)] pub(crate) fn get_project_from_document_url(&self, uri: &Url) -> Option> { let project_url = self.find_parent_workspace(uri.clone())?; self.get_project(&project_url) } /// Parse and index all ObjectScript files under the workspace containing `uri`. + #[cfg(test)] pub(crate) async fn index_workspace(&self, uri: &Url) { let Some(project) = self.get_project_from_document_url(&uri) else { return; @@ -71,93 +74,55 @@ impl BackendTester { return; }; let root = root.to_path_buf(); + self.index_root_into_project(project, root).await; + } + + /// Append all supported files under `root` to an existing project's index. + pub(crate) async fn index_workspace_root(&self, project_uri: &Url, root: PathBuf) { + let Some(project) = self.get_project(project_uri) else { + return; + }; + self.index_root_into_project(project, root).await; + } + + async fn index_root_into_project(&self, project: Arc, root: PathBuf) { + let paths = get_paths(&root); // Run indexing on Tokio's blocking thread pool let handle = tokio::task::spawn_blocking(move || { - let mut cls_parser = Parser::new(); - if cls_parser - .set_language(&LANGUAGE_OBJECTSCRIPT_UDL.into()) - .is_err() - { - eprintln!("Failed to load ObjectScript UDL grammar"); - return; + eprintln!("[index] scanning {}", root.display()); + let (prepared, issues): (Vec<(BulkIndexDocument, Range)>, Vec) = paths + .into_par_iter() + .map_init(IndexParsers::new, |parsers, (path, file_type)| { + prepare_document(path, file_type, parsers) + }) + .partition_map(|outcome| match outcome { + PreparedFile::Ready(document, class_name_range) => { + Either::Left((document, class_name_range)) + } + PreparedFile::Failed(issue) => Either::Right(issue), + }); + for issue in issues { + eprintln!("[index] skipped file: {issue}"); } - - let mut routine_parser = Parser::new(); - if routine_parser - .set_language(&LANGUAGE_OBJECTSCRIPT_ROUTINE.into()) - .is_err() + let mut duplicate_class_diagnostics = Vec::new(); + let lock_started = std::time::Instant::now(); + eprintln!("[index] waiting for project write lock"); + let mut data = project.data.write(); + eprintln!( + "[index] acquired project write lock in {:.3?}", + lock_started.elapsed() + ); { - eprintln!("Failed to load ObjectScript routine grammar"); - return; - } - let mut documents_already_existing = Vec::new(); - for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - - let Some(ext) = path.extension().and_then(|s| s.to_str()) else { - continue; - }; - - let (filetype, use_routine) = match ext { - "cls" => (FileType::Cls, false), - "inc" => (FileType::Routine, true), - "rtn" => (FileType::Routine, true), - "mac" => (FileType::Routine, true), - "int" => (FileType::Routine, true), - _ => continue, - }; - - let code = match std::fs::read_to_string(path) { - Ok(s) => s, - Err(_) => continue, - }; - - let url = match Url::from_file_path(path) { - Ok(u) => u, - Err(_) => continue, - }; - - let tree = if use_routine { - match routine_parser.parse(&code, None) { - Some(t) => t, - None => { - eprintln!("Failed to parse file: {:?}", path); - continue; - } - } - } else { - match cls_parser.parse(&code, None) { - Some(t) => t, - None => { - eprintln!("Failed to parse file: {:?}", path); - continue; - } - } - }; - let is_rtn = if filetype == FileType::Routine { - true - } else { - false - }; - - if let Some((member_range, member_name, class_name_def_range)) = - get_member_name_and_range_from_root(code.as_str(), tree.root_node(), is_rtn) - { - let mut data = project.data.write(); - let workspace_contains_class_name = data.classes.contains_key(&member_name); - let already_exists = data.add_document_if_absent( - url.clone(), - code.clone(), - &tree, - filetype, - member_name.clone(), - member_range, - None, + let mut bulk = data.begin_bulk_index(); + for (document, class_name_def_range) in prepared { + let url = document.url.clone(); + let class_name = document.document.class_name.clone(); + let lsp_range = ts_range_to_lsp_range( + document.document.content.as_str(), + class_name_def_range, ); - if already_exists { - documents_already_existing.push(url); - } else if !already_exists && workspace_contains_class_name { - let lsp_range = ts_range_to_lsp_range(code.as_str(), class_name_def_range); + let registration = bulk.register(document); + if !registration.duplicate_document && registration.duplicate_class { let diagnostic = Diagnostic { range: lsp_range, severity: Some(DiagnosticSeverity::ERROR), @@ -166,20 +131,25 @@ impl BackendTester { source: Some("ObjectScript".to_string()), message: format!( "A Class named {:?} already exists in this workspace.", - &member_name + &class_name ), related_information: None, tags: None, data: None, }; - data.other_class_diagnostics - .entry(url.clone()) - .or_insert(Vec::new()) - .push(diagnostic); + duplicate_class_diagnostics.push((url, diagnostic)); } } + bulk.finalize(); + } + for (url, diagnostic) in duplicate_class_diagnostics { + data.other_class_diagnostics + .entry(url) + .or_insert_with(Vec::new) + .push(diagnostic); } }); + // Wait for completion (and handle join errors) if let Err(join_err) = handle.await { eprintln!("index_workspace_scope spawn_blocking failed: {join_err:?}"); diff --git a/objectscript-lsp/src/common.rs b/objectscript-lsp/src/common.rs index 25faac5..e9abae7 100644 --- a/objectscript-lsp/src/common.rs +++ b/objectscript-lsp/src/common.rs @@ -1,5 +1,145 @@ -use objectscript_core::common::get_node_children; -use tree_sitter::Node; +use objectscript_core::common::{ + get_member_name_and_range_from_root, get_node_children, initial_build_scope_tree, +}; +use objectscript_core::document::Document; +use objectscript_core::parse_structures::{FileType, IndexParsers}; +use objectscript_core::scope_tree::ScopeTree; +use objectscript_core::workspace::BulkIndexDocument; +use std::path::{Path, PathBuf}; +use tower_lsp::lsp_types::Url; +use tree_sitter::{Node, Range}; +use walkdir::WalkDir; + +pub enum PreparedFile { + Ready(BulkIndexDocument, Range), + Failed(IndexingIssue), +} + +#[derive(Debug)] +pub enum IndexingIssue { + Read { + path: PathBuf, + kind: std::io::ErrorKind, + }, + InvalidUrl { + path: PathBuf, + }, + ParseFailed { + path: PathBuf, + file_type: FileType, + }, + MissingDeclaration { + path: PathBuf, + url: Url, + }, +} + +impl std::fmt::Display for IndexingIssue { + fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Read { path, kind } => { + write!(formatter, "could not read {}: {kind:?}", path.display()) + } + Self::InvalidUrl { path } => { + write!( + formatter, + "could not convert {} to a file URL", + path.display() + ) + } + Self::ParseFailed { path, file_type } => { + write!( + formatter, + "parser returned no tree for {file_type:?} {}", + path.display() + ) + } + Self::MissingDeclaration { path, url } => { + write!( + formatter, + "could not find a class/routine declaration in {} ({url})", + path.display() + ) + } + } + } +} + +pub fn get_paths(root: &Path) -> Vec<(PathBuf, FileType)> { + WalkDir::new(root) + .into_iter() + .filter_map(Result::ok) + .filter_map(|entry| { + let path = entry.into_path(); + if !path.is_file() { + return None; + } + let file_type = match path.extension().and_then(|ext| ext.to_str()) { + Some("cls") => FileType::Cls, + Some("inc" | "rtn" | "mac" | "int") => FileType::Routine, + Some("xml") => FileType::Xml, + _ => return None, + }; + Some((path, file_type)) + }) + .collect() +} + +pub fn prepare_document( + path: PathBuf, + file_type: FileType, + parsers: &mut IndexParsers, +) -> PreparedFile { + let content = match std::fs::read_to_string(&path) { + Ok(content) => content, + Err(error) => { + return PreparedFile::Failed(IndexingIssue::Read { + path, + kind: error.kind(), + }); + } + }; + let tree = match file_type { + FileType::Routine => parsers.routine.parse(&content, None), + FileType::Cls => parsers.cls.parse(&content, None), + FileType::Xml => parsers.xml.parse(&content, None), + }; + let Some(tree) = tree else { + return PreparedFile::Failed(IndexingIssue::ParseFailed { path, file_type }); + }; + let url = match Url::from_file_path(&path) { + Ok(url) => url, + Err(_) => return PreparedFile::Failed(IndexingIssue::InvalidUrl { path }), + }; + let is_routine = file_type == FileType::Routine; + let declaration = if file_type == FileType::Xml { + Some(( + tree.root_node().range(), + "XML".to_string(), + tree.root_node().range(), + )) + } else { + get_member_name_and_range_from_root(&content, tree.root_node(), is_routine) + }; + let Some((class_range, class_name, class_name_def_range)) = declaration else { + return PreparedFile::Failed(IndexingIssue::MissingDeclaration { path, url }); + }; + let scope_tree = if file_type == FileType::Xml { + ScopeTree::new() + } else { + initial_build_scope_tree(&tree, &content, is_routine) + }; + let document = Document::new(content, tree, file_type, class_name, None, scope_tree, None); + PreparedFile::Ready( + BulkIndexDocument { + url, + class_range, + document, + }, + class_name_def_range, + ) +} + /// Generate a human-readable diagnostic message for a syntax error node. pub fn diagnostic_message(node: Node, error_text: &str) -> Option { if let Some(sibling_node) = node.prev_named_sibling() { diff --git a/objectscript-lsp/src/lsp.rs b/objectscript-lsp/src/lsp.rs index 5f068db..ecb969f 100644 --- a/objectscript-lsp/src/lsp.rs +++ b/objectscript-lsp/src/lsp.rs @@ -14,7 +14,6 @@ use objectscript_core::workspace::{ProjectData, ProjectState}; use serde_json; use serde_json::Value; use std::collections::HashMap; -use std::sync::Arc; use std::sync::atomic::Ordering; use tower_lsp::LanguageServer; use tower_lsp::jsonrpc::Result; @@ -561,10 +560,7 @@ impl LanguageServer for BackendWrapper { if let Ok(Some(folders)) = self.0.client.workspace_folders().await { for workspace in folders { - let backend = Arc::clone(&self.0); - tokio::spawn(async move { - let _ = backend.index_workspace(&workspace.uri).await; - }); + self.0.index_workspace(&workspace.uri).await; } } diff --git a/objectscript-lsp/src/main.rs b/objectscript-lsp/src/main.rs index 49c96bb..9962a34 100644 --- a/objectscript-lsp/src/main.rs +++ b/objectscript-lsp/src/main.rs @@ -1,17 +1,75 @@ use crate::server::BackendWrapper; +use objectscript_core::workspace::ProjectState; +use std::path::PathBuf; use tower_lsp::{LspService, Server}; -#[cfg(test)] +// #[cfg(test)] mod backend_testing; +use crate::backend_testing::BackendTester; +use tower_lsp::lsp_types::Url; mod common; mod lsp; mod server; +use rayon::ThreadPoolBuilder; +use std::str::FromStr; #[cfg(test)] mod test; +async fn setup_backend_and_workspace(project_root: PathBuf) -> (BackendTester, Url) { + let state = ProjectState::new(); + if state + .project_root_path + .set(Some(project_root.clone())) + .is_err() + { + eprintln!("failed to set the root path"); + } + let backend = BackendTester::new(); + let uri = Url::from_file_path(&project_root).unwrap(); + backend.add_project(uri.clone(), state); + let _ = backend.index_workspace_root(&uri, project_root).await; + (backend, uri) +} + #[tokio::main] async fn main() { - let stdin = tokio::io::stdin(); - let stdout = tokio::io::stdout(); - let (service, socket) = LspService::build(|client| BackendWrapper::new(client)).finish(); - Server::new(stdin, stdout, socket).serve(service).await; + let worker_count = std::env::var("OBJECTSCRIPT_INDEX_WORKERS") + .ok() + .and_then(|value| value.parse::().ok()) + .filter(|count| *count > 0) + .unwrap_or_else(|| { + std::thread::available_parallelism() + .map(|count| count.get()) + .unwrap_or(4) + .min(8) + }); + ThreadPoolBuilder::new() + .num_threads(worker_count) + .build_global() + .expect("Rayon pool must be configured once"); + if std::env::args().any(|arg| arg == "--server") { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + let (service, socket) = LspService::build(|client| BackendWrapper::new(client)).finish(); + Server::new(stdin, stdout, socket).serve(service).await; + return; + } + + let iris_current_version_project_root = + PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202502/").unwrap(); + let iris_new_version_project_root = + PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202602/").unwrap(); + + // index both IRIS versions concurrently + let ((old_iris_backend, old_iris_uri), (new_iris_backend, new_iris_uri)) = tokio::join!( + setup_backend_and_workspace(iris_current_version_project_root), + setup_backend_and_workspace(iris_new_version_project_root), + ); + + let _old_iris_workspace = old_iris_backend + .get_project(&old_iris_uri) + .expect("missing project for old iris version"); + + let _new_iris_workspace = new_iris_backend + .get_project(&new_iris_uri) + .expect("missing project for new iris version"); } diff --git a/objectscript-lsp/src/server.rs b/objectscript-lsp/src/server.rs index 29798c9..160f5a4 100644 --- a/objectscript-lsp/src/server.rs +++ b/objectscript-lsp/src/server.rs @@ -1,18 +1,17 @@ -use objectscript_core::common::{get_member_name_and_range_from_root, ts_range_to_lsp_range}; -use objectscript_core::parse_structures::FileType; -use objectscript_core::workspace::ProjectState; +use crate::common::{IndexingIssue, PreparedFile, get_paths, prepare_document}; +use objectscript_core::common::ts_range_to_lsp_range; +use objectscript_core::parse_structures::{FileType, IndexParsers}; +use objectscript_core::workspace::{BulkIndexDocument, ProjectState}; use parking_lot::RwLock; +use rayon::iter::Either; +use rayon::prelude::*; use std::collections::HashMap; use std::path::PathBuf; use std::sync::Arc; use std::sync::atomic::{AtomicBool, Ordering}; use tower_lsp::Client; use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, MessageType, Url}; -use tree_sitter::Parser; -use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; -use tree_sitter_objectscript_routine::LANGUAGE_OBJECTSCRIPT_ROUTINE; -use tree_sitter_xml::LANGUAGE_XML; -use walkdir::WalkDir; +use tree_sitter::Range; /// Arc-wrapped backend providing the LSP language server implementation. pub struct BackendWrapper(pub(crate) Arc); @@ -22,6 +21,7 @@ impl BackendWrapper { Self(Arc::new(Backend::new(client))) } } + pub(crate) struct Backend { /// LSP Client. pub(crate) client: Client, @@ -121,13 +121,16 @@ impl Backend { /// parsed with the appropriate Tree-sitter grammar, and inserted into the project's document /// store if absent. After the scan, inheritance and variable information is built once. pub(crate) async fn index_workspace(&self, uri: &Url) { - let Some(project) = self.get_project_from_document_url(&uri) else { + let Some(project_uri) = self.find_parent_workspace(uri.clone()) else { eprintln!( "Failed to get project from document with url: {:?}", uri.path() ); return; }; + let Some(project) = self.get_project(&project_uri) else { + return; + }; let Some(root) = project.root_path() else { self.client .log_message(MessageType::ERROR, "project root path doesn't exist") @@ -135,128 +138,56 @@ impl Backend { return; }; let root = root.to_path_buf(); + self.index_workspace_root(&project_uri, root).await; + } + + /// Append all supported files under `root` to an existing project's index. + pub(crate) async fn index_workspace_root(&self, project_uri: &Url, root: PathBuf) { + let Some(project) = self.get_project(project_uri) else { + return; + }; + self.index_root_into_project(project, root).await; + } + + async fn index_root_into_project(&self, project: Arc, root: PathBuf) { + let paths = get_paths(&root); // Run indexing on Tokio's blocking thread pool let handle = tokio::task::spawn_blocking(move || { - let mut cls_parser = Parser::new(); - if cls_parser - .set_language(&LANGUAGE_OBJECTSCRIPT_UDL.into()) - .is_err() - { - eprintln!("Error: Failed to load ObjectScript UDL grammar"); - return; + let index_started = std::time::Instant::now(); + eprintln!("[index] scanning {}", root.display()); + let (prepared, issues): (Vec<(BulkIndexDocument, Range)>, Vec) = paths + .into_par_iter() + .map_init(IndexParsers::new, |parsers, (path, file_type)| { + prepare_document(path, file_type, parsers) + }) + .partition_map(|outcome| match outcome { + PreparedFile::Ready(document, class_name_range) => { + Either::Left((document, class_name_range)) + } + PreparedFile::Failed(issue) => Either::Right(issue), + }); + for issue in issues { + eprintln!("[index] skipped file: {issue}"); } - - let mut routine_parser = Parser::new(); - if routine_parser - .set_language(&LANGUAGE_OBJECTSCRIPT_ROUTINE.into()) - .is_err() + let lock_started = std::time::Instant::now(); + eprintln!("[index] waiting for project write lock"); + let mut data = project.data.write(); + eprintln!( + "[index] acquired project write lock in {:.3?}", + lock_started.elapsed() + ); + let mut duplicate_class_diagnostics = Vec::new(); { - eprintln!("Error: Failed to load ObjectScript routine grammar"); - return; - } - - let mut xml_parser = Parser::new(); - if xml_parser.set_language(&LANGUAGE_XML.into()).is_err() { - eprintln!("Error: Failed to load XML grammar"); - return; - } - - let mut documents_already_existing = Vec::new(); - for entry in WalkDir::new(&root).into_iter().filter_map(|e| e.ok()) { - let path = entry.path(); - - let Some(ext) = path.extension().and_then(|s| s.to_str()) else { - continue; - }; - - let (filetype, is_rtn) = match ext { - "cls" => (FileType::Cls, false), - "inc" => (FileType::Routine, true), - "rtn" => (FileType::Routine, true), - "mac" => (FileType::Routine, true), - "int" => (FileType::Routine, true), - "xml" => (FileType::Xml, false), - _ => continue, - }; - - let content = match std::fs::read_to_string(path) { - Ok(s) => s, - Err(_) => { - eprintln!("Error: Failed to read file contents: {}", path.display()); - continue; - } - }; - - let url = match Url::from_file_path(path) { - Ok(u) => u, - Err(_) => { - eprintln!("Error: Failed to convert path to Url: {}", path.display()); - continue; - } - }; - - let tree = match filetype { - FileType::Routine => match routine_parser.parse(&content, None) { - Some(t) => t, - None => { - eprintln!("Failed to parse file for: {:?}", path.display()); - continue; - } - }, - FileType::Cls => match cls_parser.parse(&content, None) { - Some(t) => t, - None => { - eprintln!("Failed to parse file for: {:?}", path.display()); - continue; - } - }, - FileType::Xml => match xml_parser.parse(&content, None) { - Some(t) => t, - None => { - eprintln!("Failed to parse file for: {:?}", path.display()); - continue; - } - }, - }; - - let (class_range, class_name, class_name_def_range) = if filetype == FileType::Xml { - ( - tree.root_node().range(), - "XML".to_string(), - tree.root_node().range(), - ) - } else { - if let Some((class_range, class_name, class_name_def_range)) = - get_member_name_and_range_from_root(&content, tree.root_node(), is_rtn) - { - (class_range, class_name, class_name_def_range) - } else { - eprintln!( - "Error: Failed to get name from root node for file url: {:?}", - url.path() - ); - continue; - } - }; - - // Commit inside the ProjectData lock - { - let mut data = project.data.write(); - let workspace_contains_class_name = data.classes.contains_key(&class_name); - let already_exists = data.add_document_if_absent( - url.clone(), - content.clone(), - &tree, - filetype, - class_name.clone(), - class_range, - None, + let mut bulk = data.begin_bulk_index(); + for (document, class_name_def_range) in prepared { + let url = document.url.clone(); + let class_name = document.document.class_name.clone(); + let lsp_range = ts_range_to_lsp_range( + document.document.content.as_str(), + class_name_def_range, ); - if already_exists { - documents_already_existing.push(url); - } else if !already_exists && workspace_contains_class_name { - let lsp_range = - ts_range_to_lsp_range(content.as_str(), class_name_def_range); + let registration = bulk.register(document); + if !registration.duplicate_document && registration.duplicate_class { let diagnostic = Diagnostic { range: lsp_range, severity: Some(DiagnosticSeverity::ERROR), @@ -271,14 +202,22 @@ impl Backend { tags: None, data: None, }; - data.other_class_diagnostics - .entry(url.clone()) - .or_insert(Vec::new()) - .push(diagnostic); + duplicate_class_diagnostics.push((url, diagnostic)); } } + bulk.finalize(); + } + for (url, diagnostic) in duplicate_class_diagnostics { + data.other_class_diagnostics + .entry(url) + .or_insert_with(Vec::new) + .push(diagnostic); } - eprintln!("INFO: Finished indexing workspace"); + eprintln!( + "[index] finished {} in {:.3?}", + root.display(), + index_started.elapsed() + ); }); // Wait for completion (and handle join errors) if let Err(join_err) = handle.await { diff --git a/objectscript-lsp/src/test.rs b/objectscript-lsp/src/test.rs index 39d4c3d..36ba592 100644 --- a/objectscript-lsp/src/test.rs +++ b/objectscript-lsp/src/test.rs @@ -100,6 +100,28 @@ mod tests { (backend, uri) } + #[tokio::test] + async fn test_append_customer_root_preserves_sys_class_boundary() { + let test_root = env::current_dir().unwrap().join("objectscript-tests"); + let sys_root = test_root.join("diagnostics"); + let customer_root = test_root.join("gotodef").join("relative-method-call"); + let (backend, project_uri) = setup_backend_and_workspace(sys_root).await; + let project = backend + .get_project(&project_uri) + .expect("missing project state"); + + project.data.write().mark_current_classes_as_sys(); + backend + .index_workspace_root(&project_uri, customer_root) + .await; + + let data = project.data.read(); + assert!(data.sys_classes.contains("clean")); + assert!(!data.sys_classes.contains("hk")); + assert!(data.classes.contains_key("hk")); + assert!(data.classes.contains_key("hksubclass")); + } + fn point_for_substring_n(content: &str, needle: &str, occurrence: usize) -> Point { assert!(occurrence > 0, "occurrence must be >= 1"); let mut start = 0usize; @@ -318,8 +340,8 @@ mod tests { } } assert_eq!(superclass_count, 2); - assert_eq!(subclassone_count, 1); - assert_eq!(subclasstwo_count, 1); + assert_eq!(subclassone_count, 0); + assert_eq!(subclasstwo_count, 0); let before_y = before_public_variables .get("y") .expect("missing public variable y"); @@ -395,8 +417,8 @@ mod tests { } } assert_eq!(superclass_count, 2); - assert_eq!(subclassone_count, 1); - assert_eq!(subclasstwo_count, 1); + assert_eq!(subclassone_count, 0); + assert_eq!(subclasstwo_count, 0); let after_y = after_public_variables .get("y") .expect("missing public variable y after update"); @@ -434,7 +456,7 @@ mod tests { } #[tokio::test] - async fn test_class_keyword_inheritance() { + async fn test_class_keywords() { // KEYWORDS: language = objectscript, inheritance = right, Not ProcedureBlock let project_root = env::current_dir() .unwrap() @@ -567,11 +589,13 @@ mod tests { assert!(dependent_names.contains("SubClassOne")); assert!(dependent_names.contains("SubClassTwo")); - // In multiplePubVarDefs, x is not in current scope, so workspace-wide public definitions are returned. + // In multiplePubVarDefs, x is not in current scope, so public definitions + // on reachable methods are returned. ProcedureBlock is not inherited, so + // the subclass definitions remain private. let x_use_point = point_for_substring_n(content, "w x", 2); let x_locations = project_data.get_variable_definition(&document_url, x_use_point, "x".to_string()); - assert_eq!(x_locations.len(), 2); + assert_eq!(x_locations.len(), 1); let paths: HashSet = x_locations .into_iter() .map(|(url, _)| url.path().to_string()) @@ -581,7 +605,7 @@ mod tests { .iter() .any(|p| p.ends_with("testing-variable-building.cls")) ); - assert!(paths.iter().any(|p| p.ends_with("subclass.cls"))); + assert!(!paths.iter().any(|p| p.ends_with("subclass.cls"))); } #[tokio::test] @@ -590,8 +614,16 @@ mod tests { .unwrap() .join("objectscript-tests") .join("nested_dots"); - let actual_result_path = test_route.join("test-nested-refactor-actual.mac"); - let expected_result_path = test_route.join("test-nested-refactor-expected.mac"); + let actual = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("refactor-actual"); + let expected = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("refactor-expected"); + let actual_result_path = actual.join("test-nested-refactor-actual.mac"); + let expected_result_path = expected.join("test-nested-refactor-expected.mac"); let test_mac_path = test_route.join("test-nested-refactor.mac"); let test_mac_url = Url::from_file_path(&test_mac_path).unwrap(); let (backend, uri) = setup_backend_and_workspace(test_route).await; @@ -613,8 +645,16 @@ mod tests { .unwrap() .join("objectscript-tests") .join("dotted-block"); - let actual_result_path = test_route.join("test-dotted-block-actual.mac"); - let expected_result_path = test_route.join("test-dotted-block-expected.mac"); + let actual = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("refactor-actual"); + let expected = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("refactor-expected"); + let actual_result_path = actual.join("test-dotted-block-actual.mac"); + let expected_result_path = expected.join("test-dotted-block-expected.mac"); let _ = std::fs::remove_file(&actual_result_path); let test_mac_path = test_route.join("test-dotted-block.mac"); let test_mac_url = Url::from_file_path(&test_mac_path).unwrap(); @@ -637,8 +677,16 @@ mod tests { .unwrap() .join("objectscript-tests") .join("local"); - let actual_result_path = routines_root.join("test-large-dotted-statements-actual.mac"); - let expected_result_path = routines_root.join("test-large-dotted-statements-expected.mac"); + let actual = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("refactor-actual"); + let expected = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("refactor-expected"); + let actual_result_path = actual.join("test-large-dotted-statements-actual.mac"); + let expected_result_path = expected.join("test-large-dotted-statements-expected.mac"); let _ = std::fs::remove_file(&actual_result_path); let test_mac_path = routines_root.join("test-large-dotted-statements.mac"); let test_mac_url = Url::from_file_path(&test_mac_path).unwrap(); @@ -661,7 +709,16 @@ mod tests { .unwrap() .join("objectscript-tests") .join("routines"); - let actual_result_path = routines_root.join("test-refactor-do-actual.mac"); + let actual = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("refactor-actual"); + let expected = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("refactor-expected"); + let actual_result_path = actual.join("test-refactor-do-actual.mac"); + let expected_result_path = expected.join("test-refactor-do-expected.mac"); let _ = std::fs::remove_file(&actual_result_path); let test_mac_path = routines_root.join("test-refactor-do.mac"); @@ -676,9 +733,7 @@ mod tests { .expect("missing planned refactor for test-refactor-do.mac"); let tree = parse_routine(refactored.as_str()); std::fs::write(&actual_result_path, &refactored).unwrap(); - let contents = - std::fs::read_to_string("objectscript-tests/routines/test-refactor-do-expected.mac") - .unwrap(); + let contents = std::fs::read_to_string(expected_result_path).unwrap(); let expected_tree = parse_routine(contents.as_str()); assert_eq!( tree.root_node().to_sexp(), @@ -2046,7 +2101,11 @@ dottedComment #[tokio::test] async fn test_routine_variable_definition_keeps_distinct_call_paths() { - let project_root = env::current_dir().unwrap().join("routines"); + let project_root = env::current_dir() + .unwrap() + .join("objectscript-tests") + .join("gotodef") + .join("routines"); let (backend, uri) = setup_backend_and_workspace(project_root.clone()).await; let project_state = backend.get_project(&uri).expect("missing project state"); let project_data = project_state.data.read(); @@ -2087,9 +2146,10 @@ dottedComment "x in tagcalls.helper should resolve to the nearest definition on each call path" ); assert!(resolved_lines.contains(&("tag-calls.mac".to_string(), "set x = 1".to_string()))); - assert!( - resolved_lines.contains(&("offset-goto.mac".to_string(), "set x = 72".to_string())) - ); + assert!(resolved_lines.contains(&( + "cross-routine-ref.mac".to_string(), + "set x = 1".to_string() + ))); } // ========================================================================= From 266af29638d126b8f15974e0cca3ca2859c1c675 Mon Sep 17 00:00:00 2001 From: Hannah Date: Sun, 20 Sep 2026 18:47:51 -0400 Subject: [PATCH 2/8] feat: add granular class comparison --- objectscript-lsp/README.md | 2 + .../crates/objectscript-core/src/lib.rs | 1 + .../objectscript-core/src/workspace_diff.rs | 599 ++++++++++++++++++ .../documentation/class-comparison.md | 193 ++++++ 4 files changed, 795 insertions(+) create mode 100644 objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs create mode 100644 objectscript-lsp/documentation/class-comparison.md diff --git a/objectscript-lsp/README.md b/objectscript-lsp/README.md index 06c3cda..cfebb69 100644 --- a/objectscript-lsp/README.md +++ b/objectscript-lsp/README.md @@ -36,6 +36,8 @@ We built this language server to provide editor-independent ObjectScript semanti documented in [documentation/bulk-workspace-indexing.md](documentation/bulk-workspace-indexing.md). - Remaining measured bottlenecks and optimization priorities are documented in [documentation/indexing-optimization-roadmap.md](documentation/indexing-optimization-roadmap.md). +- Granular, ID-independent class and member comparison is documented in + [documentation/class-comparison.md](documentation/class-comparison.md). ## Workspace Layout diff --git a/objectscript-lsp/crates/objectscript-core/src/lib.rs b/objectscript-lsp/crates/objectscript-core/src/lib.rs index f6af95e..26e3e6c 100644 --- a/objectscript-lsp/crates/objectscript-core/src/lib.rs +++ b/objectscript-lsp/crates/objectscript-core/src/lib.rs @@ -15,3 +15,4 @@ pub mod scope_structures; pub mod scope_tree; pub mod variable; pub mod workspace; +pub mod workspace_diff; diff --git a/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs b/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs new file mode 100644 index 0000000..651b32c --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs @@ -0,0 +1,599 @@ +use crate::document::Document; +use crate::parse_structures::{ + CodeMode, Language, Method, MethodType, Parameter, Property, TypeName, VariableDefType, +}; +use crate::workspace::ProjectData; +use rayon::prelude::*; +use std::collections::{BTreeMap, BTreeSet}; + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ValueChange { + pub before: T, + pub after: T, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct MemberChanges { + pub added: Vec, + pub removed: Vec, + pub changed: BTreeMap, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct CollectionChanges { + pub added: Vec, + pub removed: Vec, +} + +impl Default for CollectionChanges { + fn default() -> Self { + Self { + added: Vec::new(), + removed: Vec::new(), + } + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct VariableSnapshot { + pub name: String, + pub argument_type: Option, + pub is_public: bool, + pub variable_type: VariableDefType, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct MethodSnapshot { + pub method_type: MethodType, + pub return_type: Option, + pub is_public: bool, + pub procedure_block: Option, + pub language: Option, + pub code_mode: CodeMode, + pub public_variables_declared: Vec, + pub final_keyword: Option, + pub variables: Vec, + /// Exact declaration/body source for implementation-level comparison. + pub implementation: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct PropertySnapshot { + pub required: bool, + pub is_public: bool, + pub final_keyword: Option, + pub multidimensional: bool, + pub return_type: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ParameterSnapshot { + pub final_keyword: Option, + pub return_type: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ClassSnapshot { + pub name: String, + pub imports: Vec, + /// Parent order is significant for ObjectScript multiple inheritance. + pub inherited_classes: Vec, + pub inheritance_direction: Option, + pub procedure_block: Option, + pub language: Option, + pub final_keyword: Option, + pub methods: BTreeMap, + pub properties: BTreeMap, + pub parameters: BTreeMap, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct MethodDiff { + pub method_type: Option>, + pub return_type: Option>>, + pub is_public: Option>, + pub procedure_block: Option>>, + pub language: Option>>, + pub code_mode: Option>, + pub public_variables_declared: Option>>, + pub final_keyword: Option>>, + pub variables: CollectionChanges, + /// Exact method declaration/body source changed. + pub source_changed: bool, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct PropertyDiff { + pub required: Option>, + pub is_public: Option>, + pub final_keyword: Option>>, + pub multidimensional: Option>, + pub return_type: Option>>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ParameterDiff { + pub final_keyword: Option>>, + pub return_type: Option>>, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct ClassDiff { + pub class_name: String, + pub imports: Option>>, + pub inherited_classes: Option>>, + pub inheritance_direction: Option>>, + pub procedure_block: Option>>, + pub language: Option>>, + pub final_keyword: Option>>, + pub methods: MemberChanges, + pub properties: MemberChanges, + pub parameters: MemberChanges, + /// Source changed, but no difference is represented by the current model. + pub unmodeled_source_change: bool, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub enum ClassComparison { + Unchanged { + class_name: String, + }, + Added { + class_name: String, + }, + Removed { + class_name: String, + }, + SnapshotUnavailable { + class_name: String, + baseline_available: bool, + target_available: bool, + }, + Changed(ClassDiff), +} + +pub fn compare_classes_parallel( + class_names: &[String], + baseline: &ProjectData, + target: &ProjectData, +) -> Vec { + class_names + .par_iter() + .map(|class_name| compare_class(baseline, target, class_name)) + .collect() +} + +pub fn compare_class( + baseline: &ProjectData, + target: &ProjectData, + class_name: &str, +) -> ClassComparison { + let baseline_document = class_document(baseline, class_name); + let target_document = class_document(target, class_name); + match (baseline_document, target_document) { + (None, None) => ClassComparison::Unchanged { + class_name: class_name.to_string(), + }, + (None, Some(_)) => ClassComparison::Added { + class_name: class_name.to_string(), + }, + (Some(_), None) => ClassComparison::Removed { + class_name: class_name.to_string(), + }, + (Some(before), Some(after)) if before.content == after.content => { + ClassComparison::Unchanged { + class_name: class_name.to_string(), + } + } + (Some(_), Some(_)) => { + let before = snapshot_class(baseline, class_name); + let after = snapshot_class(target, class_name); + let baseline_available = before.is_some(); + let target_available = after.is_some(); + let (Some(before), Some(after)) = (before, after) else { + return ClassComparison::SnapshotUnavailable { + class_name: class_name.to_string(), + baseline_available, + target_available, + }; + }; + ClassComparison::Changed(diff_class(before, after)) + } + } +} + +pub fn snapshot_class(data: &ProjectData, class_name: &str) -> Option { + let class_id = *data.classes.get(class_name)?; + let class = data.global_semantic_model.get_class(&class_id)?; + let document = class_document(data, class_name)?; + + let mut imports = class.imports.clone(); + imports.sort(); + let inherited_classes = class + .inherited_classes + .iter() + .map(|(name, _)| name.clone()) + .collect(); + + let methods = class + .methods + .iter() + .filter_map(|(name, method_ref)| { + let method = data.global_semantic_model.get_method(method_ref)?; + let implementation = data + .global_semantic_model + .get_method_symbol(method_ref) + .or_else(|| document.scope_tree.private_method_defs.get(method_ref)) + .and_then(|symbol| { + document + .content + .get(symbol.location.start_byte..symbol.location.end_byte) + }) + .map(str::to_owned); + Some(( + name.clone(), + snapshot_method(data, method_ref, method, implementation), + )) + }) + .collect(); + + let local = data.global_semantic_model.get_local_semantic(&class_id); + let properties = class + .properties + .iter() + .filter_map(|(name, property_ref)| { + data.global_semantic_model + .get_property(property_ref) + .or_else(|| local.and_then(|model| model.get_property(property_ref))) + .map(|property| (name.clone(), snapshot_property(property))) + }) + .collect(); + let parameters = class + .parameters + .iter() + .filter_map(|(name, parameter_ref)| { + data.global_semantic_model + .get_parameter(parameter_ref) + .map(|parameter| (name.clone(), snapshot_parameter(parameter))) + }) + .collect(); + + Some(ClassSnapshot { + name: class.name.clone(), + imports, + inherited_classes, + inheritance_direction: class.inheritance_direction.clone(), + procedure_block: class.is_procedure_block, + language: class.default_language.clone(), + final_keyword: class.is_final, + methods, + properties, + parameters, + }) +} + +fn class_document<'a>(data: &'a ProjectData, class_name: &str) -> Option<&'a Document> { + let class_id = data.classes.get(class_name)?; + let symbol = data.global_semantic_model.get_class_symbol(class_id)?; + data.documents.get(&symbol.url) +} + +fn snapshot_method( + data: &ProjectData, + method_ref: &crate::parse_structures::MethodRef, + method: &Method, + implementation: Option, +) -> MethodSnapshot { + let local = data + .global_semantic_model + .get_local_semantic(&method_ref.class); + let mut variables = Vec::new(); + for refs in method.variables.values() { + for (variable_ref, scope_id) in refs { + let variable = if let Some(public_id) = variable_ref.pub_id { + data.global_semantic_model + .get_variable(method_ref, public_id.0, scope_id) + } else if let Some(private_id) = variable_ref.priv_id { + local.and_then(|model| model.get_variable(method_ref, private_id.0, scope_id)) + } else { + None + }; + if let Some(variable) = variable { + variables.push(VariableSnapshot { + name: variable.name.clone(), + argument_type: variable.arg_type.clone(), + is_public: variable.is_public, + variable_type: variable.variable_type.clone(), + }); + } + } + } + let mut public_variables_declared: Vec<_> = + method.public_variables_declared.iter().cloned().collect(); + public_variables_declared.sort(); + MethodSnapshot { + method_type: method.method_type, + return_type: method.return_type.clone(), + is_public: method.is_public, + procedure_block: method.is_procedure_block, + language: method.language.clone(), + code_mode: method.code_mode.clone(), + public_variables_declared, + final_keyword: method.is_final, + variables, + implementation, + } +} + +fn snapshot_property(property: &Property) -> PropertySnapshot { + PropertySnapshot { + required: property.required, + is_public: property.is_public, + final_keyword: property.is_final, + multidimensional: property.multidimensional, + return_type: property.return_type.clone(), + } +} + +fn snapshot_parameter(parameter: &Parameter) -> ParameterSnapshot { + ParameterSnapshot { + final_keyword: parameter.is_final, + return_type: parameter.return_type.clone(), + } +} + +fn diff_class(before: ClassSnapshot, after: ClassSnapshot) -> ClassDiff { + let methods = diff_members(&before.methods, &after.methods, diff_method); + let properties = diff_members(&before.properties, &after.properties, diff_property); + let parameters = diff_members(&before.parameters, &after.parameters, diff_parameter); + let mut result = ClassDiff { + class_name: before.name.clone(), + imports: value_change(&before.imports, &after.imports), + inherited_classes: value_change(&before.inherited_classes, &after.inherited_classes), + inheritance_direction: value_change( + &before.inheritance_direction, + &after.inheritance_direction, + ), + procedure_block: value_change(&before.procedure_block, &after.procedure_block), + language: value_change(&before.language, &after.language), + final_keyword: value_change(&before.final_keyword, &after.final_keyword), + methods, + properties, + parameters, + unmodeled_source_change: false, + }; + result.unmodeled_source_change = !result.has_semantic_changes(); + result +} + +impl ClassDiff { + pub fn has_semantic_changes(&self) -> bool { + self.imports.is_some() + || self.inherited_classes.is_some() + || self.inheritance_direction.is_some() + || self.procedure_block.is_some() + || self.language.is_some() + || self.final_keyword.is_some() + || has_member_changes(&self.methods) + || has_member_changes(&self.properties) + || has_member_changes(&self.parameters) + } +} + +fn diff_method(before: &MethodSnapshot, after: &MethodSnapshot) -> Option { + let variables = multiset_changes(&before.variables, &after.variables); + let result = MethodDiff { + method_type: value_change(&before.method_type, &after.method_type), + return_type: value_change(&before.return_type, &after.return_type), + is_public: value_change(&before.is_public, &after.is_public), + procedure_block: value_change(&before.procedure_block, &after.procedure_block), + language: value_change(&before.language, &after.language), + code_mode: value_change(&before.code_mode, &after.code_mode), + public_variables_declared: value_change( + &before.public_variables_declared, + &after.public_variables_declared, + ), + final_keyword: value_change(&before.final_keyword, &after.final_keyword), + variables, + source_changed: before.implementation != after.implementation, + }; + (result.method_type.is_some() + || result.return_type.is_some() + || result.is_public.is_some() + || result.procedure_block.is_some() + || result.language.is_some() + || result.code_mode.is_some() + || result.public_variables_declared.is_some() + || result.final_keyword.is_some() + || !result.variables.added.is_empty() + || !result.variables.removed.is_empty() + || result.source_changed) + .then_some(result) +} + +fn diff_property(before: &PropertySnapshot, after: &PropertySnapshot) -> Option { + let result = PropertyDiff { + required: value_change(&before.required, &after.required), + is_public: value_change(&before.is_public, &after.is_public), + final_keyword: value_change(&before.final_keyword, &after.final_keyword), + multidimensional: value_change(&before.multidimensional, &after.multidimensional), + return_type: value_change(&before.return_type, &after.return_type), + }; + (result.required.is_some() + || result.is_public.is_some() + || result.final_keyword.is_some() + || result.multidimensional.is_some() + || result.return_type.is_some()) + .then_some(result) +} + +fn diff_parameter(before: &ParameterSnapshot, after: &ParameterSnapshot) -> Option { + let result = ParameterDiff { + final_keyword: value_change(&before.final_keyword, &after.final_keyword), + return_type: value_change(&before.return_type, &after.return_type), + }; + (result.final_keyword.is_some() || result.return_type.is_some()).then_some(result) +} + +fn diff_members( + before: &BTreeMap, + after: &BTreeMap, + compare: impl Fn(&T, &T) -> Option, +) -> MemberChanges { + let before_names: BTreeSet<_> = before.keys().cloned().collect(); + let after_names: BTreeSet<_> = after.keys().cloned().collect(); + let added = after_names.difference(&before_names).cloned().collect(); + let removed = before_names.difference(&after_names).cloned().collect(); + let changed = before_names + .intersection(&after_names) + .filter_map(|name| compare(&before[name], &after[name]).map(|diff| (name.clone(), diff))) + .collect(); + MemberChanges { + added, + removed, + changed, + } +} + +fn value_change(before: &T, after: &T) -> Option> { + (before != after).then(|| ValueChange { + before: before.clone(), + after: after.clone(), + }) +} + +fn multiset_changes(before: &[T], after: &[T]) -> CollectionChanges { + let mut matched_after = vec![false; after.len()]; + let mut removed = Vec::new(); + for value in before { + if let Some((index, _)) = after + .iter() + .enumerate() + .find(|(index, candidate)| !matched_after[*index] && *candidate == value) + { + matched_after[index] = true; + } else { + removed.push(value.clone()); + } + } + let added = after + .iter() + .zip(matched_after) + .filter_map(|(value, matched)| (!matched).then(|| value.clone())) + .collect(); + CollectionChanges { added, removed } +} + +fn has_member_changes(changes: &MemberChanges) -> bool { + !changes.added.is_empty() || !changes.removed.is_empty() || !changes.changed.is_empty() +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::parse_structures::FileType; + use crate::workspace::ProjectState; + use tower_lsp::lsp_types::Url; + + const BEFORE: &str = r#" +Class Demo.Compare [ Not ProcedureBlock ] +{ +Property Status As %String; + +ClassMethod Calculate() As %Integer +{ + set oldVar = 1 + quit oldVar +} +} +"#; + + const AFTER: &str = r#" +Class Demo.Compare [ Final, Not ProcedureBlock ] +{ +Property Status As %Integer [ Required ]; +Property Added As %String; + +ClassMethod Calculate() As %String +{ + set newVar = 1 + quit newVar +} +} +"#; + + fn project(content: &str) -> ProjectState { + let project = ProjectState::new(); + project.handle_document_opened( + Url::parse("file:///workspace/Demo.Compare.cls").unwrap(), + content.to_string(), + FileType::Cls, + 1, + ); + project + } + + #[test] + fn identical_source_short_circuits_to_unchanged() { + let baseline = project(BEFORE); + let target = project(BEFORE); + let baseline = baseline.data.read(); + let target = target.data.read(); + assert_eq!( + compare_class(&baseline, &target, "Demo.Compare"), + ClassComparison::Unchanged { + class_name: "Demo.Compare".to_string() + } + ); + } + + #[test] + fn reports_granular_class_member_and_variable_changes() { + let baseline = project(BEFORE); + let target = project(AFTER); + let baseline = baseline.data.read(); + let target = target.data.read(); + let ClassComparison::Changed(diff) = compare_class(&baseline, &target, "Demo.Compare") + else { + panic!("expected a changed class"); + }; + + assert_eq!( + diff.final_keyword, + Some(ValueChange { + before: None, + after: Some(true), + }) + ); + assert_eq!(diff.properties.added, vec!["Added"]); + let status = diff + .properties + .changed + .get("Status") + .expect("Status property should change"); + assert!(status.return_type.is_some()); + + let method = diff + .methods + .changed + .get("Calculate") + .expect("Calculate method should change"); + assert!(method.return_type.is_some()); + assert!(method.source_changed); + assert!( + method + .variables + .removed + .iter() + .any(|variable| variable.name == "oldVar") + ); + assert!( + method + .variables + .added + .iter() + .any(|variable| variable.name == "newVar") + ); + assert!(!diff.unmodeled_source_change); + } +} diff --git a/objectscript-lsp/documentation/class-comparison.md b/objectscript-lsp/documentation/class-comparison.md new file mode 100644 index 0000000..0bd1b73 --- /dev/null +++ b/objectscript-lsp/documentation/class-comparison.md @@ -0,0 +1,193 @@ +# Granular Class Comparison + +`objectscript_core::workspace_diff` compares classes from independently indexed +workspaces without comparing workspace-local IDs. + +This is intended for the IRIS upgrade workflow: + +1. Index SYS version 1 and the customer workspace together. +2. Compute the transitive SYS dependency-name set from version 1. +3. Index SYS version 2 independently. +4. Compare only those dependent class names between the two projects. + +## API + +```rust +use objectscript_core::workspace_diff::{ + ClassComparison, + compare_class, + compare_classes_parallel, +}; +``` + +Compare one class: + +```rust +let comparison = compare_class( + &baseline_project_data, + &target_project_data, + "%Library.Example", +); +``` + +Compare a dependency-name set in parallel: + +```rust +let comparisons = compare_classes_parallel( + &dependent_sys_class_names, + &baseline_project_data, + &target_project_data, +); +``` + +Both projects are read-only during comparison. The outer operation parallelizes +by class; member comparison inside one class remains serial to avoid nested +Rayon work. + +## Fast path + +The comparator first looks up each class's defining `Document`. + +- Equal source strings return `ClassComparison::Unchanged` immediately. +- A missing target class returns `Removed`. +- A missing baseline class returns `Added`. +- Different source strings trigger semantic snapshot construction and granular + comparison. + +This avoids allocating snapshots for unchanged SYS classes. + +## ID-independent snapshots + +`ClassId`, `MethodRef`, `PropertyRef`, `ParameterRef`, `VariableRef`, and +`ScopeId` are local to one workspace and are never compared directly. + +The comparator dereferences them and builds value snapshots keyed by class and +member names. + +### Class fields + +- direct imports, normalized by sorting; +- direct inherited class names, preserving parent order; +- inheritance direction; +- `ProcedureBlock`; +- `Language`; +- `Final`; +- own methods; +- own properties; +- own parameters. + +Inherited effective-member maps are not compared as if they were declarations. +They can be derived after direct class changes are known. + +### Method fields + +- method kind; +- return type; +- visibility; +- `ProcedureBlock`; +- `Language`; +- `CodeMode`; +- `Final`; +- sorted `PublicList` names; +- variables; +- exact method declaration/body source. + +The current method symbol range includes the method source represented by the +semantic model. `MethodDiff::source_changed` is deliberately named “source,” +not “body,” because a signature/keyword edit can also change this range. + +### Variable fields + +- name; +- argument type, when represented; +- public/private status; +- inferred `VariableDefType`. + +Variables are compared as multisets. Workspace-local variable and scope IDs are +ignored, and repeated equivalent definitions retain occurrence counts. + +### Property fields + +- return type; +- required; +- visibility; +- final; +- multidimensional. + +### Parameter fields + +- return type; +- final. + +## Result model + +```rust +pub enum ClassComparison { + Unchanged { class_name: String }, + Added { class_name: String }, + Removed { class_name: String }, + SnapshotUnavailable { + class_name: String, + baseline_available: bool, + target_available: bool, + }, + Changed(ClassDiff), +} +``` + +`ClassDiff` reports changed class fields and added, removed, or changed methods, +properties, and parameters. Each changed member contains field-level +`ValueChange { before, after }` values. + +`unmodeled_source_change` is true when class source differs but none of the +currently modeled semantic fields differ. This preserves visibility into +unsupported UDL constructs, comments, or formatting changes. + +## Performance characteristics + +- Source equality is $O(n)$ in class source bytes and allocates nothing. +- Snapshots are built only for changed classes. +- Class comparisons run through Rayon. +- Maps in snapshots use `BTreeMap` for readable stable reports. +- Variable multiset comparison is quadratic in the number of definitions in one + method. Methods normally contain few definitions; replace it with counted + hash keys only if profiling identifies it as significant. +- Exact method source is cloned only for changed classes while their snapshots + are compared. + +The expensive operation remains workspace indexing. Comparison should be run +after both workspace indexes are complete. + +## Current limitations + +The report can only compare facts retained by the semantic model. It does not +yet fully model: + +- ordered method argument signatures and every argument modifier/default; +- relationships; +- indexes; +- queries; +- triggers; +- XData; +- storage definitions; +- include and macro dependencies. + +Exact class and method source-change reporting prevents these differences from +being silently treated as unchanged. Add typed snapshots as these constructs +enter the semantic model. + +## Dependency closure is separate + +The comparison module accepts class names; it does not decide which SYS classes +the customer depends on. + +The dependency-closure implementation should: + +1. start from all customer-owned methods in the baseline workspace; +2. traverse outgoing ordinary and oref call edges transitively; +3. add reached SYS class names; +4. recursively add direct superclass names; +5. pass the resulting names to `compare_classes_parallel`. + +Use class names as cross-workspace identity. Never pass baseline IDs into the +target workspace. From 24c63ff4273b8f4566c59a7548c411eeba374067 Mon Sep 17 00:00:00 2001 From: Hannah Date: Mon, 21 Sep 2026 15:53:45 -0400 Subject: [PATCH 3/8] test classdiff --- .../crates/objectscript-core/src/class.rs | 64 +++++ .../objectscript-core/src/global_semantic.rs | 4 +- .../objectscript-core/src/parse_structures.rs | 96 +++++++ .../crates/objectscript-core/src/workspace.rs | 246 +++++++++++++++--- .../objectscript-core/src/workspace_diff.rs | 207 +++++++++++++-- .../documentation/class-comparison.md | 136 +++++++++- .../objectscript-tests/customer/customer1.cls | 6 + .../new_scope_res_final/scope-resolution.cls | 33 +++ .../new_scope_res_final/subclass.cls | 7 + .../new_scope_res_final/subclass_two.cls | 6 + .../testing-variable-building.cls | 23 ++ .../old/scope-resolution.cls | 33 +++ .../objectscript-tests/old/subclass.cls | 7 + .../objectscript-tests/old/subclass_two.cls | 6 + .../old/testing-variable-building.cls | 23 ++ objectscript-lsp/src/main.rs | 159 ++++++++++- objectscript-lsp/src/test.rs | 68 +++++ 17 files changed, 1058 insertions(+), 66 deletions(-) create mode 100644 objectscript-lsp/objectscript-tests/customer/customer1.cls create mode 100644 objectscript-lsp/objectscript-tests/new_scope_res_final/scope-resolution.cls create mode 100644 objectscript-lsp/objectscript-tests/new_scope_res_final/subclass.cls create mode 100644 objectscript-lsp/objectscript-tests/new_scope_res_final/subclass_two.cls create mode 100644 objectscript-lsp/objectscript-tests/new_scope_res_final/testing-variable-building.cls create mode 100644 objectscript-lsp/objectscript-tests/old/scope-resolution.cls create mode 100644 objectscript-lsp/objectscript-tests/old/subclass.cls create mode 100644 objectscript-lsp/objectscript-tests/old/subclass_two.cls create mode 100644 objectscript-lsp/objectscript-tests/old/testing-variable-building.cls diff --git a/objectscript-lsp/crates/objectscript-core/src/class.rs b/objectscript-lsp/crates/objectscript-core/src/class.rs index e2be388..0005198 100644 --- a/objectscript-lsp/crates/objectscript-core/src/class.rs +++ b/objectscript-lsp/crates/objectscript-core/src/class.rs @@ -104,6 +104,14 @@ impl Class { next_method_id: 0, next_parameter_id: 0, next_property_id: 0, + next_foreign_key_id: 0, + next_index_id: 0, + next_projection_id: 0, + next_query_id: 0, + next_relationship_id: 0, + next_storage_id: 0, + next_trigger_id: 0, + next_xdata_id: 0, is_final: None, } } @@ -152,6 +160,62 @@ impl Class { id } + /// Allocates and returns the next sequential relationship ID for this class. + pub fn get_next_relationship_id(&mut self) -> usize { + let id = self.next_relationship_id; + self.next_relationship_id += 1; + id + } + + /// Allocates and returns the next sequential foreignkey ID for this class. + pub fn get_next_foreignkey_id(&mut self) -> usize { + let id = self.next_foreign_key_id; + self.next_foreign_key_id += 1; + id + } + + /// Allocates and returns the next sequential query ID for this class. + pub fn get_next_query_id(&mut self) -> usize { + let id = self.next_query_id; + self.next_query_id += 1; + id + } + + /// Allocates and returns the next sequential index ID for this class. + pub fn get_next_index_id(&mut self) -> usize { + let id = self.next_index_id; + self.next_index_id += 1; + id + } + + /// Allocates and returns the next sequential trigger ID for this class. + pub fn get_next_trigger_id(&mut self) -> usize { + let id = self.next_trigger_id; + self.next_trigger_id += 1; + id + } + + /// Allocates and returns the next sequential xdata ID for this class. + pub fn get_next_xdata_id(&mut self) -> usize { + let id = self.next_xdata_id; + self.next_xdata_id += 1; + id + } + + /// Allocates and returns the next sequential projection ID for this class. + pub fn get_next_projection_id(&mut self) -> usize { + let id = self.next_projection_id; + self.next_projection_id += 1; + id + } + + /// Allocates and returns the next sequential storage ID for this class. + pub fn get_next_storage_id(&mut self) -> usize { + let id = self.next_storage_id; + self.next_storage_id += 1; + id + } + /// Allocates and returns the next sequential property ID for this class. pub fn get_next_property_id(&mut self) -> usize { let id = self.next_property_id; diff --git a/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs b/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs index d38b722..5cb1805 100644 --- a/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs +++ b/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs @@ -716,7 +716,9 @@ impl GlobalSemanticModel { for (parent_name, parent_range) in &cls.inherited_classes { if let Some(&parent_id) = name_to_id.get(parent_name) { if let Some(&parent_idx) = id_to_idx.get(&parent_id) { - if entries[parent_idx].1.active { + if entries[parent_idx].1.active + && entries[parent_idx].1.is_final != Some(true) + { children[parent_idx].push((*child_id, parent_range.clone())); } } diff --git a/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs b/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs index 7cf536c..2d33a3a 100644 --- a/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs +++ b/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs @@ -31,6 +31,38 @@ pub struct PropertyId(pub usize); #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct ParameterId(pub usize); +/// Stores the Relationship Index, which is assigned by `class.get_next_relationship_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct RelationshipId(pub usize); + +/// Stores the ForeignKey Index, which is assigned by `class.get_next_foreign_key_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct ForeignKeyId(pub usize); + +/// Stores the Query Index, which is assigned by `class.get_next_query_key_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct QueryId(pub usize); + +/// Stores the Index Index, which is assigned by `class.get_next_index_key_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct IndexId(pub usize); + +/// Stores the ForeignKey Index, which is assigned by `class.get_next_trigger_key_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct TriggerId(pub usize); + +/// Stores the Xdata Index, which is assigned by `class.get_next_xdata_key_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct XdataId(pub usize); + +/// Stores the Projection Index, which is assigned by `class.get_next_projection_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct ProjectionId(pub usize); + +/// Stores the storage Index, which is assigned by `class.get_next_storage_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct StorageId(pub usize); + /// Differentiates the kind of class member an identifier node represents. #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum MemberType { @@ -129,6 +161,62 @@ pub struct PropertyRef { pub id: PropertyId, } +/// Reference to a relationship in a class. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct RelationshipRef { + pub class: ClassId, + pub id: RelationshipId, +} + +/// Reference to a ForeignKey in a class. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ForeignKeyRef { + pub class: ClassId, + pub id: ForeignKeyId, +} + +/// Reference to a Index in a class. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct IndexRef { + pub class: ClassId, + pub id: IndexId, +} + +/// Reference to a trigger in a class. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct TriggerRef { + pub class: ClassId, + pub id: TriggerId, +} + +/// Reference to a xdata in a class. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct XdataRef { + pub class: ClassId, + pub id: XdataId, +} + +/// Reference to a query in a class. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct QueryRef { + pub class: ClassId, + pub id: QueryId, +} + +/// Reference to a projection in a class. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ProjectionRef { + pub class: ClassId, + pub id: ProjectionId, +} + +/// Reference to a storage in a class. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct StorageRef { + pub class: ClassId, + pub id: StorageId, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct Parameter { /// If true, the Parameter cannot be overwritten by subclasses. @@ -207,6 +295,14 @@ pub struct Class { pub(crate) next_method_id: usize, pub(crate) next_parameter_id: usize, pub(crate) next_property_id: usize, + pub(crate) next_relationship_id: usize, + pub(crate) next_index_id: usize, + pub(crate) next_foreign_key_id: usize, + pub(crate) next_query_id: usize, + pub(crate) next_trigger_id: usize, + pub(crate) next_xdata_id: usize, + pub(crate) next_projection_id: usize, + pub(crate) next_storage_id: usize, /// If true, this class and all of its members cannot be overwritten by subclasses. pub is_final: Option, } diff --git a/objectscript-lsp/crates/objectscript-core/src/workspace.rs b/objectscript-lsp/crates/objectscript-core/src/workspace.rs index 3b98685..612b700 100644 --- a/objectscript-lsp/crates/objectscript-core/src/workspace.rs +++ b/objectscript-lsp/crates/objectscript-core/src/workspace.rs @@ -182,7 +182,6 @@ struct PendingBulkClass { class_range: Range, document: Document, class: Class, - inherited_classes: Vec<(String, LspRange)>, properties: HashMap, parameters: HashMap, methods: HashMap, @@ -253,15 +252,14 @@ impl<'a> BulkWorkspaceIndex<'a> { .expect("registered class document must contain a class definition") }; let mut class = Class::new(class_name.clone(), is_rtn); - let (_, _, methods, properties, parameters, inherited_classes, _, diagnostics) = - class.build_class( - starting_node, - &document.content, - is_rtn, - &class_id, - class_range, - &class_name, - ); + let (_, _, methods, properties, parameters, _, _, diagnostics) = class.build_class( + starting_node, + &document.content, + is_rtn, + &class_id, + class_range, + &class_name, + ); class.build_imports(&document.tree, &document.content); let class_is_final = class.is_final; let class_is_procedure_block = class.is_procedure_block; @@ -315,7 +313,6 @@ impl<'a> BulkWorkspaceIndex<'a> { class_range, document, class, - inherited_classes, properties, parameters, methods, @@ -334,7 +331,6 @@ impl<'a> BulkWorkspaceIndex<'a> { data.documents.insert(input.url, input.document); } let mut pending_calls = Vec::with_capacity(pending.len()); - let mut pending_inheritance = Vec::with_capacity(pending.len()); for mut prepared in pending { let class_name = prepared.class_name.clone(); let url = prepared.url.clone(); @@ -463,10 +459,6 @@ impl<'a> BulkWorkspaceIndex<'a> { } data.documents.insert(url.clone(), prepared.document); pending_calls.push((url.clone(), class_id, calls, oref_calls)); - prepared.inherited_classes.shrink_to_fit(); - // Retain only inheritance inputs needed by the serial graph phase. - // This avoids keeping trees/content alive twice. - pending_inheritance.push((url, class_id, class_name, prepared.inherited_classes)); } eprintln!( "[index] committed prepared results in {:.3?}", @@ -476,35 +468,51 @@ impl<'a> BulkWorkspaceIndex<'a> { // Establish the complete inheritance graph. No override/member // table is built until the graph is complete. let inheritance_started = std::time::Instant::now(); + let inheritance_inputs: Vec<(Url, ClassId, Vec<(String, LspRange)>)> = data + .classes + .values() + .filter_map(|class_id| { + let class = data.global_semantic_model.get_class(class_id)?; + let symbol = data.global_semantic_model.get_class_symbol(class_id)?; + Some(( + symbol.url.clone(), + *class_id, + class.inherited_classes.clone(), + )) + }) + .collect(); eprintln!( "[index] building inheritance for {} classes/routines", - pending_inheritance.len() + inheritance_inputs.len() ); let affected: HashSet = data.classes.values().copied().collect(); - for (url, class_id, class_name, inherited_classes) in &pending_inheritance { - // Any previously unresolved references to this newly registered class - // are resolved by the complete graph build below. - data.unresolved_inheritance_references.remove(class_name); - data.inheritance_diagonstics.remove(class_name); - - // Preserve diagnostics for parents that are still genuinely absent. - let missing_parents: Vec<(String, LspRange)> = inherited_classes + data.unresolved_inheritance_references.clear(); + data.inheritance_diagonstics.clear(); + for (url, class_id, inherited_classes) in &inheritance_inputs { + let invalid_parents: Vec<(String, LspRange)> = inherited_classes .iter() - .filter(|(name, _)| !data.classes.contains_key(name)) + .filter(|(name, _)| { + let Some(parent_id) = data.classes.get(name) else { + return true; + }; + data.global_semantic_model + .get_class(parent_id) + .is_some_and(|parent| parent.is_final == Some(true)) + }) .cloned() .collect(); let mut unused_affected = HashSet::new(); data.add_dependent_class_to_inherited_class( *class_id, url.clone(), - &missing_parents, + &invalid_parents, &mut unused_affected, ); } data.dependent_class_index = data.global_semantic_model.build_dependents(&data.classes); eprintln!( "[index] built inheritance for {} classes/routines in {:.3?}", - pending_inheritance.len(), + inheritance_inputs.len(), inheritance_started.elapsed() ); @@ -728,6 +736,133 @@ impl ProjectData { (routine_members, current_class_methods) } + /// Return direct and transitive SYS dependencies of all customer classes. + /// + /// Direct dependencies are SYS classes directly inherited by a customer + /// class or directly called by one of its methods. Transitive dependencies + /// are additional SYS classes reached through outgoing method calls or SYS + /// superclass chains. The returned sets are disjoint and use class names + /// because workspace-local ids cannot be compared across IRIS versions. + pub fn get_sys_dependencies(&self) -> (HashSet, HashSet) { + let customer_classes: Vec = self + .classes + .iter() + .filter_map(|(class_name, class_id)| { + (!self.sys_classes.contains(class_name) + || self.sys_classes_overwritten.contains(class_name)) + .then_some(*class_id) + }) + .collect(); + + let mut direct_ids = HashSet::new(); + let mut reachable_sys_ids = HashSet::new(); + let mut start_nodes = Vec::new(); + + for class_id in customer_classes { + let Some(customer_class) = self.global_semantic_model.get_class(&class_id) else { + continue; + }; + + for (parent_name, _) in &customer_class.inherited_classes { + if self.sys_classes.contains(parent_name) + && !self.sys_classes_overwritten.contains(parent_name) + && let Some(parent_id) = self.classes.get(parent_name) + { + direct_ids.insert(*parent_id); + reachable_sys_ids.insert(*parent_id); + } + } + + for node in self.dependency_graph.get_class_nodes(&class_id) { + start_nodes.push(node); + for edge in self + .dependency_graph + .graph + .edges_directed(node, petgraph::Direction::Outgoing) + { + let target = self.dependency_graph.graph[edge.target()]; + let Some(target_class) = self.global_semantic_model.get_class(&target.class) + else { + continue; + }; + if self.sys_classes.contains(&target_class.name) + && !self.sys_classes_overwritten.contains(&target_class.name) + { + direct_ids.insert(target.class); + reachable_sys_ids.insert(target.class); + } + } + } + } + + let mut visited_nodes = HashSet::new(); + let mut method_queue = std::collections::VecDeque::new(); + for node in start_nodes { + if visited_nodes.insert(node) { + method_queue.push_back(node); + } + } + while let Some(node) = method_queue.pop_front() { + for edge in self + .dependency_graph + .graph + .edges_directed(node, petgraph::Direction::Outgoing) + { + let target_node = edge.target(); + let target = self.dependency_graph.graph[target_node]; + if let Some(target_class) = self.global_semantic_model.get_class(&target.class) + && self.sys_classes.contains(&target_class.name) + && !self.sys_classes_overwritten.contains(&target_class.name) + { + reachable_sys_ids.insert(target.class); + } + if visited_nodes.insert(target_node) { + method_queue.push_back(target_node); + } + } + } + + let mut class_queue: std::collections::VecDeque = + reachable_sys_ids.iter().copied().collect(); + while let Some(class_id) = class_queue.pop_front() { + let Some(class) = self.global_semantic_model.get_class(&class_id) else { + continue; + }; + for (parent_name, _) in &class.inherited_classes { + if !self.sys_classes.contains(parent_name) + || self.sys_classes_overwritten.contains(parent_name) + { + continue; + } + let Some(parent_id) = self.classes.get(parent_name).copied() else { + continue; + }; + if reachable_sys_ids.insert(parent_id) { + class_queue.push_back(parent_id); + } + } + } + + let direct_sys_dependencies = direct_ids + .iter() + .filter_map(|class_id| { + self.global_semantic_model + .get_class(class_id) + .map(|class| class.name.clone()) + }) + .collect(); + let transitive_sys_dependencies = reachable_sys_ids + .difference(&direct_ids) + .filter_map(|class_id| { + self.global_semantic_model + .get_class(class_id) + .map(|class| class.name.clone()) + }) + .collect(); + + (direct_sys_dependencies, transitive_sys_dependencies) + } + fn refactor_document_with_parser( &self, url: &Url, @@ -1478,6 +1613,31 @@ impl ProjectData { ) { for (inherited_cls_name, inherited_class_ref_range) in inherited_classes { if let Some(inherited_class_id) = self.classes.get(inherited_cls_name).copied() { + if let Some(cls) = self.global_semantic_model.get_class(&inherited_class_id) { + if cls.is_final == Some(true) { + let diagnostic = Diagnostic { + range: inherited_class_ref_range.clone(), + severity: Some(DiagnosticSeverity::WARNING), + code: None, + code_description: None, + source: Some("ObjectScript".to_string()), + message: "Attempted to inherit Class that is marked as final." + .to_string(), + related_information: None, + tags: None, + data: None, + }; + self.unresolved_inheritance_references + .entry(inherited_cls_name.clone()) + .or_insert(Vec::new()) + .push((dependent_class_id, inherited_class_ref_range.clone())); + self.inheritance_diagonstics + .entry(inherited_cls_name.clone()) + .or_insert(HashMap::new()) + .insert(dependent_document_url.clone(), diagnostic); + continue; + } + } self.dependent_class_index .direct_subclasses .entry(inherited_class_id) @@ -3417,7 +3577,16 @@ impl ProjectData { .map(|c| { c.inherited_classes .iter() - .filter(|(parent_name, _)| cls_name_to_id.contains_key(parent_name)) + .filter(|(parent_name, _)| { + let Some(parent_id) = self.classes.get(parent_name) else { + return false; + }; + cls_name_to_id.contains_key(parent_name) + && self + .global_semantic_model + .get_class(parent_id) + .is_some_and(|parent| parent.is_final != Some(true)) + }) .count() }) .unwrap_or(0); @@ -3480,13 +3649,28 @@ impl ProjectData { let mut property_table: HashMap = HashMap::new(); let mut parameter_table: HashMap = HashMap::new(); + let valid_parent = |name: &String| { + self.classes + .get(name) + .and_then(|parent_id| self.global_semantic_model.get_class(parent_id)) + .is_some_and(|parent| parent.is_final != Some(true)) + }; let parent_names: Vec = if let Some(inheritance_direction) = inheritance_direction && inheritance_direction == "right" { - parents.iter().rev().map(|(name, _)| name.clone()).collect() + parents + .iter() + .rev() + .filter(|(name, _)| valid_parent(name)) + .map(|(name, _)| name.clone()) + .collect() } else { - parents.iter().map(|(name, _)| name.clone()).collect() + parents + .iter() + .filter(|(name, _)| valid_parent(name)) + .map(|(name, _)| name.clone()) + .collect() }; for parent_name in &parent_names { diff --git a/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs b/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs index 651b32c..742747f 100644 --- a/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs +++ b/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs @@ -4,7 +4,7 @@ use crate::parse_structures::{ }; use crate::workspace::ProjectData; use rayon::prelude::*; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashSet}; #[derive(Clone, Debug, Eq, PartialEq)] pub struct ValueChange { @@ -78,6 +78,7 @@ pub struct ClassSnapshot { pub imports: Vec, /// Parent order is significant for ObjectScript multiple inheritance. pub inherited_classes: Vec, + pub unresolved_inherited_classes: BTreeSet, pub inheritance_direction: Option, pub procedure_block: Option, pub language: Option, @@ -122,6 +123,7 @@ pub struct ClassDiff { pub class_name: String, pub imports: Option>>, pub inherited_classes: Option>>, + pub unresolved_inherited_classes: Option>>, pub inheritance_direction: Option>>, pub procedure_block: Option>>, pub language: Option>>, @@ -152,6 +154,18 @@ pub enum ClassComparison { Changed(ClassDiff), } +impl ClassComparison { + pub fn class_name(&self) -> &str { + match self { + Self::Unchanged { class_name } + | Self::Added { class_name } + | Self::Removed { class_name } + | Self::SnapshotUnavailable { class_name, .. } => class_name, + Self::Changed(diff) => &diff.class_name, + } + } +} + pub fn compare_classes_parallel( class_names: &[String], baseline: &ProjectData, @@ -163,6 +177,26 @@ pub fn compare_classes_parallel( .collect() } +/// Compare every class name present in either workspace. +/// +/// The union ensures classes that exist in only one workspace are reported as +/// added or removed. Result order is unspecified; callers that need a stable +/// report should sort by class name at the presentation boundary. +pub fn compare_workspaces_parallel( + baseline: &ProjectData, + target: &ProjectData, +) -> Vec { + let class_names: Vec = baseline + .classes + .keys() + .chain(target.classes.keys()) + .cloned() + .collect::>() + .into_iter() + .collect(); + compare_classes_parallel(&class_names, baseline, target) +} + pub fn compare_class( baseline: &ProjectData, target: &ProjectData, @@ -181,27 +215,56 @@ pub fn compare_class( class_name: class_name.to_string(), }, (Some(before), Some(after)) if before.content == after.content => { - ClassComparison::Unchanged { - class_name: class_name.to_string(), - } - } - (Some(_), Some(_)) => { - let before = snapshot_class(baseline, class_name); - let after = snapshot_class(target, class_name); - let baseline_available = before.is_some(); - let target_available = after.is_some(); - let (Some(before), Some(after)) = (before, after) else { - return ClassComparison::SnapshotUnavailable { + let unresolved_before = unresolved_inherited_classes(baseline, class_name); + let unresolved_after = unresolved_inherited_classes(target, class_name); + if matches!( + (&unresolved_before, &unresolved_after), + (Some(before), Some(after)) if before == after + ) { + ClassComparison::Unchanged { class_name: class_name.to_string(), - baseline_available, - target_available, - }; - }; - ClassComparison::Changed(diff_class(before, after)) + } + } else { + compare_snapshots(baseline, target, class_name) + } } + (Some(_), Some(_)) => compare_snapshots(baseline, target, class_name), } } +fn compare_snapshots( + baseline: &ProjectData, + target: &ProjectData, + class_name: &str, +) -> ClassComparison { + let before = snapshot_class(baseline, class_name); + let after = snapshot_class(target, class_name); + let baseline_available = before.is_some(); + let target_available = after.is_some(); + let (Some(before), Some(after)) = (before, after) else { + return ClassComparison::SnapshotUnavailable { + class_name: class_name.to_string(), + baseline_available, + target_available, + }; + }; + ClassComparison::Changed(diff_class(before, after)) +} + +fn unresolved_inherited_classes(data: &ProjectData, class_name: &str) -> Option> { + let class_id = data.classes.get(class_name)?; + let class = data.global_semantic_model.get_class(class_id)?; + Some( + class + .inherited_classes + .iter() + .filter_map(|(parent_name, _)| { + (!data.classes.contains_key(parent_name)).then_some(parent_name.clone()) + }) + .collect(), + ) +} + pub fn snapshot_class(data: &ProjectData, class_name: &str) -> Option { let class_id = *data.classes.get(class_name)?; let class = data.global_semantic_model.get_class(&class_id)?; @@ -214,6 +277,7 @@ pub fn snapshot_class(data: &ProjectData, class_name: &str) -> Option Option ClassDiff { class_name: before.name.clone(), imports: value_change(&before.imports, &after.imports), inherited_classes: value_change(&before.inherited_classes, &after.inherited_classes), + unresolved_inherited_classes: value_change( + &before.unresolved_inherited_classes, + &after.unresolved_inherited_classes, + ), inheritance_direction: value_change( &before.inheritance_direction, &after.inheritance_direction, @@ -370,6 +439,7 @@ impl ClassDiff { pub fn has_semantic_changes(&self) -> bool { self.imports.is_some() || self.inherited_classes.is_some() + || self.unresolved_inherited_classes.is_some() || self.inheritance_direction.is_some() || self.procedure_block.is_some() || self.language.is_some() @@ -596,4 +666,107 @@ ClassMethod Calculate() As %String ); assert!(!diff.unmodeled_source_change); } + + #[test] + fn compares_union_of_both_workspaces() { + fn add_class(project: &ProjectState, class_name: &str, content: &str) { + project.handle_document_opened( + Url::parse(&format!("file:///workspace/{class_name}.cls")).unwrap(), + content.to_string(), + FileType::Cls, + 1, + ); + } + + let baseline = ProjectState::new(); + add_class(&baseline, "Demo.Compare", BEFORE); + add_class( + &baseline, + "Demo.Unchanged", + "Class Demo.Unchanged { Property Value As %String; }", + ); + add_class( + &baseline, + "Demo.Removed", + "Class Demo.Removed { Property Value As %String; }", + ); + + let target = ProjectState::new(); + add_class(&target, "Demo.Compare", AFTER); + add_class( + &target, + "Demo.Unchanged", + "Class Demo.Unchanged { Property Value As %String; }", + ); + add_class( + &target, + "Demo.Added", + "Class Demo.Added { Property Value As %String; }", + ); + + let baseline = baseline.data.read(); + let target = target.data.read(); + let comparisons = compare_workspaces_parallel(&baseline, &target); + + assert!(comparisons.iter().any(|comparison| matches!( + comparison, + ClassComparison::Changed(diff) if diff.class_name == "Demo.Compare" + ))); + assert!(comparisons.iter().any(|comparison| matches!( + comparison, + ClassComparison::Unchanged { class_name } + if class_name == "Demo.Unchanged" + ))); + assert!(comparisons.iter().any(|comparison| matches!( + comparison, + ClassComparison::Added { class_name } if class_name == "Demo.Added" + ))); + assert!(comparisons.iter().any(|comparison| matches!( + comparison, + ClassComparison::Removed { class_name } if class_name == "Demo.Removed" + ))); + assert_eq!(comparisons.len(), 4); + } + + #[test] + fn reports_inheritance_resolution_change_with_identical_source() { + const CHILD: &str = "Class Demo.Child Extends Demo.Parent { }"; + + let baseline = ProjectState::new(); + baseline.handle_document_opened( + Url::parse("file:///workspace/Demo.Child.cls").unwrap(), + CHILD.to_string(), + FileType::Cls, + 1, + ); + + let target = ProjectState::new(); + target.handle_document_opened( + Url::parse("file:///workspace/Demo.Child.cls").unwrap(), + CHILD.to_string(), + FileType::Cls, + 1, + ); + target.handle_document_opened( + Url::parse("file:///workspace/Demo.Parent.cls").unwrap(), + "Class Demo.Parent { }".to_string(), + FileType::Cls, + 1, + ); + + let baseline = baseline.data.read(); + let target = target.data.read(); + let ClassComparison::Changed(diff) = compare_class(&baseline, &target, "Demo.Child") else { + panic!("inheritance resolution change should be reported"); + }; + + assert_eq!( + diff.unresolved_inherited_classes, + Some(ValueChange { + before: BTreeSet::from(["Demo.Parent".to_string()]), + after: BTreeSet::new(), + }) + ); + assert!(!diff.unmodeled_source_change); + } } diff --git a/objectscript-lsp/documentation/class-comparison.md b/objectscript-lsp/documentation/class-comparison.md index 0bd1b73..f55bb9b 100644 --- a/objectscript-lsp/documentation/class-comparison.md +++ b/objectscript-lsp/documentation/class-comparison.md @@ -10,6 +10,102 @@ This is intended for the IRIS upgrade workflow: 3. Index SYS version 2 independently. 4. Compare only those dependent class names between the two projects. +## Implementation status + +### Implemented + +- [x] ID-independent class snapshots. +- [x] ID-independent method, variable, property, and parameter snapshots. +- [x] Exact source-equality short-circuit for unchanged classes. +- [x] Added, removed, unchanged, unavailable, and granular changed results. +- [x] Parallel comparison across requested class names. +- [x] Parallel comparison across the union of all classes in two workspaces. +- [x] Field-level class and member changes. +- [x] Unresolved-versus-resolved direct inheritance changes, even when class + source is identical. +- [x] Variable comparison as a multiset rather than by workspace-local IDs. +- [x] Reporting of source differences not represented by the current semantic + model. +- [x] Unit tests for the unchanged fast path and granular class/member/variable + changes. +- [x] Public export through `objectscript_core::workspace_diff`. + +### Remaining integration work + +- [x] Compute the transitive customer-to-SYS dependency-name closure from the + baseline workspace. +- [x] Add direct superclass names recursively to that closure. +- [x] Invoke `compare_classes_parallel` from the development two-version + workflow in `main`. +- [x] Write a deterministic human-readable comparison report containing direct + and transitive dependency names, summary counts, and granular differences. +- [ ] Decide whether the text report is the final supported output boundary or + add JSON, LSP, or MCP presentation. +- [ ] Add integration tests that independently index two fixture workspaces and + compare a realistic dependency closure. +- [ ] Add explicit tests for added classes, removed classes, unavailable + snapshots, multiple inheritance ordering, and repeated equivalent + variable definitions. +- [ ] Benchmark comparison over the real dependent SYS class set and report the + unchanged-source hit rate. +- [ ] Decide whether source-only changes such as comments and formatting should + appear in the final user-facing report or be optionally suppressed. +- [ ] Add typed snapshots for relationships, indexes, queries, triggers, XData, + storage definitions, and include/macro dependencies as those constructs + enter the semantic model. +- [ ] Consider serializable result types after the output boundary is chosen. + Do not add serialization solely for internal comparison. + +The comparison engine and development file-report workflow are implemented. +Production LSP/MCP exposure and broader integration coverage remain. + +## Development workflow integration + +The development `main` now: + +1. indexes the baseline SYS root; +2. marks those classes as SYS; +3. appends and indexes the customer root in the same `ProjectData`; +4. indexes the target SYS root independently; +5. computes direct and transitive SYS dependency names from the baseline; +6. compares those names between baseline and target in parallel; +7. writes a human-readable report file. + +Default paths match the local upgrade workspace. Override them without editing +source: + +```sh +OBJECTSCRIPT_BASELINE_SYS_ROOT=/path/to/old/sys \ +OBJECTSCRIPT_TARGET_SYS_ROOT=/path/to/new/sys \ +OBJECTSCRIPT_CUSTOMER_ROOT=/path/to/customer \ +OBJECTSCRIPT_COMPARISON_OUTPUT=/path/to/report.txt \ +OBJECTSCRIPT_INDEX_WORKERS=8 \ +cargo run --release --bin objectscript-lsp +``` + +By default, the development workflow compares only customer SYS dependencies. +To compare every class in the baseline SYS root with every class in the target +SYS root, omit customer indexing and use: + +```sh +OBJECTSCRIPT_COMPARE_ALL=true \ +OBJECTSCRIPT_BASELINE_SYS_ROOT=/path/to/old/sys \ +OBJECTSCRIPT_TARGET_SYS_ROOT=/path/to/new/sys \ +OBJECTSCRIPT_COMPARISON_OUTPUT=/path/to/all-classes-report.txt \ +OBJECTSCRIPT_INDEX_WORKERS=8 \ +cargo run --release --bin objectscript-lsp +``` + +The report contains: + +- sorted direct SYS dependency names; +- sorted transitive-only SYS dependency names; +- unchanged/changed/added/removed/unavailable counts; +- detailed `ClassComparison` debug output for every non-unchanged class. + +The normal LSP transport still starts with `--server` in this development +executable. + ## API ```rust @@ -17,6 +113,7 @@ use objectscript_core::workspace_diff::{ ClassComparison, compare_class, compare_classes_parallel, + compare_workspaces_parallel, }; ``` @@ -40,6 +137,20 @@ let comparisons = compare_classes_parallel( ); ``` +Compare the union of all class names in two workspaces: + +```rust +let comparisons = compare_workspaces_parallel( + &baseline_project_data, + &target_project_data, +); +``` + +This reports classes found in only the target as `Added`, classes found only in +the baseline as `Removed`, and compares classes present in both. Result order is +unspecified; sort by class name only at presentation boundaries that require +stable output. + Both projects are read-only during comparison. The outer operation parallelizes by class; member comparison inside one class remains serial to avoid nested Rayon work. @@ -48,12 +159,16 @@ Rayon work. The comparator first looks up each class's defining `Document`. -- Equal source strings return `ClassComparison::Unchanged` immediately. +- Equal source strings return `ClassComparison::Unchanged` immediately only + when both workspaces also have the same unresolved-parent set. - A missing target class returns `Removed`. - A missing baseline class returns `Added`. - Different source strings trigger semantic snapshot construction and granular comparison. +This means identical child source is still reported as changed when a parent is +missing in one workspace and resolves in the other. + This avoids allocating snapshots for unchanged SYS classes. ## ID-independent snapshots @@ -68,6 +183,7 @@ member names. - direct imports, normalized by sorting; - direct inherited class names, preserving parent order; +- unresolved direct inherited class names; - inheritance direction; - `ProcedureBlock`; - `Language`; @@ -176,18 +292,22 @@ Exact class and method source-change reporting prevents these differences from being silently treated as unchanged. Add typed snapshots as these constructs enter the semantic model. -## Dependency closure is separate +## Dependency closure The comparison module accepts class names; it does not decide which SYS classes the customer depends on. -The dependency-closure implementation should: +`ProjectData::get_sys_dependencies` computes that selection separately. It: + +1. Starts from all customer-owned methods in the baseline workspace. +2. Traverses outgoing ordinary and oref call edges transitively. +3. Records SYS classes directly called or inherited by customer classes. +4. Records additional SYS classes reached through SYS-to-SYS calls. +5. Recursively adds direct SYS superclass names. +6. Returns disjoint direct and transitive class-name sets. -1. start from all customer-owned methods in the baseline workspace; -2. traverse outgoing ordinary and oref call edges transitively; -3. add reached SYS class names; -4. recursively add direct superclass names; -5. pass the resulting names to `compare_classes_parallel`. +The upgrade workflow should combine the returned names and pass them to +`compare_classes_parallel`. Use class names as cross-workspace identity. Never pass baseline IDs into the target workspace. diff --git a/objectscript-lsp/objectscript-tests/customer/customer1.cls b/objectscript-lsp/objectscript-tests/customer/customer1.cls new file mode 100644 index 0000000..ef2a83c --- /dev/null +++ b/objectscript-lsp/objectscript-tests/customer/customer1.cls @@ -0,0 +1,6 @@ +Class CustomerOne { + ClassMethod callToSYS() { + w "calling subclass one" + d ##class(SubClassOne).newVarChange() + } +} diff --git a/objectscript-lsp/objectscript-tests/new_scope_res_final/scope-resolution.cls b/objectscript-lsp/objectscript-tests/new_scope_res_final/scope-resolution.cls new file mode 100644 index 0000000..16103a5 --- /dev/null +++ b/objectscript-lsp/objectscript-tests/new_scope_res_final/scope-resolution.cls @@ -0,0 +1,33 @@ +Class ScopeResolution [Not ProcedureBlock, Final] +{ + +ClassMethod m1() { + w x +} + +Method m2() { + set x = 200 + d ..m1() +} + +Method m3() { + set x = 300 + d ..m2() +} + +Method conditionalScope() { + set y = 1 + if (1 = 1) { + set y = 2 + } + set y = 3 + w y +} + +Method sameScope() { + set z = 10 + set z = 20 + w z +} + +} diff --git a/objectscript-lsp/objectscript-tests/new_scope_res_final/subclass.cls b/objectscript-lsp/objectscript-tests/new_scope_res_final/subclass.cls new file mode 100644 index 0000000..e8b20db --- /dev/null +++ b/objectscript-lsp/objectscript-tests/new_scope_res_final/subclass.cls @@ -0,0 +1,7 @@ +Class SubClassOne Extends SuperClass { + ClassMethod newVarChange() [ProcedureBlock = 0] { + set x = 7 + d ##class(SuperClass).multiplePubVarDefs() + d ..m1() + } +} diff --git a/objectscript-lsp/objectscript-tests/new_scope_res_final/subclass_two.cls b/objectscript-lsp/objectscript-tests/new_scope_res_final/subclass_two.cls new file mode 100644 index 0000000..45af992 --- /dev/null +++ b/objectscript-lsp/objectscript-tests/new_scope_res_final/subclass_two.cls @@ -0,0 +1,6 @@ +Class SubClassTwo Extends SuperClass { + ClassMethod newVarChange() [ProcedureBlock = 0] { + set x = 10 + d ..multiplePubVarDefs() + } +} diff --git a/objectscript-lsp/objectscript-tests/new_scope_res_final/testing-variable-building.cls b/objectscript-lsp/objectscript-tests/new_scope_res_final/testing-variable-building.cls new file mode 100644 index 0000000..6254dee --- /dev/null +++ b/objectscript-lsp/objectscript-tests/new_scope_res_final/testing-variable-building.cls @@ -0,0 +1,23 @@ +Class SuperClass Extends ScopeResolution [Not ProcedureBlock] +{ + ClassMethod newVarChange() { + set x = 32 + w x + w y + d ..multiplePubVarDefs() + } + + ClassMethod multiplePubVarDefs() { + set y = 24 + zw + w x + w y + write "Goodbye" + } + + ClassMethod testingIndentation() { + set y = 3 + set x = 12 + w y + } +} diff --git a/objectscript-lsp/objectscript-tests/old/scope-resolution.cls b/objectscript-lsp/objectscript-tests/old/scope-resolution.cls new file mode 100644 index 0000000..9101dac --- /dev/null +++ b/objectscript-lsp/objectscript-tests/old/scope-resolution.cls @@ -0,0 +1,33 @@ +Class ScopeResolution [Not ProcedureBlock] +{ + +ClassMethod m1() { + w x +} + +Method m2() { + set x = 200 + d ..m1() +} + +Method m3() { + set x = 300 + d ..m2() +} + +Method conditionalScope() { + set y = 1 + if (1 = 1) { + set y = 2 + } + set y = 3 + w y +} + +Method sameScope() { + set z = 10 + set z = 20 + w z +} + +} diff --git a/objectscript-lsp/objectscript-tests/old/subclass.cls b/objectscript-lsp/objectscript-tests/old/subclass.cls new file mode 100644 index 0000000..e8b20db --- /dev/null +++ b/objectscript-lsp/objectscript-tests/old/subclass.cls @@ -0,0 +1,7 @@ +Class SubClassOne Extends SuperClass { + ClassMethod newVarChange() [ProcedureBlock = 0] { + set x = 7 + d ##class(SuperClass).multiplePubVarDefs() + d ..m1() + } +} diff --git a/objectscript-lsp/objectscript-tests/old/subclass_two.cls b/objectscript-lsp/objectscript-tests/old/subclass_two.cls new file mode 100644 index 0000000..45af992 --- /dev/null +++ b/objectscript-lsp/objectscript-tests/old/subclass_two.cls @@ -0,0 +1,6 @@ +Class SubClassTwo Extends SuperClass { + ClassMethod newVarChange() [ProcedureBlock = 0] { + set x = 10 + d ..multiplePubVarDefs() + } +} diff --git a/objectscript-lsp/objectscript-tests/old/testing-variable-building.cls b/objectscript-lsp/objectscript-tests/old/testing-variable-building.cls new file mode 100644 index 0000000..6254dee --- /dev/null +++ b/objectscript-lsp/objectscript-tests/old/testing-variable-building.cls @@ -0,0 +1,23 @@ +Class SuperClass Extends ScopeResolution [Not ProcedureBlock] +{ + ClassMethod newVarChange() { + set x = 32 + w x + w y + d ..multiplePubVarDefs() + } + + ClassMethod multiplePubVarDefs() { + set y = 24 + zw + w x + w y + write "Goodbye" + } + + ClassMethod testingIndentation() { + set y = 3 + set x = 12 + w y + } +} diff --git a/objectscript-lsp/src/main.rs b/objectscript-lsp/src/main.rs index 9962a34..97f969d 100644 --- a/objectscript-lsp/src/main.rs +++ b/objectscript-lsp/src/main.rs @@ -1,5 +1,10 @@ use crate::server::BackendWrapper; use objectscript_core::workspace::ProjectState; +use objectscript_core::workspace_diff::{ + ClassComparison, compare_classes_parallel, compare_workspaces_parallel, +}; +use std::collections::BTreeSet; +use std::io::{BufWriter, Write}; use std::path::PathBuf; use tower_lsp::{LspService, Server}; // #[cfg(test)] @@ -30,6 +35,68 @@ async fn setup_backend_and_workspace(project_root: PathBuf) -> (BackendTester, U (backend, uri) } +fn write_comparison_report( + output_path: &PathBuf, + baseline_root: &PathBuf, + target_root: &PathBuf, + customer_root: &PathBuf, + comparison_scope: &str, + direct_dependencies: &BTreeSet, + transitive_dependencies: &BTreeSet, + comparisons: &[ClassComparison], +) -> std::io::Result<()> { + let file = std::fs::File::create(output_path)?; + let mut output = BufWriter::new(file); + let mut unchanged = 0; + let mut added = 0; + let mut removed = 0; + let mut changed = 0; + let mut unavailable = 0; + for comparison in comparisons { + match comparison { + ClassComparison::Unchanged { .. } => unchanged += 1, + ClassComparison::Added { .. } => added += 1, + ClassComparison::Removed { .. } => removed += 1, + ClassComparison::Changed(_) => changed += 1, + ClassComparison::SnapshotUnavailable { .. } => unavailable += 1, + } + } + + writeln!(output, "ObjectScript SYS dependency comparison")?; + writeln!(output, "baseline SYS: {}", baseline_root.display())?; + writeln!(output, "target SYS: {}", target_root.display())?; + writeln!(output, "customer: {}", customer_root.display())?; + writeln!(output, "scope: {comparison_scope}")?; + writeln!(output)?; + writeln!(output, "direct dependencies: {}", direct_dependencies.len())?; + for class_name in direct_dependencies { + writeln!(output, " {class_name}")?; + } + writeln!( + output, + "transitive-only dependencies: {}", + transitive_dependencies.len() + )?; + for class_name in transitive_dependencies { + writeln!(output, " {class_name}")?; + } + writeln!(output)?; + writeln!(output, "comparison summary")?; + writeln!(output, " unchanged: {unchanged}")?; + writeln!(output, " changed: {changed}")?; + writeln!(output, " added: {added}")?; + writeln!(output, " removed: {removed}")?; + writeln!(output, " snapshot unavailable: {unavailable}")?; + writeln!(output)?; + writeln!(output, "differences")?; + for comparison in comparisons { + if !matches!(comparison, ClassComparison::Unchanged { .. }) { + writeln!(output, "{comparison:#?}")?; + } + } + output.flush() +} + #[tokio::main] async fn main() { let worker_count = std::env::var("OBJECTSCRIPT_INDEX_WORKERS") @@ -54,22 +121,96 @@ async fn main() { return; } - let iris_current_version_project_root = - PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202502/").unwrap(); - let iris_new_version_project_root = - PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202602/").unwrap(); + let iris_current_version_project_root = std::env::var("OBJECTSCRIPT_BASELINE_SYS_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|_| { + PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202502/").unwrap() + }); + let iris_new_version_project_root = std::env::var("OBJECTSCRIPT_TARGET_SYS_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|_| { + PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202602/").unwrap() + }); + let customer_project_root = std::env::var("OBJECTSCRIPT_CUSTOMER_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|_| PathBuf::from("/Users/hkimura/objectscript-dependencies/customer/")); + let comparison_output = std::env::var("OBJECTSCRIPT_COMPARISON_OUTPUT") + .map(PathBuf::from) + .unwrap_or_else(|_| { + PathBuf::from("/Users/hkimura/objectscript-dependencies/class-comparison-report.txt") + }); + let compare_all_classes = std::env::var("OBJECTSCRIPT_COMPARE_ALL") + .map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES")) + .unwrap_or(false); - // index both IRIS versions concurrently + let baseline_sys_root = iris_current_version_project_root.clone(); + let baseline_customer_root = customer_project_root.clone(); + let baseline_compare_all = compare_all_classes; + let baseline_index = async { + let (backend, uri) = setup_backend_and_workspace(baseline_sys_root).await; + if !baseline_compare_all { + let project = backend + .get_project(&uri) + .expect("missing baseline IRIS project"); + project.data.write().mark_current_classes_as_sys(); + backend + .index_workspace_root(&uri, baseline_customer_root) + .await; + } + (backend, uri) + }; + + // Index the baseline SYS + customer project and target SYS project concurrently. let ((old_iris_backend, old_iris_uri), (new_iris_backend, new_iris_uri)) = tokio::join!( - setup_backend_and_workspace(iris_current_version_project_root), - setup_backend_and_workspace(iris_new_version_project_root), + baseline_index, + setup_backend_and_workspace(iris_new_version_project_root.clone()), ); - let _old_iris_workspace = old_iris_backend + let old_iris_workspace = old_iris_backend .get_project(&old_iris_uri) .expect("missing project for old iris version"); - let _new_iris_workspace = new_iris_backend + let new_iris_workspace = new_iris_backend .get_project(&new_iris_uri) .expect("missing project for new iris version"); + + let old_data = old_iris_workspace.data.read(); + let new_data = new_iris_workspace.data.read(); + let (direct, transitive) = old_data.get_sys_dependencies(); + let direct: BTreeSet = direct.into_iter().collect(); + let transitive: BTreeSet = transitive.into_iter().collect(); + let dependency_names: Vec = direct.iter().chain(transitive.iter()).cloned().collect(); + let (comparison_scope, mut comparisons) = if compare_all_classes { + ( + "all classes in either workspace", + compare_workspaces_parallel(&old_data, &new_data), + ) + } else { + ( + "customer SYS dependencies", + compare_classes_parallel(&dependency_names, &old_data, &new_data), + ) + }; + comparisons.sort_by(|left, right| left.class_name().cmp(right.class_name())); + write_comparison_report( + &comparison_output, + &iris_current_version_project_root, + &iris_new_version_project_root, + &customer_project_root, + comparison_scope, + &direct, + &transitive, + &comparisons, + ) + .unwrap_or_else(|error| { + panic!( + "failed to write comparison report {}: {error}", + comparison_output.display() + ) + }); + eprintln!( + "[compare] wrote {} class comparisons to {}", + comparisons.len(), + comparison_output.display() + ); } diff --git a/objectscript-lsp/src/test.rs b/objectscript-lsp/src/test.rs index 36ba592..d66d658 100644 --- a/objectscript-lsp/src/test.rs +++ b/objectscript-lsp/src/test.rs @@ -100,6 +100,24 @@ mod tests { (backend, uri) } + #[tokio::test] + async fn test_is_final_creates_unresolved_inheritance() { + let test_root = env::current_dir().unwrap().join("objectscript-tests"); + let workspace_root = test_root.join("new_scope_res_final"); + let (backend, uri) = setup_backend_and_workspace(workspace_root.clone()).await; + + let workspace = backend + .get_project(&uri) + .expect("missing project for new test version"); + + let project_data = workspace.data.read(); + assert!( + project_data + .unresolved_inheritance_references + .contains_key("ScopeResolution") + ) + } + #[tokio::test] async fn test_append_customer_root_preserves_sys_class_boundary() { let test_root = env::current_dir().unwrap().join("objectscript-tests"); @@ -122,6 +140,56 @@ mod tests { assert!(data.classes.contains_key("hksubclass")); } + #[test] + fn test_sys_dependencies_include_calls_and_superclasses() { + let project = ProjectState::new(); + project + .project_root_path + .set(Some(env::current_dir().unwrap())) + .expect("project root should be unset"); + + let sys_sources = [ + ( + "sys-base.cls", + "Class Sys.Base { ClassMethod Base() { Quit } }", + ), + ( + "sys-leaf.cls", + "Class Sys.Leaf { ClassMethod Leaf() { Quit } }", + ), + ( + "sys-middle.cls", + "Class Sys.Middle Extends Sys.Base { ClassMethod Mid() { Do ##class(Sys.Leaf).Leaf() } }", + ), + ]; + for (version, (file_name, content)) in sys_sources.into_iter().enumerate() { + project.handle_document_opened( + Url::from_file_path(env::current_dir().unwrap().join(file_name)) + .expect("valid SYS file URL"), + content.to_string(), + FileType::Cls, + version as i32, + ); + } + project.data.write().mark_current_classes_as_sys(); + + project.handle_document_opened( + Url::from_file_path(env::current_dir().unwrap().join("customer-app.cls")) + .expect("valid customer file URL"), + "Class Customer.App Extends Sys.Middle { ClassMethod Run() { Do ##class(Sys.Middle).Mid() } }" + .to_string(), + FileType::Cls, + 4, + ); + + let (direct, transitive) = project.data.read().get_sys_dependencies(); + assert_eq!(direct, HashSet::from(["Sys.Middle".to_string()])); + assert_eq!( + transitive, + HashSet::from(["Sys.Base".to_string(), "Sys.Leaf".to_string()]) + ); + } + fn point_for_substring_n(content: &str, needle: &str, occurrence: usize) -> Point { assert!(occurrence > 0, "occurrence must be >= 1"); let mut start = 0usize; From 290cbfccfcb2fcd2effd9f249500148ead232654 Mon Sep 17 00:00:00 2001 From: Hannah Date: Tue, 22 Sep 2026 09:21:36 -0400 Subject: [PATCH 4/8] combine udl class header query --- .../crates/objectscript-core/src/class.rs | 228 ++++++++++-------- .../objectscript-core/src/global_semantic.rs | 4 +- .../crates/objectscript-core/src/method.rs | 32 ++- .../objectscript-core/src/parse_structures.rs | 32 ++- .../crates/objectscript-core/src/workspace.rs | 72 +++--- .../objectscript-core/src/workspace_diff.rs | 23 +- objectscript-lsp/src/test.rs | 10 +- 7 files changed, 216 insertions(+), 185 deletions(-) diff --git a/objectscript-lsp/crates/objectscript-core/src/class.rs b/objectscript-lsp/crates/objectscript-core/src/class.rs index 0005198..3c258f5 100644 --- a/objectscript-lsp/crates/objectscript-core/src/class.rs +++ b/objectscript-lsp/crates/objectscript-core/src/class.rs @@ -5,8 +5,8 @@ use crate::common::{ }; use crate::parse_structures::{ - Class, ClassId, Language, MemberType, Method, MethodId, MethodRef, MethodType, Parameter, - ParameterId, ParameterRef, Property, PropertyId, PropertyRef, + Class, ClassId, InheritanceDirection, Language, MemberType, Method, MethodId, MethodRef, + MethodType, Parameter, ParameterId, ParameterRef, Property, PropertyId, PropertyRef, }; use std::collections::{HashMap, HashSet}; use std::sync::OnceLock; @@ -17,13 +17,9 @@ use tree_sitter::{ use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; use tree_sitter_objectscript_routine::LANGUAGE_OBJECTSCRIPT_ROUTINE; -const UDL_CLASS_HEADER_QUERY: &str = r#" -[ - (class_definition (class_extends (class_name (identifier) @inherits))) - (class_definition (class_keyword) @classkeyword) -]"#; - const UDL_CLASS_MEMBER_QUERY: &str = r#"(class_definition + (class_extends (class_name (identifier) @inherits)) ? + (class_keyword) @classkeyword (class_body (class_statement [ @@ -31,6 +27,14 @@ const UDL_CLASS_MEMBER_QUERY: &str = r#"(class_definition (classmethod (method_definition) @classmethod) (parameter) @parameter (property) @property + (relationship) @relationship + (foreignkey) @foreignkey + (query) @query + (index) @index + (trigger) @trigger + (xdata) @xdata + (projection) @projection + (storage) @storage ]) ) )"#; @@ -54,17 +58,17 @@ fn cached_query( }) } -fn udl_class_header_query() -> &'static Query { - static QUERY: OnceLock = OnceLock::new(); - cached_query( - &QUERY, - LANGUAGE_OBJECTSCRIPT_UDL.into(), - UDL_CLASS_HEADER_QUERY, - "UDL class header", - ) -} +// fn udl_class_header_query() -> &'static Query { +// static QUERY: OnceLock = OnceLock::new(); +// cached_query( +// &QUERY, +// LANGUAGE_OBJECTSCRIPT_UDL.into(), +// UDL_CLASS_HEADER_QUERY, +// "UDL class header", +// ) +// } -fn udl_class_member_query() -> &'static Query { +fn udl_class_query() -> &'static Query { static QUERY: OnceLock = OnceLock::new(); cached_query( &QUERY, @@ -93,12 +97,20 @@ impl Class { name, imports: Vec::new(), inherited_classes: Vec::new(), - inheritance_direction: None, - is_procedure_block: None, - default_language: None, + inheritance_direction: InheritanceDirection::Left, + is_procedure_block: true, + default_language: Language::Objectscript, methods: HashMap::new(), properties: HashMap::new(), parameters: HashMap::new(), + relationships: HashMap::new(), + foreignkeys: HashMap::new(), + queries: HashMap::new(), + triggers: HashMap::new(), + indices: HashMap::new(), + projections: HashMap::new(), + xdata: HashMap::new(), + storage: HashMap::new(), active: true, is_rtn, next_method_id: 0, @@ -112,17 +124,17 @@ impl Class { next_storage_id: 0, next_trigger_id: 0, next_xdata_id: 0, - is_final: None, + is_final: false, } } pub fn reset_keywords(&mut self) { self.active = true; - self.is_final = None; + self.is_final = false; self.inherited_classes = Vec::new(); - self.inheritance_direction = None; - self.is_procedure_block = None; - self.default_language = None; + self.inheritance_direction = InheritanceDirection::Left; + self.is_procedure_block = true; + self.default_language = Language::Objectscript; } /// Resets this `Class` to a clean state and sets its `name` and `active` flag. @@ -133,9 +145,9 @@ impl Class { self.name = class_name; self.imports = Vec::new(); self.inherited_classes = Vec::new(); - self.inheritance_direction = None; - self.is_procedure_block = None; - self.default_language = None; + self.inheritance_direction = InheritanceDirection::Left; + self.is_procedure_block = true; + self.default_language = Language::Objectscript; self.methods = HashMap::new(); self.properties = HashMap::new(); self.parameters = HashMap::new(); @@ -143,7 +155,7 @@ impl Class { self.next_method_id = 0; self.next_parameter_id = 0; self.next_property_id = 0; - self.is_final = None; + self.is_final = false; } /// Allocates and returns the next sequential method ID for this class. @@ -285,90 +297,41 @@ impl Class { // NOTE: right now, properties and parameters are not incremental.. they are so small in terms of what it takes to rebuild that it doesn't make sense to incrementally build them atm self.properties.clear(); self.parameters.clear(); + self.properties.clear(); + self.relationships.clear(); + self.foreignkeys.clear(); + self.queries.clear(); + self.indices.clear(); + self.triggers.clear(); + self.projections.clear(); + self.xdata.clear(); + self.storage.clear(); self.next_property_id = 0; self.next_parameter_id = 0; - if !is_rtn { - let query = udl_class_header_query(); - let inherits_idx = query.capture_index_for_name("inherits"); - let keyword_idx = query.capture_index_for_name("classkeyword"); - let mut cursor = QueryCursor::new(); - let mut iter = cursor.matches(query, root_node, content.as_bytes()); - - while let Some(query_match) = iter.next() { - let mut i = 0; - while i < query_match.captures.len() { - let capture = &query_match.captures[i]; - if inherits_idx == Some(capture.index) { - if let Some(inherited_cls_name) = - get_string_at_byte_range(content, capture.node.byte_range()) - { - let lsp_range = ts_range_to_lsp_range(content, capture.node.range()); - inherited_classes.push((inherited_cls_name.clone(), lsp_range)); - // inherited_class_ranges.insert(inherited_cls_name.clone(), lsp_range); - if let Some((old_inherited_class, _)) = - self.inherited_classes.get(inherited_count) - { - if &inherited_cls_name != old_inherited_class { - inheritance_changed = true; - } - } else { - inheritance_changed = true; - } - } - inherited_count += 1; - } else if keyword_idx == Some(capture.index) - && let Some(keyword_str) = - get_string_at_byte_range(content, capture.node.byte_range()) - { - let (not, keyword_name, values) = - get_keyword_and_value(keyword_str.as_str()); - if keyword_name == "procedureblock" { - if not { - self.is_procedure_block = Some(false); - } else { - self.is_procedure_block = Some(true); - } - } else if keyword_name == "language" { - if let Some(value) = values.get(0).copied() { - if value == "objectscript" { - self.default_language = Some(Language::Objectscript); - } else if value == "tsql" { - self.default_language = Some(Language::TSql); - } - } - } else if keyword_name == "inheritance" { - if let Some(value) = values.get(0).copied() { - if value == "right" { - self.inheritance_direction = Some("right".to_string()); - } else { - self.inheritance_direction = Some("left".to_string()); - } - if self.inheritance_direction != old_inheritance_direction { - inheritance_changed = true; - } - } - } else if keyword_name == "final" { - if not { - self.is_final = Some(false); - } else { - self.is_final = Some(true); - } - } - } - i += 1; - } - } - } + self.next_relationship_id = 0; + self.next_foreign_key_id = 0; + self.next_query_id = 0; + self.next_index_id = 0; + self.next_trigger_id = 0; + self.next_xdata_id = 0; + self.next_projection_id = 0; + self.next_storage_id = 0; let query = if is_rtn { routine_member_query() } else { - udl_class_member_query() + udl_class_query() }; { let mut capture_indices = HashMap::new(); if let Some(method_idx) = query.capture_index_for_name("classmethod") { capture_indices.insert(method_idx, MemberType::ClassMethodCall); } + if let Some(inherits_idx) = query.capture_index_for_name("inherits") { + capture_indices.insert(inherits_idx, MemberType::InheritedClass); + } + if let Some(keyword_idx) = query.capture_index_for_name("classkeyword") { + capture_indices.insert(keyword_idx, MemberType::ClassKeyword); + } if let Some(routine_idx) = query.capture_index_for_name("routinedef") { capture_indices.insert(routine_idx, MemberType::Routine); } @@ -399,6 +362,65 @@ impl Class { let capture = &query_match.captures[i]; if let Some(cap_type) = capture_indices.get(&capture.index) { match cap_type { + MemberType::ClassKeyword => { + if let Some(keyword_str) = + get_string_at_byte_range(content, capture.node.byte_range()) + { + let (not, keyword_name, values) = + get_keyword_and_value(keyword_str.as_str()); + if keyword_name == "procedureblock" { + if not { + self.is_procedure_block = false; + } + } else if keyword_name == "language" { + if let Some(value) = values.get(0).copied() { + if value == "tsql" { + self.default_language = Language::TSql; + } + } + } else if keyword_name == "inheritance" { + if let Some(value) = values.get(0).copied() { + if value == "right" { + self.inheritance_direction = + InheritanceDirection::Right; + } + if self.inheritance_direction + != old_inheritance_direction + { + inheritance_changed = true; + } + } + } else if keyword_name == "final" { + if !not { + self.is_final = true; + } + } + } + i += 1; + continue; + } + MemberType::InheritedClass => { + if let Some(inherited_cls_name) = + get_string_at_byte_range(content, capture.node.byte_range()) + { + let lsp_range = + ts_range_to_lsp_range(content, capture.node.range()); + inherited_classes.push((inherited_cls_name.clone(), lsp_range)); + // inherited_class_ranges.insert(inherited_cls_name.clone(), lsp_range); + if let Some((old_inherited_class, _)) = + self.inherited_classes.get(inherited_count) + { + if &inherited_cls_name != old_inherited_class { + inheritance_changed = true; + } + } else { + inheritance_changed = true; + } + } + inherited_count += 1; + i += 1; + continue; + } MemberType::Procedure => { let procedure_statement_node = capture.node; if let Some(( diff --git a/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs b/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs index 5cb1805..fc2f58e 100644 --- a/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs +++ b/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs @@ -716,9 +716,7 @@ impl GlobalSemanticModel { for (parent_name, parent_range) in &cls.inherited_classes { if let Some(&parent_id) = name_to_id.get(parent_name) { if let Some(&parent_idx) = id_to_idx.get(&parent_id) { - if entries[parent_idx].1.active - && entries[parent_idx].1.is_final != Some(true) - { + if entries[parent_idx].1.active && entries[parent_idx].1.is_final != true { children[parent_idx].push((*child_id, parent_range.clone())); } } diff --git a/objectscript-lsp/crates/objectscript-core/src/method.rs b/objectscript-lsp/crates/objectscript-core/src/method.rs index 069f656..9b5be11 100644 --- a/objectscript-lsp/crates/objectscript-core/src/method.rs +++ b/objectscript-lsp/crates/objectscript-core/src/method.rs @@ -293,7 +293,7 @@ impl Method { content: &str, scope_tree: &ScopeTree, variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, - class_is_procedure_block: Option, + class_is_procedure_block: bool, is_class_method: bool, method_range: Range, class_name: &str, @@ -397,9 +397,7 @@ impl Method { false } } else { - self.is_procedure_block - .unwrap_or(class_is_procedure_block.unwrap_or(true)) - == false + self.is_procedure_block.unwrap_or(class_is_procedure_block) == false || self.public_variables_declared.contains(variable_name) }; let var = Variable::new( @@ -487,7 +485,7 @@ impl Method { content: &str, scope_tree: &ScopeTree, variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, - class_is_procedure_block: Option, + class_is_procedure_block: bool, ) { { let query = class_method_argument_query(); @@ -560,11 +558,9 @@ impl Method { if let Some(var_name) = &var_name && let Some(var_range) = var_range { - let variable_is_public = self - .is_procedure_block - .unwrap_or(class_is_procedure_block.unwrap_or(true)) - == false - || self.public_variables_declared.contains(var_name); + let variable_is_public = + self.is_procedure_block.unwrap_or(class_is_procedure_block) == false + || self.public_variables_declared.contains(var_name); let var = Variable::new( var_name.clone(), arg_type, @@ -823,9 +819,9 @@ impl Method { method_type: MethodType, method_range: Range, public_variables_declared: HashSet, // only procedure passes this - class_is_final: Option, - old_class_is_final: Option, - class_is_procedure_block: Option, + class_is_final: bool, + old_class_is_final: bool, + class_is_procedure_block: bool, class_name: &str, ) -> ( bool, @@ -993,8 +989,8 @@ impl Method { &mut self, node: Node, content: &str, - class_is_final: Option, - old_class_is_final: Option, + class_is_final: bool, + old_class_is_final: bool, ) -> (bool, bool) { // reset keywords to default based on method type let mut is_final_changed = false; @@ -1020,7 +1016,7 @@ impl Method { { let (not, keyword_name, values) = get_keyword_and_value(keyword_str.as_str()); - if keyword_name == "final" && !class_is_final.unwrap_or(false) { + if keyword_name == "final" && !class_is_final { if not { self.is_final = Some(false); } else { @@ -1104,8 +1100,8 @@ impl Method { }; self.return_type = Some(typename); } - let old_final_keyword_res = old_is_final.unwrap_or(old_class_is_final.unwrap_or(false)); - let new_final_keyword = self.is_final.unwrap_or(class_is_final.unwrap_or(false)); + let old_final_keyword_res = old_is_final.unwrap_or(old_class_is_final); + let new_final_keyword = self.is_final.unwrap_or(class_is_final); if old_final_keyword_res != new_final_keyword { is_final_changed = true; } diff --git a/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs b/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs index 2d33a3a..4d2b239 100644 --- a/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs +++ b/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs @@ -95,6 +95,8 @@ pub enum MemberType { Keyword, Procedure, DottedStatementTag, + InheritedClass, + ClassKeyword, } pub struct IndexParsers { @@ -122,6 +124,12 @@ impl IndexParsers { } } +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum InheritanceDirection { + Left, + Right, +} + /// DFS visitation state. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum DfsState { @@ -277,17 +285,33 @@ pub struct Class { /// Direct parent classes in the `Extends` list. pub inherited_classes: Vec<(String, LspRange)>, /// Inheritance conflict resolution direction (`left`, or `right`, default is `left`). - pub inheritance_direction: Option, + pub inheritance_direction: InheritanceDirection, /// Optional ProcedureBlock default for this class; If defined, methods will inherit this keyword if they don't specify it themselves. - pub is_procedure_block: Option, + pub is_procedure_block: bool, /// Optional default Language keyword for this class. - pub default_language: Option, + pub default_language: Language, /// Stores method name -> MethodRef for each method in this class. pub methods: HashMap, /// Stores property name -> id for each property in this class. pub properties: HashMap, /// Stores parameter name -> id for each parameter in this class. pub parameters: HashMap, + /// Stores relationship name -> id for each relationship in this class. + pub relationships: HashMap, + /// Stores ForeignKey name -> id for each ForeignKey in this class. + pub foreignkeys: HashMap, + /// Stores query name -> id for each query in this class. + pub queries: HashMap, + /// Stores Index name -> id for each Index in this class. + pub indices: HashMap, + /// Stores trigger name -> id for each trigger in this class. + pub triggers: HashMap, + /// Stores projection name -> id for each Projection in this class. + pub projections: HashMap, + /// Stores Xdata name -> XdataRef for each Xdata member in this class. + pub xdata: HashMap, + /// Stores storage name -> StorageRef for each storage member in this class. + pub storage: HashMap, /// Whether this class entry is considered live/usable (e.g., false after removal). pub active: bool, /// Whether this representation is of a routine. @@ -304,7 +328,7 @@ pub struct Class { pub(crate) next_projection_id: usize, pub(crate) next_storage_id: usize, /// If true, this class and all of its members cannot be overwritten by subclasses. - pub is_final: Option, + pub is_final: bool, } /// Language keyword values supported for classes/methods. diff --git a/objectscript-lsp/crates/objectscript-core/src/workspace.rs b/objectscript-lsp/crates/objectscript-core/src/workspace.rs index 612b700..26621e6 100644 --- a/objectscript-lsp/crates/objectscript-core/src/workspace.rs +++ b/objectscript-lsp/crates/objectscript-core/src/workspace.rs @@ -9,8 +9,9 @@ use crate::global_semantic::GlobalSemanticModel; use crate::local_semantic::LocalSemanticModel; use crate::override_index::OverrideIndex; use crate::parse_structures::{ - Class, ClassId, FileType, Method, MethodRef, MethodType, Parameter, ParameterRef, Property, - PropertyRef, RefactorLevel, UnresolvedMethodRef, Variable, VariableDefType, VariableRef, + Class, ClassId, FileType, InheritanceDirection, Method, MethodRef, MethodType, Parameter, + ParameterRef, Property, PropertyRef, RefactorLevel, UnresolvedMethodRef, Variable, + VariableDefType, VariableRef, }; use crate::refactor::{ refactor_conditionals_in_document, refactor_for_statements, refactor_legacy_do_statements, @@ -288,7 +289,7 @@ impl<'a> BulkWorkspaceIndex<'a> { range, public_variables, class_is_final, - None, + false, class_is_procedure_block, &class_name, ); @@ -497,7 +498,7 @@ impl<'a> BulkWorkspaceIndex<'a> { }; data.global_semantic_model .get_class(parent_id) - .is_some_and(|parent| parent.is_final == Some(true)) + .is_some_and(|parent| parent.is_final == true) }) .cloned() .collect(); @@ -1286,7 +1287,7 @@ impl ProjectData { &class_name, class_id, &inherited_classes, - class_is_final.unwrap_or(false), + class_is_final, &mut classes_to_recompute_inheritance, &url, ); @@ -1327,7 +1328,7 @@ impl ProjectData { method_range, public_variables_declared, class_is_final, - None, + false, class_is_procedure_block, &class_name, ); @@ -1354,7 +1355,7 @@ impl ProjectData { method_range, public_variables_declared, class_is_final, - None, + false, class_is_procedure_block, &class_name, ); @@ -1377,7 +1378,7 @@ impl ProjectData { method_range, public_variables_declared, class_is_final, - None, + false, class_is_procedure_block, &class_name, ); @@ -1464,7 +1465,7 @@ impl ProjectData { method_name, method_ref, true, - class_is_final.unwrap_or(false), + class_is_final, ); } @@ -1549,7 +1550,7 @@ impl ProjectData { property_name, property_ref, true, - class_is_final.unwrap_or(false), + class_is_final, ); } for (parameter_name, (parameter, parameter_range, parameter_ref)) in parameters { @@ -1572,7 +1573,7 @@ impl ProjectData { parameter_name, parameter_ref, true, - class_is_final.unwrap_or(false), + class_is_final, ); } self.documents.insert(url.clone(), document); @@ -1614,7 +1615,7 @@ impl ProjectData { for (inherited_cls_name, inherited_class_ref_range) in inherited_classes { if let Some(inherited_class_id) = self.classes.get(inherited_cls_name).copied() { if let Some(cls) = self.global_semantic_model.get_class(&inherited_class_id) { - if cls.is_final == Some(true) { + if cls.is_final { let diagnostic = Diagnostic { range: inherited_class_ref_range.clone(), severity: Some(DiagnosticSeverity::WARNING), @@ -1969,7 +1970,6 @@ impl ProjectData { self.global_semantic_model.get_method(stale_method_ref) { let old_method_name = old_method.name.clone(); - let old_method_is_final = old_method.is_final; for (method_ref, method_call_range) in &method_caller_refs { let lsp_range = ts_range_to_lsp_range(content, *method_call_range); let Some(cls_sym) = self @@ -2002,10 +2002,8 @@ impl ProjectData { .extend(method_caller_refs); if let Some(new_class) = self.global_semantic_model.get_class(&class_id) && let Some(new_method_ref) = new_class.get_method_ref(&old_method_name) - && new_class - .is_final - .unwrap_or(old_method_is_final.unwrap_or(false)) { + // TODO: not sure if this should be resolved here.. self.resolve_unresolved_method( &(new_class_name.to_string(), old_method_name), *new_method_ref, @@ -2543,8 +2541,8 @@ impl ProjectData { &old_inherited_classes, &new_inherited_classes, inheritance_changed, - old_is_final.unwrap_or(false), - new_class_is_final.unwrap_or(false), + old_is_final, + new_class_is_final, &mut classes_to_fully_recompute_inheritance, &url, content, @@ -2565,8 +2563,8 @@ impl ProjectData { &old_class_name, &classes_to_fully_recompute_inheritance, &subclasses_to_recompute_inheritance, - new_class_is_final.unwrap_or(false), - old_is_final.unwrap_or(false), + new_class_is_final, + old_is_final, &mut methods_already_rebuilt, &mut scope_tree, content, @@ -2744,7 +2742,7 @@ impl ProjectData { method_name, method_ref, true, - new_class_is_final.unwrap_or(false), + new_class_is_final, ); } for (method_ref, unresolved_oref_methods) in unresolved_orefs { @@ -2832,7 +2830,7 @@ impl ProjectData { property_name, property_ref, true, - new_class_is_final.unwrap_or(false), + new_class_is_final, ); } @@ -2858,14 +2856,14 @@ impl ProjectData { parameter_name, parameter_ref, true, - new_class_is_final.unwrap_or(false), + new_class_is_final, ); } let scope_tree_snapshot = scope_tree.clone(); let mut curr_class_hash = HashSet::new(); curr_class_hash.insert(old_class_id); - if !new_class_is_final.unwrap_or(false) { + if !new_class_is_final { if let Some(dependents) = self .dependent_class_index .direct_subclasses @@ -2995,7 +2993,7 @@ impl ProjectData { method_name.clone(), method_ref, true, - new_class_is_final.unwrap_or(false), + new_class_is_final, ); } if method_is_public_changed { @@ -3061,7 +3059,7 @@ impl ProjectData { method_name.clone(), method_ref, false, - new_class_is_final.unwrap_or(false), + new_class_is_final, ); unresolved_orefs.insert(method_ref, unresolved_oref_method_refs); for (variable, variable_range, variable_dependencies, variable_scope_id) in @@ -3585,7 +3583,7 @@ impl ProjectData { && self .global_semantic_model .get_class(parent_id) - .is_some_and(|parent| parent.is_final != Some(true)) + .is_some_and(|parent| !parent.is_final) }) .count() }) @@ -3653,11 +3651,9 @@ impl ProjectData { self.classes .get(name) .and_then(|parent_id| self.global_semantic_model.get_class(parent_id)) - .is_some_and(|parent| parent.is_final != Some(true)) + .is_some_and(|parent| !parent.is_final) }; - let parent_names: Vec = if let Some(inheritance_direction) = - inheritance_direction - && inheritance_direction == "right" + let parent_names: Vec = if inheritance_direction == InheritanceDirection::Right { parents .iter() @@ -3968,9 +3964,7 @@ impl ProjectData { // Find inherited entry for this method name from parents let mut inherited_entry: Option<(PropertyRef, bool)> = None; - let parent_names: Vec = if let Some(inheritance_direction) = - inheritance_direction - && inheritance_direction == "right" + let parent_names: Vec = if inheritance_direction == InheritanceDirection::Right { parents.iter().rev().map(|(name, _)| name.clone()).collect() } else { @@ -4176,9 +4170,7 @@ impl ProjectData { // Find inherited entry for this method name from parents let mut inherited_entry: Option<(ParameterRef, bool)> = None; - let parent_names: Vec = if let Some(inheritance_direction) = - inheritance_direction - && inheritance_direction == "right" + let parent_names: Vec = if inheritance_direction == InheritanceDirection::Right { parents.iter().rev().map(|(name, _)| name.clone()).collect() } else { @@ -4381,9 +4373,7 @@ impl ProjectData { // Find inherited entry for this method name from parents let mut inherited_entry: Option<(MethodRef, bool)> = None; - let parent_names: Vec = if let Some(inheritance_direction) = - inheritance_direction - && inheritance_direction == "right" + let parent_names: Vec = if inheritance_direction == InheritanceDirection::Right { parents.iter().rev().map(|(name, _)| name.clone()).collect() } else { @@ -4873,7 +4863,7 @@ impl ProjectData { let cls_is_procedure_block = if let Some(c) = self.global_semantic_model.get_class(&method_ref.class) { - c.is_procedure_block.unwrap_or(true) + c.is_procedure_block } else { return false; }; diff --git a/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs b/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs index 742747f..7725ea4 100644 --- a/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs +++ b/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs @@ -1,6 +1,7 @@ use crate::document::Document; use crate::parse_structures::{ - CodeMode, Language, Method, MethodType, Parameter, Property, TypeName, VariableDefType, + CodeMode, InheritanceDirection, Language, Method, MethodType, Parameter, Property, TypeName, + VariableDefType, }; use crate::workspace::ProjectData; use rayon::prelude::*; @@ -79,10 +80,10 @@ pub struct ClassSnapshot { /// Parent order is significant for ObjectScript multiple inheritance. pub inherited_classes: Vec, pub unresolved_inherited_classes: BTreeSet, - pub inheritance_direction: Option, - pub procedure_block: Option, - pub language: Option, - pub final_keyword: Option, + pub inheritance_direction: InheritanceDirection, + pub procedure_block: bool, + pub language: Language, + pub final_keyword: bool, pub methods: BTreeMap, pub properties: BTreeMap, pub parameters: BTreeMap, @@ -124,10 +125,10 @@ pub struct ClassDiff { pub imports: Option>>, pub inherited_classes: Option>>, pub unresolved_inherited_classes: Option>>, - pub inheritance_direction: Option>>, - pub procedure_block: Option>>, - pub language: Option>>, - pub final_keyword: Option>>, + pub inheritance_direction: Option>, + pub procedure_block: Option>, + pub language: Option>, + pub final_keyword: Option>, pub methods: MemberChanges, pub properties: MemberChanges, pub parameters: MemberChanges, @@ -631,8 +632,8 @@ ClassMethod Calculate() As %String assert_eq!( diff.final_keyword, Some(ValueChange { - before: None, - after: Some(true), + before: false, + after: true, }) ); assert_eq!(diff.properties.added, vec!["Added"]); diff --git a/objectscript-lsp/src/test.rs b/objectscript-lsp/src/test.rs index d66d658..3bcc2f8 100644 --- a/objectscript-lsp/src/test.rs +++ b/objectscript-lsp/src/test.rs @@ -5,7 +5,7 @@ mod tests { advance_point, get_keyword_and_value, parse_line_ref, point_to_byte, position_to_point, }; use objectscript_core::parse_structures::{ - FileType, Language, MethodRef, RefactorLevel, VariableRef, + FileType, InheritanceDirection, Language, MethodRef, RefactorLevel, VariableRef, }; use objectscript_core::refactor::refactor_conditionals_in_document; use objectscript_core::workspace::{ProjectData, ProjectState}; @@ -542,9 +542,9 @@ mod tests { }; // eprintln!("CLASS: {:#?}", class); - assert_eq!(class.is_procedure_block, Some(false)); - assert_eq!(class.default_language, Some(Language::Objectscript)); - assert_eq!(class.inheritance_direction, Some("right".to_string())); + assert_eq!(class.is_procedure_block, false); + assert_eq!(class.default_language, Language::Objectscript); + assert_eq!(class.inheritance_direction, InheritanceDirection::Right); // get methods for (_, method_ref) in class.methods.clone() { let method = gsm.methods.get(&method_ref).unwrap(); @@ -1476,7 +1476,7 @@ Method Test() .global_semantic_model .get_class(child_right_id) .expect("class should exist"); - assert_eq!(class.inheritance_direction, Some("right".to_string())); + assert_eq!(class.inheritance_direction, InheritanceDirection::Right); assert!( !class.inherited_classes.is_empty(), "should have inherited classes" From 6f319e38288815ca1480c450b6e0289c2fe5bb18 Mon Sep 17 00:00:00 2001 From: Hannah Date: Thu, 1 Oct 2026 10:58:59 -0400 Subject: [PATCH 5/8] feat: support for all types of class members, and parellization of indexing --- objectscript-lsp/Cargo.toml | 6 +- objectscript-lsp/README.md | 6 +- .../crates/objectscript-core/Cargo.toml | 6 +- .../benches/update_document.rs | 8 + .../examples/cold_workspace_bench.rs | 8 + .../examples/update_bench.rs | 8 + .../crates/objectscript-core/src/class.rs | 1457 ++++++++++++----- .../crates/objectscript-core/src/common.rs | 364 +++- .../objectscript-core/src/foreignkey.rs | 85 + .../objectscript-core/src/global_semantic.rs | 205 ++- .../crates/objectscript-core/src/index.rs | 140 ++ .../crates/objectscript-core/src/lib.rs | 8 + .../objectscript-core/src/local_semantic.rs | 28 +- .../crates/objectscript-core/src/method.rs | 964 +++++------ .../crates/objectscript-core/src/parameter.rs | 126 +- .../objectscript-core/src/parse_structures.rs | 280 +++- .../objectscript-core/src/projection.rs | 57 + .../crates/objectscript-core/src/property.rs | 165 +- .../crates/objectscript-core/src/query.rs | 80 + .../objectscript-core/src/relationship.rs | 81 + .../objectscript-core/src/scope_structures.rs | 18 + .../objectscript-core/src/scope_tree.rs | 55 +- .../crates/objectscript-core/src/storage.rs | 20 + .../crates/objectscript-core/src/trigger.rs | 68 + .../crates/objectscript-core/src/workspace.rs | 423 ++++- .../objectscript-core/src/workspace_diff.rs | 140 +- .../crates/objectscript-core/src/xdata.rs | 51 + .../tests/class_comparison.rs | 1049 ++++++++++++ .../documentation/class-comparison.md | 313 ---- objectscript-lsp/src/test.rs | 175 +- 30 files changed, 4827 insertions(+), 1567 deletions(-) create mode 100644 objectscript-lsp/crates/objectscript-core/src/foreignkey.rs create mode 100644 objectscript-lsp/crates/objectscript-core/src/index.rs create mode 100644 objectscript-lsp/crates/objectscript-core/src/projection.rs create mode 100644 objectscript-lsp/crates/objectscript-core/src/query.rs create mode 100644 objectscript-lsp/crates/objectscript-core/src/relationship.rs create mode 100644 objectscript-lsp/crates/objectscript-core/src/storage.rs create mode 100644 objectscript-lsp/crates/objectscript-core/src/trigger.rs create mode 100644 objectscript-lsp/crates/objectscript-core/src/xdata.rs create mode 100644 objectscript-lsp/crates/objectscript-core/tests/class_comparison.rs delete mode 100644 objectscript-lsp/documentation/class-comparison.md diff --git a/objectscript-lsp/Cargo.toml b/objectscript-lsp/Cargo.toml index 482440d..1b4ae41 100644 --- a/objectscript-lsp/Cargo.toml +++ b/objectscript-lsp/Cargo.toml @@ -13,9 +13,9 @@ objectscript-core = { path = "crates/objectscript-core" } tower-lsp = "0.20.0" tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "fs"] } tree-sitter = "0.26.6" -tree-sitter-objectscript = "1.9.20" -tree-sitter-objectscript-routine = "1.9.20" -tree-sitter-objectscript-playground = "1.9.20" +tree-sitter-objectscript = "1.10.0" +tree-sitter-objectscript-routine = "1.10.0" +tree-sitter-objectscript-playground = "1.10.0" tree-sitter-xml = "0.7.0" serde = "1.0.228" serde_json = "1.0.145" diff --git a/objectscript-lsp/README.md b/objectscript-lsp/README.md index cfebb69..de5cf59 100644 --- a/objectscript-lsp/README.md +++ b/objectscript-lsp/README.md @@ -83,9 +83,9 @@ In this case, the `DependencyGraph` is used to determine all possible paths to t ## Grammar Baseline - `tree-sitter = 0.26.6` -- `tree-sitter-objectscript = 1.9.20` -- `tree-sitter-objectscript-routine = 1.9.20` -- `tree-sitter-objectscript-playground = 1.9.20` +- `tree-sitter-objectscript = 1.10.0` +- `tree-sitter-objectscript-routine = 1.10.0` +- `tree-sitter-objectscript-playground = 1.10.0` - `tree-sitter-xml = 0.7.0` ## Roadmap diff --git a/objectscript-lsp/crates/objectscript-core/Cargo.toml b/objectscript-lsp/crates/objectscript-core/Cargo.toml index fe8503d..4fb7540 100644 --- a/objectscript-lsp/crates/objectscript-core/Cargo.toml +++ b/objectscript-lsp/crates/objectscript-core/Cargo.toml @@ -18,9 +18,9 @@ required-features = ["update-bench"] tower-lsp = "0.20.0" tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "fs"] } tree-sitter = "0.26.6" -tree-sitter-objectscript = "1.9.20" -tree-sitter-objectscript-routine = "1.9.20" -tree-sitter-objectscript-playground = "1.9.20" +tree-sitter-objectscript = "1.10.0" +tree-sitter-objectscript-routine = "1.10.0" +tree-sitter-objectscript-playground = "1.10.0" tree-sitter-xml = "0.7.0" serde = "1.0.228" serde_json = "1.0.145" diff --git a/objectscript-lsp/crates/objectscript-core/benches/update_document.rs b/objectscript-lsp/crates/objectscript-core/benches/update_document.rs index f6783da..23f2392 100644 --- a/objectscript-lsp/crates/objectscript-core/benches/update_document.rs +++ b/objectscript-lsp/crates/objectscript-core/benches/update_document.rs @@ -195,6 +195,14 @@ fn build_project_data(prepared: &PreparedEdit) -> ProjectData { method_defs: HashMap::new(), property_defs: HashMap::new(), parameter_defs: HashMap::new(), + relationship_defs: HashMap::new(), + foreignkey_defs: HashMap::new(), + query_defs: HashMap::new(), + index_defs: HashMap::new(), + trigger_defs: HashMap::new(), + xdata_defs: HashMap::new(), + projection_defs: HashMap::new(), + storage_defs: HashMap::new(), pub_var_defs: HashMap::new(), override_index: OverrideIndex::new(), dependent_class_index: Dependents::new(), diff --git a/objectscript-lsp/crates/objectscript-core/examples/cold_workspace_bench.rs b/objectscript-lsp/crates/objectscript-core/examples/cold_workspace_bench.rs index a2d7403..ec01177 100644 --- a/objectscript-lsp/crates/objectscript-core/examples/cold_workspace_bench.rs +++ b/objectscript-lsp/crates/objectscript-core/examples/cold_workspace_bench.rs @@ -264,6 +264,14 @@ fn empty_project_data() -> ProjectData { pub_var_defs: HashMap::new(), parameter_defs: HashMap::new(), property_defs: HashMap::new(), + relationship_defs: HashMap::new(), + foreignkey_defs: HashMap::new(), + query_defs: HashMap::new(), + index_defs: HashMap::new(), + trigger_defs: HashMap::new(), + xdata_defs: HashMap::new(), + projection_defs: HashMap::new(), + storage_defs: HashMap::new(), override_index: OverrideIndex::new(), dependent_class_index: Dependents::new(), dependency_graph: DependencyGraph::new(), diff --git a/objectscript-lsp/crates/objectscript-core/examples/update_bench.rs b/objectscript-lsp/crates/objectscript-core/examples/update_bench.rs index 8f26a0b..0ee2374 100644 --- a/objectscript-lsp/crates/objectscript-core/examples/update_bench.rs +++ b/objectscript-lsp/crates/objectscript-core/examples/update_bench.rs @@ -180,6 +180,14 @@ fn build_project_data(prepared: &PreparedEdit) -> ProjectData { method_defs: HashMap::new(), property_defs: HashMap::new(), parameter_defs: HashMap::new(), + relationship_defs: HashMap::new(), + foreignkey_defs: HashMap::new(), + query_defs: HashMap::new(), + index_defs: HashMap::new(), + trigger_defs: HashMap::new(), + xdata_defs: HashMap::new(), + projection_defs: HashMap::new(), + storage_defs: HashMap::new(), pub_var_defs: HashMap::new(), override_index: OverrideIndex::new(), dependent_class_index: Dependents::new(), diff --git a/objectscript-lsp/crates/objectscript-core/src/class.rs b/objectscript-lsp/crates/objectscript-core/src/class.rs index 3c258f5..56973cc 100644 --- a/objectscript-lsp/crates/objectscript-core/src/class.rs +++ b/objectscript-lsp/crates/objectscript-core/src/class.rs @@ -1,30 +1,118 @@ use crate::common::{ - get_dotted_subroutine_info, get_keyword_and_value, get_node_children, get_parameter_name, - get_procedure_info, get_property_name, get_routine_method_range, get_string_at_byte_range, - get_subroutine_info, ts_range_to_lsp_range, + get_dotted_subroutine_info, get_keyword_and_value, get_node_children, get_procedure_info, + get_routine_method_range, get_string_at_byte_range, get_subroutine_info, ts_range_to_lsp_range, }; +use crate::foreignkey::build_foreignkey_struct; +use crate::index::build_index_struct; +use crate::method::build_method_struct; +use crate::parameter::build_parameter_struct; use crate::parse_structures::{ - Class, ClassId, InheritanceDirection, Language, MemberType, Method, MethodId, MethodRef, - MethodType, Parameter, ParameterId, ParameterRef, Property, PropertyId, PropertyRef, + Class, ClassId, ForeignKey, ForeignKeyId, ForeignKeyRef, Index, IndexId, IndexRef, + InheritanceDirection, Language, MemberType, Method, MethodId, MethodRef, MethodType, Parameter, + ParameterId, ParameterRef, Projection, ProjectionId, ProjectionRef, Property, PropertyId, + PropertyRef, Query, QueryId, QueryRef, Relationship, RelationshipId, RelationshipRef, Storage, + StorageId, StorageRef, Trigger, TriggerId, TriggerRef, XData, XdataId, XdataRef, }; +use crate::projection::build_projection_struct; +use crate::property::build_property_struct; +use crate::query::build_query_struct; +use crate::relationship::build_relationship_struct; +use crate::storage::build_storage_struct; +use crate::trigger::build_trigger_struct; +use crate::xdata::build_xdata_struct; +use rayon::prelude::*; use std::collections::{HashMap, HashSet}; use std::sync::OnceLock; use tower_lsp::lsp_types::{Diagnostic, DiagnosticSeverity, Range as LspRange}; use tree_sitter::{ - Language as TsLanguage, Node, Query, QueryCursor, Range, StreamingIterator, Tree, + Language as TsLanguage, Node, Query as TsQuery, QueryCursor, Range, StreamingIterator, Tree, }; use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; use tree_sitter_objectscript_routine::LANGUAGE_OBJECTSCRIPT_ROUTINE; -const UDL_CLASS_MEMBER_QUERY: &str = r#"(class_definition - (class_extends (class_name (identifier) @inherits)) ? - (class_keyword) @classkeyword - (class_body - (class_statement - [ +#[derive(Clone, Copy)] +struct MemberCapture<'tree> { + member_type: MemberType, + node: Node<'tree>, +} + +enum BuiltClassMember { + Method { + method: Method, + range: Range, + name_range: Range, + }, + Property(Property, Range), + Parameter(Parameter, Range), + Relationship(Relationship, Range), + ForeignKey(ForeignKey, Range), + Query(Query, Range), + Index(Index, Range), + Trigger(Trigger, Range), + XData(XData, Range), + Projection(Projection, Range), + Storage(Storage, Range), +} + +fn build_class_member(capture: MemberCapture<'_>, content: &str) -> Option { + let node = capture.node; + match capture.member_type { + MemberType::ClassMethodCall | MemberType::MethodDef | MemberType::ClientMethod => { + let method_type = match capture.member_type { + MemberType::ClassMethodCall => MethodType::ClassMethod, + MemberType::MethodDef => MethodType::InstanceMethod, + MemberType::ClientMethod => MethodType::ClientMethod, + _ => unreachable!(), + }; + let method = build_method_struct(node, method_type, content)?; + let name_range = node.named_child(0)?.named_child(0)?.range(); + Some(BuiltClassMember::Method { + method, + range: node.range(), + name_range, + }) + } + MemberType::RelativeProperty => build_property_struct(node, content) + .map(|value| BuiltClassMember::Property(value, node.range())), + MemberType::RelativeParameter => build_parameter_struct(node, content) + .map(|value| BuiltClassMember::Parameter(value, node.range())), + MemberType::Relationship => build_relationship_struct(node, content) + .map(|value| BuiltClassMember::Relationship(value, node.range())), + MemberType::Foreignkey => build_foreignkey_struct(node, content) + .map(|value| BuiltClassMember::ForeignKey(value, node.range())), + MemberType::Query => build_query_struct(node, content) + .map(|value| BuiltClassMember::Query(value, node.range())), + MemberType::Index => build_index_struct(node, content) + .map(|value| BuiltClassMember::Index(value, node.range())), + MemberType::Trigger => build_trigger_struct(node, content) + .map(|value| BuiltClassMember::Trigger(value, node.range())), + MemberType::Xdata => build_xdata_struct(node, content) + .map(|value| BuiltClassMember::XData(value, node.range())), + MemberType::Projection => build_projection_struct(node, content) + .map(|value| BuiltClassMember::Projection(value, node.range())), + MemberType::Storage => build_storage_struct(node, content) + .map(|value| BuiltClassMember::Storage(value, node.range())), + _ => None, + } +} + +const UDL_CLASS_MEMBER_QUERY: &str = r#" + (class_definition + (class_extends + (class_name + (identifier) @inherits))) + + (class_definition + (class_keyword) @classkeyword) + + (class_definition + (class_body + (class_statement + [ (method (method_definition) @method) (classmethod (method_definition) @classmethod) + (clientmethod) @clientmethod (parameter) @parameter (property) @property (relationship) @relationship @@ -35,9 +123,8 @@ const UDL_CLASS_MEMBER_QUERY: &str = r#"(class_definition (xdata) @xdata (projection) @projection (storage) @storage - ]) - ) - )"#; + ]))) +"#; const ROUTINE_MEMBER_QUERY: &str = r#" [(routine_definition) @routinedef ? @@ -47,29 +134,19 @@ const ROUTINE_MEMBER_QUERY: &str = r#" (statement (tag_statement)) @subroutine ?]"#; fn cached_query( - query: &'static OnceLock, + query: &'static OnceLock, language: TsLanguage, source: &str, name: &str, -) -> &'static Query { +) -> &'static TsQuery { query.get_or_init(|| { - Query::new(&language, source) + TsQuery::new(&language, source) .unwrap_or_else(|error| panic!("failed to compile {name} Tree-sitter query: {error}")) }) } -// fn udl_class_header_query() -> &'static Query { -// static QUERY: OnceLock = OnceLock::new(); -// cached_query( -// &QUERY, -// LANGUAGE_OBJECTSCRIPT_UDL.into(), -// UDL_CLASS_HEADER_QUERY, -// "UDL class header", -// ) -// } - -fn udl_class_query() -> &'static Query { - static QUERY: OnceLock = OnceLock::new(); +fn udl_class_query() -> &'static TsQuery { + static QUERY: OnceLock = OnceLock::new(); cached_query( &QUERY, LANGUAGE_OBJECTSCRIPT_UDL.into(), @@ -78,8 +155,8 @@ fn udl_class_query() -> &'static Query { ) } -fn routine_member_query() -> &'static Query { - static QUERY: OnceLock = OnceLock::new(); +fn routine_member_query() -> &'static TsQuery { + static QUERY: OnceLock = OnceLock::new(); cached_query( &QUERY, LANGUAGE_OBJECTSCRIPT_ROUTINE.into(), @@ -151,13 +228,52 @@ impl Class { self.methods = HashMap::new(); self.properties = HashMap::new(); self.parameters = HashMap::new(); + self.relationships = HashMap::new(); + self.foreignkeys = HashMap::new(); + self.queries = HashMap::new(); + self.indices = HashMap::new(); + self.triggers = HashMap::new(); + self.projections = HashMap::new(); + self.xdata = HashMap::new(); + self.storage = HashMap::new(); self.active = active; self.next_method_id = 0; self.next_parameter_id = 0; self.next_property_id = 0; + self.next_relationship_id = 0; + self.next_index_id = 0; + self.next_foreign_key_id = 0; + self.next_query_id = 0; + self.next_trigger_id = 0; + self.next_xdata_id = 0; + self.next_projection_id = 0; + self.next_storage_id = 0; self.is_final = false; } + pub fn clear_metadata_for_members_rebuilt(&mut self) { + self.next_parameter_id = 0; + self.next_property_id = 0; + self.next_relationship_id = 0; + self.next_index_id = 0; + self.next_foreign_key_id = 0; + self.next_query_id = 0; + self.next_trigger_id = 0; + self.next_xdata_id = 0; + self.next_projection_id = 0; + self.next_storage_id = 0; + self.properties.clear(); + self.parameters.clear(); + self.relationships.clear(); + self.foreignkeys.clear(); + self.queries.clear(); + self.indices.clear(); + self.triggers.clear(); + self.projections.clear(); + self.xdata.clear(); + self.storage.clear(); + } + /// Allocates and returns the next sequential method ID for this class. pub fn get_next_method_id(&mut self) -> usize { let id = self.next_method_id; @@ -276,6 +392,14 @@ impl Class { HashMap)>, // new methods HashMap, // new properties HashMap, // new parameters + HashMap, + HashMap, + HashMap, + HashMap, + HashMap, + HashMap, + HashMap, + HashMap, Vec<(String, LspRange)>, // new inherited classes HashMap)>, // all methods info Vec, @@ -288,6 +412,14 @@ impl Class { let mut new_methods = HashMap::new(); let mut new_properties = HashMap::new(); let mut new_parameters = HashMap::new(); + let mut new_relationships = HashMap::new(); + let mut new_foreignkeys = HashMap::new(); + let mut new_queries = HashMap::new(); + let mut new_indices = HashMap::new(); + let mut new_triggers = HashMap::new(); + let mut new_projections = HashMap::new(); + let mut new_xdata = HashMap::new(); + let mut new_storage = HashMap::new(); let mut all_methods = HashMap::new(); let mut diagnostics = Vec::new(); let mut old_methods: HashSet = self.methods.keys().cloned().collect(); @@ -297,7 +429,6 @@ impl Class { // NOTE: right now, properties and parameters are not incremental.. they are so small in terms of what it takes to rebuild that it doesn't make sense to incrementally build them atm self.properties.clear(); self.parameters.clear(); - self.properties.clear(); self.relationships.clear(); self.foreignkeys.clear(); self.queries.clear(); @@ -326,6 +457,9 @@ impl Class { if let Some(method_idx) = query.capture_index_for_name("classmethod") { capture_indices.insert(method_idx, MemberType::ClassMethodCall); } + if let Some(client_method_idx) = query.capture_index_for_name("clientmethod") { + capture_indices.insert(client_method_idx, MemberType::ClientMethod); + } if let Some(inherits_idx) = query.capture_index_for_name("inherits") { capture_indices.insert(inherits_idx, MemberType::InheritedClass); } @@ -350,37 +484,59 @@ impl Class { if let Some(prop_idx) = query.capture_index_for_name("property") { capture_indices.insert(prop_idx, MemberType::RelativeProperty); } - if let Some(prop_idx) = query.capture_index_for_name("dottedstatement") { - capture_indices.insert(prop_idx, MemberType::DottedStatementTag); + if let Some(relationship_idx) = query.capture_index_for_name("relationship") { + capture_indices.insert(relationship_idx, MemberType::Relationship); + } + if let Some(foreignkey_idx) = query.capture_index_for_name("foreignkey") { + capture_indices.insert(foreignkey_idx, MemberType::Foreignkey); + } + if let Some(query_idx) = query.capture_index_for_name("query") { + capture_indices.insert(query_idx, MemberType::Query); + } + if let Some(index_idx) = query.capture_index_for_name("index") { + capture_indices.insert(index_idx, MemberType::Index); + } + if let Some(trigger_idx) = query.capture_index_for_name("trigger") { + capture_indices.insert(trigger_idx, MemberType::Trigger); + } + if let Some(xdata_idx) = query.capture_index_for_name("xdata") { + capture_indices.insert(xdata_idx, MemberType::Xdata); + } + if let Some(projection_idx) = query.capture_index_for_name("projection") { + capture_indices.insert(projection_idx, MemberType::Projection); } + if let Some(storage_idx) = query.capture_index_for_name("storage") { + capture_indices.insert(storage_idx, MemberType::Storage); + } + let mut cursor = QueryCursor::new(); let mut iter = cursor.matches(query, root_node, content.as_bytes()); - while let Some(query_match) = iter.next() { - let mut i = 0; - while i < query_match.captures.len() { - let capture = &query_match.captures[i]; - if let Some(cap_type) = capture_indices.get(&capture.index) { - match cap_type { + if !is_rtn { + let mut member_captures = Vec::new(); + while let Some(query_match) = iter.next() { + for capture in query_match.captures { + let Some(member_type) = capture_indices.get(&capture.index).copied() else { + continue; + }; + match member_type { MemberType::ClassKeyword => { if let Some(keyword_str) = get_string_at_byte_range(content, capture.node.byte_range()) { let (not, keyword_name, values) = get_keyword_and_value(keyword_str.as_str()); - if keyword_name == "procedureblock" { - if not { + match keyword_name.as_str() { + "procedureblock" if not => { self.is_procedure_block = false; } - } else if keyword_name == "language" { - if let Some(value) = values.get(0).copied() { - if value == "tsql" { + "language" => { + if values.first().map(String::as_str) == Some("tsql") { self.default_language = Language::TSql; } } - } else if keyword_name == "inheritance" { - if let Some(value) = values.get(0).copied() { - if value == "right" { + "inheritance" => { + if values.first().map(String::as_str) == Some("right") { self.inheritance_direction = InheritanceDirection::Right; } @@ -390,446 +546,869 @@ impl Class { inheritance_changed = true; } } - } else if keyword_name == "final" { - if !not { + "final" if !not => { self.is_final = true; } + _ => {} } } - i += 1; - continue; } MemberType::InheritedClass => { - if let Some(inherited_cls_name) = + if let Some(inherited_class_name) = get_string_at_byte_range(content, capture.node.byte_range()) { let lsp_range = ts_range_to_lsp_range(content, capture.node.range()); - inherited_classes.push((inherited_cls_name.clone(), lsp_range)); - // inherited_class_ranges.insert(inherited_cls_name.clone(), lsp_range); - if let Some((old_inherited_class, _)) = - self.inherited_classes.get(inherited_count) + inherited_classes + .push((inherited_class_name.clone(), lsp_range)); + if self + .inherited_classes + .get(inherited_count) + .map(|(name, _)| name) + != Some(&inherited_class_name) { - if &inherited_cls_name != old_inherited_class { - inheritance_changed = true; - } - } else { inheritance_changed = true; } } inherited_count += 1; - i += 1; - continue; } - MemberType::Procedure => { - let procedure_statement_node = capture.node; - if let Some(( - method_name, - method_name_range, - method_range, - method_type, - public_variables_declared, - )) = get_procedure_info(&procedure_statement_node, content) - { - let existed = old_methods.remove(&method_name); - if all_methods.contains_key(&method_name) { - let lsp_range = - ts_range_to_lsp_range(content, method_name_range); - let diagnostic = Diagnostic { - range: lsp_range, - severity: Some(DiagnosticSeverity::ERROR), - code: None, - code_description: None, - source: Some("ObjectScript".to_string()), - message: format!( - "A Method named {:?} already exists in this class.", - &method_name - ), - related_information: None, - tags: None, - data: None, + MemberType::ClassMethodCall + | MemberType::ClientMethod + | MemberType::MethodDef + | MemberType::RelativeParameter + | MemberType::RelativeProperty + | MemberType::Relationship + | MemberType::Foreignkey + | MemberType::Query + | MemberType::Index + | MemberType::Trigger + | MemberType::Xdata + | MemberType::Projection + | MemberType::Storage => { + member_captures.push(MemberCapture { + member_type, + node: capture.node, + }); + } + _ => {} + } + } + } + + let built_members: Vec = member_captures + .into_par_iter() + .filter_map(|capture| build_class_member(capture, content)) + .collect(); + + for built_member in built_members { + match built_member { + BuiltClassMember::Method { + method, + range, + name_range, + } => { + let method_name = method.name.clone(); + let method_type = method.method_type; + let public_variables = method.public_variables_declared.clone(); + let existed = old_methods.remove(&method_name); + if all_methods.contains_key(&method_name) { + diagnostics.push(Diagnostic { + range: ts_range_to_lsp_range(content, name_range), + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("ObjectScript".to_string()), + message: format!( + "A Method named {:?} already exists in this class.", + &method_name + ), + related_information: None, + tags: None, + data: None, + }); + } + if !existed { + let method_ref = MethodRef { + id: MethodId(self.get_next_method_id()), + class: *class_id, + offset: None, + }; + self.methods.insert(method_name.clone(), method_ref); + new_methods.insert( + method_name.clone(), + (method, range, method_ref, public_variables.clone()), + ); + } + all_methods.insert(method_name, (range, method_type, public_variables)); + } + BuiltClassMember::Property(property, range) => { + let name = property.name.clone(); + let member_ref = PropertyRef { + id: PropertyId(self.get_next_property_id()), + class: *class_id, + }; + self.properties.insert(name.clone(), member_ref); + new_properties.insert(name, (property, range, member_ref)); + } + BuiltClassMember::Parameter(parameter, range) => { + let name = parameter.name.clone(); + let member_ref = ParameterRef { + id: ParameterId(self.get_next_parameter_id()), + class: *class_id, + }; + self.parameters.insert(name.clone(), member_ref); + new_parameters.insert(name, (parameter, range, member_ref)); + } + BuiltClassMember::Relationship(relationship, range) => { + let name = relationship.name.clone(); + let member_ref = RelationshipRef { + id: RelationshipId(self.get_next_relationship_id()), + class: *class_id, + }; + self.relationships.insert(name.clone(), member_ref); + new_relationships.insert(name, (relationship, range, member_ref)); + } + BuiltClassMember::ForeignKey(foreignkey, range) => { + let name = foreignkey.name.clone(); + let member_ref = ForeignKeyRef { + id: ForeignKeyId(self.get_next_foreignkey_id()), + class: *class_id, + }; + self.foreignkeys.insert(name.clone(), member_ref); + new_foreignkeys.insert(name, (foreignkey, range, member_ref)); + } + BuiltClassMember::Query(query, range) => { + let name = query.name.clone(); + let member_ref = QueryRef { + id: QueryId(self.get_next_query_id()), + class: *class_id, + }; + self.queries.insert(name.clone(), member_ref); + new_queries.insert(name, (query, range, member_ref)); + } + BuiltClassMember::Index(index, range) => { + let name = index.name.clone(); + let member_ref = IndexRef { + id: IndexId(self.get_next_index_id()), + class: *class_id, + }; + self.indices.insert(name.clone(), member_ref); + new_indices.insert(name, (index, range, member_ref)); + } + BuiltClassMember::Trigger(trigger, range) => { + let name = trigger.name.clone(); + let member_ref = TriggerRef { + id: TriggerId(self.get_next_trigger_id()), + class: *class_id, + }; + self.triggers.insert(name.clone(), member_ref); + new_triggers.insert(name, (trigger, range, member_ref)); + } + BuiltClassMember::XData(xdata, range) => { + let name = xdata.name.clone(); + let member_ref = XdataRef { + id: XdataId(self.get_next_xdata_id()), + class: *class_id, + }; + self.xdata.insert(name.clone(), member_ref); + new_xdata.insert(name, (xdata, range, member_ref)); + } + BuiltClassMember::Projection(projection, range) => { + let name = projection.name.clone(); + let member_ref = ProjectionRef { + id: ProjectionId(self.get_next_projection_id()), + class: *class_id, + }; + self.projections.insert(name.clone(), member_ref); + new_projections.insert(name, (projection, range, member_ref)); + } + BuiltClassMember::Storage(storage, range) => { + let name = storage.name.clone(); + let member_ref = StorageRef { + id: StorageId(self.get_next_storage_id()), + class: *class_id, + }; + self.storage.insert(name.clone(), member_ref); + new_storage.insert(name, (storage, range, member_ref)); + } + } + } + } else { + while let Some(query_match) = iter.next() { + let mut i = 0; + while i < query_match.captures.len() { + let capture = &query_match.captures[i]; + if let Some(cap_type) = capture_indices.get(&capture.index) { + match cap_type { + MemberType::Relationship => { + let relationship_node = capture.node; + if let Some(relationship) = + build_relationship_struct(relationship_node, content) + { + let new_relationship_id = self.get_next_relationship_id(); + let relationship_ref = RelationshipRef { + id: RelationshipId(new_relationship_id), + class: *class_id, }; - diagnostics.push(diagnostic); + let relationship_name = relationship.name.clone(); + self.relationships + .insert(relationship_name.clone(), relationship_ref); + new_relationships.insert( + relationship_name.clone(), + ( + relationship, + relationship_node.range(), + relationship_ref, + ), + ); } - if !existed { - { - let new_method_id = self.get_next_method_id(); - let method_ref = MethodRef { - id: MethodId(new_method_id), - class: *class_id, - offset: None, - }; - self.methods.insert(method_name.clone(), method_ref); - let method = Method::new( - method_name.clone(), - public_variables_declared.clone(), - method_type, - ); - new_methods.insert( - method_name.clone(), - ( - method, - method_range, - method_ref, - public_variables_declared.clone(), - ), - ); + i += 1; + continue; + } + MemberType::ClassKeyword => { + if let Some(keyword_str) = + get_string_at_byte_range(content, capture.node.byte_range()) + { + let (not, keyword_name, values) = + get_keyword_and_value(keyword_str.as_str()); + if keyword_name == "procedureblock" { + if not { + self.is_procedure_block = false; + } + } else if keyword_name == "language" { + if let Some(value) = values.first().map(String::as_str) + { + if value == "tsql" { + self.default_language = Language::TSql; + } + } + } else if keyword_name == "inheritance" { + if let Some(value) = values.first().map(String::as_str) + { + if value == "right" { + self.inheritance_direction = + InheritanceDirection::Right; + } + if self.inheritance_direction + != old_inheritance_direction + { + inheritance_changed = true; + } + } + } else if keyword_name == "final" { + if !not { + self.is_final = true; + } } } - all_methods.insert( - method_name, - (method_range, method_type, public_variables_declared), - ); + i += 1; + continue; } - i += 1; - continue; - } - MemberType::DottedStatementTag => { - let subroutine_statement_node = capture.node; - if let Some(( - method_name, - method_name_range, - method_range, - method_type, - )) = - get_dotted_subroutine_info(&subroutine_statement_node, content) - { - let existed = old_methods.remove(&method_name); - if all_methods.contains_key(&method_name) { + MemberType::InheritedClass => { + if let Some(inherited_cls_name) = + get_string_at_byte_range(content, capture.node.byte_range()) + { let lsp_range = - ts_range_to_lsp_range(content, method_name_range); - let diagnostic = Diagnostic { - range: lsp_range, - severity: Some(DiagnosticSeverity::ERROR), - code: None, - code_description: None, - source: Some("ObjectScript".to_string()), - message: format!( - "A Method named {:?} already exists in this class.", - &method_name - ), - related_information: None, - tags: None, - data: None, - }; - diagnostics.push(diagnostic); - } - if !existed { + ts_range_to_lsp_range(content, capture.node.range()); + inherited_classes + .push((inherited_cls_name.clone(), lsp_range)); + // inherited_class_ranges.insert(inherited_cls_name.clone(), lsp_range); + if let Some((old_inherited_class, _)) = + self.inherited_classes.get(inherited_count) { - let new_method_id = self.get_next_method_id(); - let method_ref = MethodRef { - id: MethodId(new_method_id), - class: *class_id, - offset: None, - }; - self.methods.insert(method_name.clone(), method_ref); - let method = Method::new( - method_name.clone(), - HashSet::new(), - method_type, - ); - new_methods.insert( - method_name.clone(), - (method, method_range, method_ref, HashSet::new()), - ); + if &inherited_cls_name != old_inherited_class { + inheritance_changed = true; + } + } else { + inheritance_changed = true; } } - all_methods.insert( - method_name, - (method_range, method_type, HashSet::new()), - ); + inherited_count += 1; + i += 1; + continue; } - i += 1; - continue; - } - MemberType::RoutineMethodCall => { - let subroutine_statement_node = capture.node; - if let Some(( - method_name, - method_name_range, - method_range, - method_type, - )) = get_subroutine_info(&subroutine_statement_node, content) - { - let existed = old_methods.remove(&method_name); - if all_methods.contains_key(&method_name) { - let lsp_range = - ts_range_to_lsp_range(content, method_name_range); - let diagnostic = Diagnostic { - range: lsp_range, - severity: Some(DiagnosticSeverity::ERROR), - code: None, - code_description: None, - source: Some("ObjectScript".to_string()), - message: format!( - "A Method named {:?} already exists in this class.", - &method_name - ), - related_information: None, - tags: None, - data: None, - }; - diagnostics.push(diagnostic); - } - if !existed { - { - let new_method_id = self.get_next_method_id(); - let method_ref = MethodRef { - id: MethodId(new_method_id), - class: *class_id, - offset: None, + MemberType::Procedure => { + let procedure_statement_node = capture.node; + if let Some(( + method_name, + method_name_range, + method_range, + method_type, + public_variables_declared, + )) = get_procedure_info(&procedure_statement_node, content) + { + let existed = old_methods.remove(&method_name); + if all_methods.contains_key(&method_name) { + let lsp_range = + ts_range_to_lsp_range(content, method_name_range); + let diagnostic = Diagnostic { + range: lsp_range, + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("ObjectScript".to_string()), + message: format!( + "A Method named {:?} already exists in this class.", + &method_name + ), + related_information: None, + tags: None, + data: None, }; - self.methods.insert(method_name.clone(), method_ref); - let method = Method::new( - method_name.clone(), - HashSet::new(), - method_type, - ); - new_methods.insert( - method_name.clone(), - (method, method_range, method_ref, HashSet::new()), - ); + diagnostics.push(diagnostic); } + if !existed { + { + let new_method_id = self.get_next_method_id(); + let method_ref = MethodRef { + id: MethodId(new_method_id), + class: *class_id, + offset: None, + }; + self.methods + .insert(method_name.clone(), method_ref); + let method = Method::new( + method_name.clone(), + public_variables_declared.clone(), + method_type, + ); + new_methods.insert( + method_name.clone(), + ( + method, + method_range, + method_ref, + public_variables_declared.clone(), + ), + ); + } + } + all_methods.insert( + method_name, + (method_range, method_type, public_variables_declared), + ); } - all_methods.insert( - method_name, - (method_range, method_type, HashSet::new()), - ); + i += 1; + continue; } - i += 1; - continue; - } - MemberType::Routine => { - let routine_node = capture.node; - if class_name != &self.name { - self.name = class_name.clone(); + MemberType::DottedStatementTag => { + let subroutine_statement_node = capture.node; + if let Some(( + method_name, + method_name_range, + method_range, + method_type, + )) = get_dotted_subroutine_info( + &subroutine_statement_node, + content, + ) { + let existed = old_methods.remove(&method_name); + if all_methods.contains_key(&method_name) { + let lsp_range = + ts_range_to_lsp_range(content, method_name_range); + let diagnostic = Diagnostic { + range: lsp_range, + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("ObjectScript".to_string()), + message: format!( + "A Method named {:?} already exists in this class.", + &method_name + ), + related_information: None, + tags: None, + data: None, + }; + diagnostics.push(diagnostic); + } + if !existed { + { + let new_method_id = self.get_next_method_id(); + let method_ref = MethodRef { + id: MethodId(new_method_id), + class: *class_id, + offset: None, + }; + self.methods + .insert(method_name.clone(), method_ref); + let method = Method::new( + method_name.clone(), + HashSet::new(), + method_type, + ); + new_methods.insert( + method_name.clone(), + ( + method, + method_range, + method_ref, + HashSet::new(), + ), + ); + } + } + all_methods.insert( + method_name, + (method_range, method_type, HashSet::new()), + ); + } + i += 1; + continue; } - if let Some(method_range) = get_routine_method_range( - &routine_node, - class_range.end_point, - class_range.end_byte, - ) { - let existed = old_methods.remove(class_name); - if !existed { - { - let new_method_id = self.get_next_method_id(); - let method_ref = MethodRef { - id: MethodId(new_method_id), - class: *class_id, - offset: None, + MemberType::RoutineMethodCall => { + let subroutine_statement_node = capture.node; + if let Some(( + method_name, + method_name_range, + method_range, + method_type, + )) = get_subroutine_info(&subroutine_statement_node, content) + { + let existed = old_methods.remove(&method_name); + if all_methods.contains_key(&method_name) { + let lsp_range = + ts_range_to_lsp_range(content, method_name_range); + let diagnostic = Diagnostic { + range: lsp_range, + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("ObjectScript".to_string()), + message: format!( + "A Method named {:?} already exists in this class.", + &method_name + ), + related_information: None, + tags: None, + data: None, }; - self.methods.insert(class_name.clone(), method_ref); - let method = Method::new( - class_name.clone(), - HashSet::new(), - MethodType::Routine, - ); - new_methods.insert( - class_name.clone(), - (method, method_range, method_ref, HashSet::new()), - ); + diagnostics.push(diagnostic); + } + if !existed { + { + let new_method_id = self.get_next_method_id(); + let method_ref = MethodRef { + id: MethodId(new_method_id), + class: *class_id, + offset: None, + }; + self.methods + .insert(method_name.clone(), method_ref); + let method = Method::new( + method_name.clone(), + HashSet::new(), + method_type, + ); + new_methods.insert( + method_name.clone(), + ( + method, + method_range, + method_ref, + HashSet::new(), + ), + ); + } } + all_methods.insert( + method_name, + (method_range, method_type, HashSet::new()), + ); } - all_methods.insert( - class_name.clone(), - (method_range, MethodType::Routine, HashSet::new()), - ); + i += 1; + continue; } - - i += 1; - continue; - } - MemberType::ClassMethodCall => { - let method_definition_capture = capture.node; - if let Some(method_name_outer) = - method_definition_capture.named_child(0) - && let Some(method_name_node) = method_name_outer.named_child(0) - && let Some(method_name) = get_string_at_byte_range( - content, - method_name_node.byte_range(), - ) - { - let existed = old_methods.remove(&method_name); - if all_methods.contains_key(&method_name) { - let lsp_range = ts_range_to_lsp_range( - content, - method_name_node.range(), + MemberType::Routine => { + let routine_node = capture.node; + if class_name != &self.name { + self.name = class_name.clone(); + } + if let Some(method_range) = get_routine_method_range( + &routine_node, + class_range.end_point, + class_range.end_byte, + ) { + let existed = old_methods.remove(class_name); + if !existed { + { + let new_method_id = self.get_next_method_id(); + let method_ref = MethodRef { + id: MethodId(new_method_id), + class: *class_id, + offset: None, + }; + self.methods.insert(class_name.clone(), method_ref); + let method = Method::new( + class_name.clone(), + HashSet::new(), + MethodType::Routine, + ); + new_methods.insert( + class_name.clone(), + ( + method, + method_range, + method_ref, + HashSet::new(), + ), + ); + } + } + all_methods.insert( + class_name.clone(), + (method_range, MethodType::Routine, HashSet::new()), ); - let diagnostic = Diagnostic { - range: lsp_range, - severity: Some(DiagnosticSeverity::ERROR), - code: None, - code_description: None, - source: Some("ObjectScript".to_string()), - message: format!( - "A Method named {:?} already exists in this class.", - &method_name - ), - related_information: None, - tags: None, - data: None, - }; - diagnostics.push(diagnostic); } - if !existed { - { - let new_method_id = self.get_next_method_id(); - let method_ref = MethodRef { - id: MethodId(new_method_id), - class: *class_id, - offset: None, + + i += 1; + continue; + } + MemberType::ClassMethodCall => { + let method_definition_capture = capture.node; + if let Some(method_name_outer) = + method_definition_capture.named_child(0) + && let Some(method_name_node) = + method_name_outer.named_child(0) + && let Some(method_name) = get_string_at_byte_range( + content, + method_name_node.byte_range(), + ) + { + let existed = old_methods.remove(&method_name); + if all_methods.contains_key(&method_name) { + let lsp_range = ts_range_to_lsp_range( + content, + method_name_node.range(), + ); + let diagnostic = Diagnostic { + range: lsp_range, + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("ObjectScript".to_string()), + message: format!( + "A Method named {:?} already exists in this class.", + &method_name + ), + related_information: None, + tags: None, + data: None, }; - self.methods.insert(method_name.clone(), method_ref); - let method = Method::new( - method_name.clone(), - HashSet::new(), + diagnostics.push(diagnostic); + } + if !existed { + { + let new_method_id = self.get_next_method_id(); + let method_ref = MethodRef { + id: MethodId(new_method_id), + class: *class_id, + offset: None, + }; + self.methods + .insert(method_name.clone(), method_ref); + let method = build_method_struct( + method_definition_capture, + MethodType::ClassMethod, + content, + ) + .unwrap_or_else(|| { + Method::new( + method_name.clone(), + HashSet::new(), + MethodType::ClassMethod, + ) + }); + new_methods.insert( + method_name.clone(), + ( + method, + method_definition_capture.range(), + method_ref, + HashSet::new(), + ), + ); + } + } + all_methods.insert( + method_name, + ( + method_definition_capture.range(), MethodType::ClassMethod, + HashSet::new(), + ), + ); + } + i += 1; + continue; + } + MemberType::MethodDef => { + let method_definition_capture = capture.node; + if let Some(method_name_outer) = + method_definition_capture.named_child(0) + && let Some(method_name_node) = + method_name_outer.named_child(0) + && let Some(method_name) = get_string_at_byte_range( + content, + method_name_node.byte_range(), + ) + { + let existed = old_methods.remove(&method_name); + if all_methods.contains_key(&method_name) { + let lsp_range = ts_range_to_lsp_range( + content, + method_name_node.range(), ); - new_methods.insert( - method_name.clone(), - ( - method, - method_definition_capture.range(), - method_ref, - HashSet::new(), + let diagnostic = Diagnostic { + range: lsp_range, + severity: Some(DiagnosticSeverity::ERROR), + code: None, + code_description: None, + source: Some("ObjectScript".to_string()), + message: format!( + "A Method named {:?} already exists in this class.", + &method_name ), - ); + related_information: None, + tags: None, + data: None, + }; + diagnostics.push(diagnostic); } + if !existed { + { + let new_method_id = self.get_next_method_id(); + let method_ref = MethodRef { + id: MethodId(new_method_id), + class: *class_id, + offset: None, + }; + self.methods + .insert(method_name.clone(), method_ref); + let method = build_method_struct( + method_definition_capture, + MethodType::InstanceMethod, + content, + ) + .unwrap_or_else(|| { + Method::new( + method_name.clone(), + HashSet::new(), + MethodType::InstanceMethod, + ) + }); + new_methods.insert( + method_name.clone(), + ( + method, + method_definition_capture.range(), + method_ref, + HashSet::new(), + ), + ); + } + } + all_methods.insert( + method_name, + ( + method_definition_capture.range(), + MethodType::InstanceMethod, + HashSet::new(), + ), + ); } - all_methods.insert( - method_name, - ( - method_definition_capture.range(), - MethodType::ClassMethod, - HashSet::new(), - ), - ); + i += 1; + continue; } - i += 1; - continue; - } - MemberType::MethodDef => { - let method_definition_capture = capture.node; - if let Some(method_name_outer) = - method_definition_capture.named_child(0) - && let Some(method_name_node) = method_name_outer.named_child(0) - && let Some(method_name) = get_string_at_byte_range( - content, - method_name_node.byte_range(), - ) - { - let existed = old_methods.remove(&method_name); - if all_methods.contains_key(&method_name) { - let lsp_range = ts_range_to_lsp_range( - content, - method_name_node.range(), + MemberType::RelativeProperty => { + let property_node = capture.node; + if let Some(property) = + build_property_struct(property_node, content) + { + let new_property_id = self.get_next_property_id(); + let property_ref = PropertyRef { + id: PropertyId(new_property_id), + class: *class_id, + }; + let property_name = property.name.clone(); + self.properties.insert(property_name.clone(), property_ref); + new_properties.insert( + property_name.clone(), + (property, property_node.range(), property_ref), ); - let diagnostic = Diagnostic { - range: lsp_range, - severity: Some(DiagnosticSeverity::ERROR), - code: None, - code_description: None, - source: Some("ObjectScript".to_string()), - message: format!( - "A Method named {:?} already exists in this class.", - &method_name - ), - related_information: None, - tags: None, - data: None, + } + i += 1; + continue; + } + MemberType::Trigger => { + let trigger_node = capture.node; + if let Some(trigger) = + build_trigger_struct(trigger_node, content) + { + let new_trigger_id = self.get_next_trigger_id(); + let trigger_ref = TriggerRef { + id: TriggerId(new_trigger_id), + class: *class_id, }; - diagnostics.push(diagnostic); + let trigger_name = trigger.name.clone(); + self.triggers.insert(trigger_name.clone(), trigger_ref); + new_triggers.insert( + trigger_name.clone(), + (trigger, trigger_node.range(), trigger_ref), + ); } - if !existed { - { - let new_method_id = self.get_next_method_id(); - let method_ref = MethodRef { - id: MethodId(new_method_id), - class: *class_id, - offset: None, - }; - self.methods.insert(method_name.clone(), method_ref); - let method = Method::new( - method_name.clone(), - HashSet::new(), - MethodType::InstanceMethod, - ); - new_methods.insert( - method_name.clone(), - ( - method, - method_definition_capture.range(), - method_ref, - HashSet::new(), - ), - ); - } + i += 1; + continue; + } + MemberType::Storage => { + let storage_node = capture.node; + if let Some(storage) = + build_storage_struct(storage_node, content) + { + let new_storage_id = self.get_next_storage_id(); + let storage_ref = StorageRef { + id: StorageId(new_storage_id), + class: *class_id, + }; + let storage_name = storage.name.clone(); + self.storage.insert(storage_name.clone(), storage_ref); + new_storage.insert( + storage_name.clone(), + (storage, storage_node.range(), storage_ref), + ); } - all_methods.insert( - method_name, - ( - method_definition_capture.range(), - MethodType::InstanceMethod, - HashSet::new(), - ), - ); + i += 1; + continue; } - i += 1; - continue; - } - MemberType::RelativeProperty => { - let property_node = capture.node; - if let Some(property_name) = - get_property_name(&property_node, content) - { - let new_property_id = self.get_next_property_id(); - let property_ref = PropertyRef { - id: PropertyId(new_property_id), - class: *class_id, - }; - self.properties.insert(property_name.clone(), property_ref); - let mut property = Property::new(property_name.clone()); - property.build_keywords(property_node, content, None, None); - new_properties.insert( - property_name.clone(), - (property, property_node.range(), property_ref), - ); + MemberType::Index => { + let index_node = capture.node; + if let Some(index) = build_index_struct(index_node, content) { + let new_index_id = self.get_next_index_id(); + let index_ref = IndexRef { + id: IndexId(new_index_id), + class: *class_id, + }; + let index_name = index.name.clone(); + self.indices.insert(index_name.clone(), index_ref); + new_indices.insert( + index_name.clone(), + (index, index_node.range(), index_ref), + ); + } + i += 1; + continue; } - i += 1; - continue; - } - MemberType::RelativeParameter => { - let parameter_node = capture.node; - if let Some(parameter_name) = - get_parameter_name(¶meter_node, content) - { - let new_parameter_id = self.get_next_parameter_id(); - let parameter_ref = ParameterRef { - id: ParameterId(new_parameter_id), - class: *class_id, - }; - self.parameters - .insert(parameter_name.clone(), parameter_ref); - - let mut parameter = Parameter::new(parameter_name.clone()); - parameter.build_keywords(parameter_node, content, None, None); - new_parameters.insert( - parameter_name.clone(), - (parameter, parameter_node.range(), parameter_ref), - ); + MemberType::Projection => { + let projection_node = capture.node; + if let Some(projection) = + build_projection_struct(projection_node, content) + { + let new_projection_id = self.get_next_projection_id(); + let projection_ref = ProjectionRef { + id: ProjectionId(new_projection_id), + class: *class_id, + }; + let projection_name = projection.name.clone(); + self.projections + .insert(projection_name.clone(), projection_ref); + new_projections.insert( + projection_name.clone(), + (projection, projection_node.range(), projection_ref), + ); + } + i += 1; + continue; + } + MemberType::Foreignkey => { + let foreignkey_node = capture.node; + if let Some(foreignkey) = + build_foreignkey_struct(foreignkey_node, content) + { + let new_foreignkey_id = self.get_next_foreignkey_id(); + let foreignkey_ref = ForeignKeyRef { + id: ForeignKeyId(new_foreignkey_id), + class: *class_id, + }; + let foreignkey_name = foreignkey.name.clone(); + self.foreignkeys + .insert(foreignkey_name.clone(), foreignkey_ref); + new_foreignkeys.insert( + foreignkey_name.clone(), + (foreignkey, foreignkey_node.range(), foreignkey_ref), + ); + } + i += 1; + continue; + } + MemberType::Query => { + let query_node = capture.node; + if let Some(query) = build_query_struct(query_node, content) { + let new_query_id = self.get_next_query_id(); + let query_ref = QueryRef { + id: QueryId(new_query_id), + class: *class_id, + }; + let query_name = query.name.clone(); + self.queries.insert(query_name.clone(), query_ref); + new_queries.insert( + query_name.clone(), + (query, query_node.range(), query_ref), + ); + } + i += 1; + continue; + } + MemberType::Xdata => { + let xdata_node = capture.node; + if let Some(xdata) = build_xdata_struct(xdata_node, content) { + let new_xdata_id = self.get_next_xdata_id(); + let xdata_ref = XdataRef { + id: XdataId(new_xdata_id), + class: *class_id, + }; + let xdata_name = xdata.name.clone(); + self.xdata.insert(xdata_name.clone(), xdata_ref); + new_xdata.insert( + xdata_name.clone(), + (xdata, xdata_node.range(), xdata_ref), + ); + } + i += 1; + continue; + } + MemberType::RelativeParameter => { + let parameter_node = capture.node; + if let Some(parameter) = + build_parameter_struct(parameter_node, content) + { + let new_parameter_id = self.get_next_parameter_id(); + let parameter_ref = ParameterRef { + id: ParameterId(new_parameter_id), + class: *class_id, + }; + self.parameters + .insert(parameter.name.clone(), parameter_ref); + new_parameters.insert( + parameter.name.clone(), + (parameter, parameter_node.range(), parameter_ref), + ); + } + i += 1; + continue; + } + _ => { + i += 1; + continue; } - i += 1; - continue; - } - _ => { - i += 1; - continue; } } + eprintln!( + "error: didn't match type, but node is {:?} and class is {:?}", + capture.node, class_name + ); + i += 1; + continue; } - eprintln!("error: didn't match type, but node is {:?}", capture.node); - i += 1; - continue; } } } @@ -841,6 +1420,14 @@ impl Class { new_methods, new_properties, new_parameters, + new_relationships, + new_foreignkeys, + new_queries, + new_indices, + new_triggers, + new_xdata, + new_projections, + new_storage, inherited_classes, all_methods, diagnostics, diff --git a/objectscript-lsp/crates/objectscript-core/src/common.rs b/objectscript-lsp/crates/objectscript-core/src/common.rs index 3572692..6103e3c 100644 --- a/objectscript-lsp/crates/objectscript-core/src/common.rs +++ b/objectscript-lsp/crates/objectscript-core/src/common.rs @@ -1,4 +1,7 @@ -use crate::parse_structures::{MemberType, MethodType, ReturnType}; +use crate::parse_structures::{ + Argument, Cardinality, CodeMode, ForeignKeyAction, IndexType, Language, MemberType, MethodType, + ReturnType, TrackedKeywords, TriggerFire, TriggerForEach, TypeName, +}; use crate::refactor::count_leading_dots_in_line; use crate::scope_structures::ScopeId; use crate::scope_tree::ScopeTree; @@ -70,6 +73,336 @@ fn xml_objectscript_injections_query() -> &'static Query { ) } +pub fn get_tracked_keywords(keywords_node: Node, content: &str) -> TrackedKeywords { + let mut is_final = None; + let mut is_public = true; + let mut is_required = false; + let mut multidimensional = false; + let mut on_update = ForeignKeyAction::NoAction; + let mut on_delete = ForeignKeyAction::NoAction; + let mut inverse = None; + let mut cardinality = None; + let mut requires = Vec::new(); + let mut index_type = IndexType::Index; + let mut trigger_insert = false; + let mut trigger_update = false; + let mut trigger_delete = false; + let mut trigger_time = TriggerFire::BEFORE; + let mut trigger_for_each = TriggerForEach::Row; + let mut language = None; + let mut code_mode = CodeMode::Code; + let mut procedure_block = None; + let mut public_variables_declared = HashSet::new(); + fn collect_keyword_leaves<'tree>(node: Node<'tree>, leaves: &mut Vec>) { + let keyword_children: Vec<_> = get_node_children(node) + .into_iter() + .filter(|child| child.kind().contains("keyword")) + .collect(); + if keyword_children.is_empty() { + leaves.push(node); + } else { + for child in keyword_children { + collect_keyword_leaves(child, leaves); + } + } + } + + let mut keyword_children = Vec::new(); + for child in get_node_children(keywords_node) { + if child.kind().contains("keyword") { + collect_keyword_leaves(child, &mut keyword_children); + } + } + for keyword in keyword_children { + if let Some(keyword_str) = get_string_at_byte_range(content, keyword.byte_range()) { + let (not, keyword_name, keyword_value) = get_keyword_and_value(keyword_str.as_str()); + if keyword_name == "final" { + if not { + is_final = Some(false); + } else { + is_final = Some(true); + } + } + if keyword_name == "notinheritable" { + is_final = Some(true); + } else if keyword_name == "inverse" { + let Some(inverse_value) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: inverse keyword should have a value"); + continue; + }; + inverse = Some(inverse_value.to_string()); + } else if keyword_name == "cardinality" { + let Some(cardinality_value) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: cardinality keyword should have a value"); + continue; + }; + cardinality = match cardinality_value { + "one" => Some(Cardinality::One), + "many" => Some(Cardinality::Many), + "children" => Some(Cardinality::Children), + "parent" => Some(Cardinality::Parent), + _ => None, + }; + } else if keyword_name == "private" { + if not { + is_public = true; + } else { + is_public = false; + } + } else if keyword_name == "required" { + if not { + is_required = true; + } else { + is_required = false; + } + } else if keyword_name == "multidimensional" { + if not { + multidimensional = true; + } else { + multidimensional = false; + } + } else if keyword_name == "ondelete" { + let Some(foreignkeyaction) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: cardinality keyword should have a value"); + continue; + }; + on_delete = match foreignkeyaction { + "setdefault" => ForeignKeyAction::SetDefault, + "noaction" => ForeignKeyAction::NoAction, + "setnull" => ForeignKeyAction::SetNull, + "cascade" => ForeignKeyAction::Cascade, + _ => continue, + }; + } else if keyword_name == "onupdate" { + let Some(foreignkeyaction) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: cardinality keyword should have a value"); + continue; + }; + on_update = match foreignkeyaction.to_lowercase().as_str() { + "setdefault" => ForeignKeyAction::SetDefault, + "noaction" => ForeignKeyAction::NoAction, + "setnull" => ForeignKeyAction::SetNull, + "cascade" => ForeignKeyAction::Cascade, + _ => continue, + }; + } else if keyword_name == "requires" { + for val in keyword_value { + requires.push(val.to_string()); + } + } else if keyword_name == "type" { + let Some(index_type_str) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: index type keyword should have a value"); + continue; + }; + index_type = match index_type_str { + "bitmap" => IndexType::Bitmap, + "bitslice" => IndexType::Bitslice, + "collatedkey" => IndexType::CollatedKey, + "columnar" => IndexType::Columnar, + "key" => IndexType::Key, + _ => continue, + }; + } else if keyword_name == "language" { + let Some(language_type_str) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: language type keyword should have a value"); + continue; + }; + language = match language_type_str.to_lowercase().as_str() { + "basic" => Some(Language::Basic), + "javascript" => Some(Language::JavaScript), + "ispl" => Some(Language::ISpl), + "tsql" => Some(Language::TSql), + "python" => Some(Language::Python), + "objectscript" => Some(Language::Objectscript), + _ => continue, + }; + } else if keyword_name == "codemode" { + let Some(codemode_type_str) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: language type keyword should have a value"); + continue; + }; + code_mode = match codemode_type_str { + "call" => CodeMode::Call, + "expression" => CodeMode::Expression, + "objectgenerator" | "generator" => CodeMode::ObjectGenerator, + _ => continue, + }; + } else if keyword_name == "procedureblock" { + if let Some(value) = keyword_value.first().map(String::as_str) { + if value == "1" { + procedure_block = Some(true); + } else if value == "0" { + procedure_block = Some(false); + } + } else { + procedure_block = Some(true); + } + } else if keyword_name == "publiclist" { + for variable in keyword_value { + public_variables_declared.insert(variable.to_string()); + } + } else if keyword_name == "event" { + let Some(event_str) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: index event keyword should have a value"); + continue; + }; + (trigger_insert, trigger_update, trigger_delete) = match event_str { + "delete" => (false, false, true), + "insert" => (true, false, false), + "update" => (false, true, false), + "insert/update" => (true, true, false), + "insert/delete" => (true, false, true), + "insert/update/delete" => (true, true, true), + _ => { + eprintln!( + "Error: failed to parse trigger event keyword {:?}", + event_str + ); + continue; + } + }; + } else if keyword_name == "time" { + let Some(time_str) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: index time keyword should have a value"); + continue; + }; + trigger_time = match time_str { + "after" => TriggerFire::AFTER, + _ => { + continue; + } + }; + } else if keyword_name == "foreach" { + let Some(foreach_str) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: index foreach keyword should have a value"); + continue; + }; + trigger_for_each = match foreach_str { + "row/object" => TriggerForEach::RowObject, + "statement" => TriggerForEach::Statement, + _ => { + continue; + } + }; + } else if keyword_name == "mimetype" { + let Some(mimetype_str) = keyword_value.first().map(String::as_str) else { + eprintln!("Error: mimetype keyword should have a value"); + continue; + }; + language = match mimetype_str { + "text/x-python" + | "\"text/x-python\"" + | "application/python" + | "\"application/python\"" => Some(Language::Python), + "text/html" | "\"text/html\"" => Some(Language::Html), + "text/markdown" | "\"text/markdown\"" => Some(Language::Markdown), + "text/css" | "\"text/css\"" => Some(Language::Css), + "application/sql" => Some(Language::Sql), + "text/yaml" | "\"text/yaml\"" | "application/yaml" | "\"application/yaml\"" => { + Some(Language::Yaml) + } + "application/json" | "\"application/json\"" => Some(Language::Json), + _ => { + continue; + } + }; + } + } + } + TrackedKeywords { + is_final, + is_public, + is_required, + multidimensional, + inverse, + cardinality, + on_delete, + on_update, + requires, + index_type, + trigger_insert, + trigger_delete, + trigger_update, + trigger_time, + trigger_for_each, + language, + code_mode, + procedure_block, + public_variables_declared, + } +} + +pub fn build_argument(argument_node: Node, content: &str) -> Option<(Argument, Range)> { + let mut argument = Argument::default(); + let mut argument_range = argument_node.range(); + let argument_children = get_node_children(argument_node); + for argument_child in argument_children { + match argument_child.kind() { + "keyword_byref" => { + argument.byref = true; + } + "keyword_output" => { + argument.output = true; + } + "method_arg" => { + if let Some(method_arg_type) = argument_child.named_child(0) { + if let Some(variable_name_node) = method_arg_type.named_child(0) + && let Some(var_name) = + get_string_at_byte_range(content, variable_name_node.byte_range()) + { + argument.name = var_name; + argument_range = variable_name_node.range(); + }; + } else { + eprintln!( + "Error: Method arg node should have named children. This node didn't {:?}", + argument_child.kind() + ); + return None; + } + } + "return_type" => { + argument.return_type = parse_return_type(argument_child, content); + } + "default_argument_value" => { + if let Some(value) = argument_child.named_child(0) { + argument.default_value = get_string_at_byte_range(content, value.byte_range()); + } + } + _ => { + eprintln!( + "Error: Unexpected child node of type {:?} in argument node", + argument_child.kind(), + ); + continue; + } + } + } + Some((argument, argument_range)) +} + +pub fn parse_return_type(return_type: Node, content: &str) -> Option { + fn collect_identifiers(node: Node, content: &str, identifiers: &mut Vec) { + if node.kind() == "identifier" { + if let Some(identifier) = get_string_at_byte_range(content, node.byte_range()) { + identifiers.push(identifier); + } + return; + } + for child in get_node_children(node) { + collect_identifiers(child, content, identifiers); + } + } + + let mut identifiers = Vec::new(); + collect_identifiers(return_type, content, &mut identifiers); + let return_type_name = identifiers.first()?.clone(); + Some(TypeName { + ret_type: find_return_type(return_type_name), + parameters: identifiers.into_iter().skip(1).collect(), + }) +} + /// Logs override resolution results for a method/superclass pair for debugging. pub fn print_statements_exit_method_overrides_fn( method_name: &str, @@ -582,12 +915,12 @@ pub fn find_var_dependencies( /// Given a *_keyword string, returns: /// bool: true if not is before the keyword -/// String: the keyword name -/// Option: the value of the keyword (if one exists) -pub fn get_keyword_and_value(keyword: &str) -> (bool, String, Vec<&str>) { +/// String: the lowercased keyword name +/// Vec: the lowercased values of the keyword (empty if none) +pub fn get_keyword_and_value(keyword: &str) -> (bool, String, Vec) { let mut not = false; let mut keyword_name = "".to_string(); - let mut keyword_value: Vec<&str> = Vec::new(); + let mut keyword_value: Vec = Vec::new(); // splits string by spaces or equal sign static KEYWORD_PART: OnceLock = OnceLock::new(); let regex = KEYWORD_PART.get_or_init(|| Regex::new(r"[^\s=,()]+").unwrap()); @@ -599,9 +932,9 @@ pub fn get_keyword_and_value(keyword: &str) -> (bool, String, Vec<&str>) { if normalized_str == "not" { not = true; } else if count > 1 && !not { - keyword_value.push(value); + keyword_value.push(normalized_str); } else { - keyword_name = normalized_str.to_string(); + keyword_name = normalized_str; } } (not, keyword_name, keyword_value) @@ -848,23 +1181,6 @@ pub fn get_routine_method_range(node: &Node, end_point: Point, end_byte: usize) return Some(routine_range); } -/// Given a property node, get the name -pub fn get_property_name(node: &Node, content: &str) -> Option { - let Some(property_name_node_outer) = node.named_child(1) else { - eprintln!( - "Error: expected property node {:?} to have child at node 1", - node.kind() - ); - return None; - }; - let Some(property_name_node) = property_name_node_outer.named_child(0) else { - eprintln!("Error: expected property name node to have child at node 0"); - return None; - }; - - get_string_at_byte_range(content, property_name_node.byte_range()) -} - /// Given a parameter node, get the name pub fn get_parameter_name(node: &Node, content: &str) -> Option { let Some(parameter_name_node_outer) = node.named_child(1) else { diff --git a/objectscript-lsp/crates/objectscript-core/src/foreignkey.rs b/objectscript-lsp/crates/objectscript-core/src/foreignkey.rs new file mode 100644 index 0000000..26d4ade --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/foreignkey.rs @@ -0,0 +1,85 @@ +use crate::common::{get_node_children, get_string_at_byte_range, get_tracked_keywords}; +use crate::parse_structures::{ForeignKey, ForeignKeyAction}; +use tree_sitter::Node; + +impl Default for ForeignKey { + fn default() -> Self { + Self { + name: "TODO".to_string(), + properties_constrained: Vec::new(), + referenced_class: "TODO".to_string(), + referenced_index: None, + on_delete: ForeignKeyAction::NoAction, + on_update: ForeignKeyAction::NoAction, + } + } +} + +pub fn build_foreignkey_struct(foreignkey_node: Node, content: &str) -> Option { + if foreignkey_node.kind() != "foreignkey" { + eprintln!( + "Error: build_foreignkey_struct was called for node {:?}, but it can only be called for foreignkey nodes", + foreignkey_node.kind() + ); + return None; + } + let mut foreign_key = ForeignKey::default(); + let foreign_key_children = get_node_children(foreignkey_node); + for foreign_key_child in foreign_key_children { + match foreign_key_child.kind() { + "foreignkey_name" => { + if let Some(foreignkey_name_node) = foreign_key_child.named_child(0) + && let Some(name) = + get_string_at_byte_range(content, foreignkey_name_node.byte_range()) + { + foreign_key.name = name; + } else { + eprintln!("Error: failed to get foreignkey name."); + return None; + } + } + "property_name" => { + if let Some(property_name_node) = foreign_key_child.named_child(0) + && let Some(property_name) = + get_string_at_byte_range(content, property_name_node.byte_range()) + { + foreign_key.properties_constrained.push(property_name); + } + } + "class_name" => { + if let Some(class_name_node) = foreign_key_child.named_child(0) + && let Some(class_name) = + get_string_at_byte_range(content, class_name_node.byte_range()) + { + foreign_key.referenced_class = class_name; + } + } + "index_name" => { + if let Some(index_name_node) = foreign_key_child.named_child(0) { + foreign_key.referenced_index = + get_string_at_byte_range(content, index_name_node.byte_range()); + } + } + "foreignkey_keywords" => { + let tracked_keywords = get_tracked_keywords(foreign_key_child, content); + foreign_key.on_update = tracked_keywords.on_update; + foreign_key.on_delete = tracked_keywords.on_delete; + } + _ => { + eprintln!( + "Error: Unrecognized foreign key child node {:?}", + foreign_key_child.kind() + ); + continue; + } + } + } + if &foreign_key.name == "TODO" || &foreign_key.referenced_class == "TODO" { + eprintln!( + "Error: failed to parse foreign key name {:?} or referenced class {:?}", + &foreign_key.name, &foreign_key.referenced_class + ); + return None; + } + Some(foreign_key) +} diff --git a/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs b/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs index fc2f58e..496c30c 100644 --- a/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs +++ b/objectscript-lsp/crates/objectscript-core/src/global_semantic.rs @@ -2,12 +2,15 @@ use crate::common::generic_exit_statements; use crate::dependency_tracker::Dependents; use crate::local_semantic::LocalSemanticModel; use crate::parse_structures::{ - Class, ClassId, DfsState, Method, MethodRef, Parameter, ParameterRef, Property, PropertyRef, - PublicVarId, Variable, VariableDefType, VariableRef, + Class, ClassId, DfsState, ForeignKey, ForeignKeyRef, Index, IndexRef, Method, MethodRef, + Parameter, ParameterRef, Projection, ProjectionRef, Property, PropertyRef, PublicVarId, Query, + QueryRef, Relationship, RelationshipRef, Storage, StorageRef, Trigger, TriggerRef, Variable, + VariableDefType, VariableRef, XData, XdataRef, }; use crate::scope_structures::{ - ClassGlobalSymbol, MethodSymbol, ParameterSymbol, PropertySymbol, ScopeId, - VariableGlobalSymbol, VariableSymbol, + ClassGlobalSymbol, ForeignKeySymbol, IndexSymbol, MethodSymbol, ParameterSymbol, + ProjectionSymbol, PropertySymbol, QuerySymbol, RelationshipSymbol, ScopeId, StorageSymbol, + TriggerSymbol, VariableGlobalSymbol, VariableSymbol, XdataSymbol, }; use std::collections::{HashMap, HashSet}; use tower_lsp::lsp_types::Url; @@ -26,6 +29,14 @@ pub struct GlobalSemanticModel { pub properties: HashMap, /// Stores Parameters in a workspace for public parameters. pub parameters: HashMap, + pub relationships: HashMap, + pub foreignkeys: HashMap, + pub queries: HashMap, + pub indices: HashMap, + pub triggers: HashMap, + pub xdata: HashMap, + pub projections: HashMap, + pub storage: HashMap, /// Stores all local semantic models in a workspace. pub lsms: HashMap, /// Stores all class symbols in a workspace. @@ -36,6 +47,14 @@ pub struct GlobalSemanticModel { pub property_defs: HashMap, /// Stores Parameter Symbols in a workspace for public properties. pub parameter_defs: HashMap, + pub relationship_defs: HashMap, + pub foreignkey_defs: HashMap, + pub query_defs: HashMap, + pub index_defs: HashMap, + pub trigger_defs: HashMap, + pub xdata_defs: HashMap, + pub projection_defs: HashMap, + pub storage_defs: HashMap, /// Stores Variable Global Symbols per Class Global Symbol pub variable_defs: HashMap>>, next_class_id: usize, @@ -59,6 +78,22 @@ impl GlobalSemanticModel { property_defs: HashMap::new(), parameter_defs: HashMap::new(), parameters: HashMap::new(), + relationships: HashMap::new(), + foreignkeys: HashMap::new(), + queries: HashMap::new(), + indices: HashMap::new(), + triggers: HashMap::new(), + xdata: HashMap::new(), + projections: HashMap::new(), + storage: HashMap::new(), + relationship_defs: HashMap::new(), + foreignkey_defs: HashMap::new(), + query_defs: HashMap::new(), + index_defs: HashMap::new(), + trigger_defs: HashMap::new(), + xdata_defs: HashMap::new(), + projection_defs: HashMap::new(), + storage_defs: HashMap::new(), next_class_id: 0, } } @@ -211,6 +246,168 @@ impl GlobalSemanticModel { self.parameters.insert(parameter_ref, parameter); } + fn member_symbol( + name: String, + location: Range, + url: Url, + ) -> crate::scope_structures::MemberSymbol { + crate::scope_structures::MemberSymbol { + name, + url, + location, + references: Vec::new(), + } + } + + pub fn new_relationship( + &mut self, + value: Relationship, + member_ref: RelationshipRef, + range: Range, + url: Url, + ) { + let symbol = Self::member_symbol(value.name.clone(), range, url); + if value.is_public { + self.relationship_defs.insert(member_ref, symbol); + self.relationships.insert(member_ref, value); + } else if let Some(lsm) = self.get_local_semantic_mut(&member_ref.class) { + lsm.new_relationship(value, member_ref); + } + } + pub fn get_relationship(&self, member_ref: &RelationshipRef) -> Option<&Relationship> { + self.relationships.get(member_ref).or_else(|| { + self.get_local_semantic(&member_ref.class)? + .get_relationship(member_ref) + }) + } + pub fn remove_relationship(&mut self, member_ref: &RelationshipRef) -> Option { + self.relationship_defs.remove(member_ref); + self.relationships.remove(member_ref).or_else(|| { + self.get_local_semantic_mut(&member_ref.class)? + .remove_relationship(member_ref) + }) + } + + pub fn new_query(&mut self, value: Query, member_ref: QueryRef, range: Range, url: Url) { + let symbol = Self::member_symbol(value.name.clone(), range, url); + if value.is_public { + self.query_defs.insert(member_ref, symbol); + self.queries.insert(member_ref, value); + } else if let Some(lsm) = self.get_local_semantic_mut(&member_ref.class) { + lsm.new_query(value, member_ref); + } + } + pub fn get_query(&self, member_ref: &QueryRef) -> Option<&Query> { + self.queries.get(member_ref).or_else(|| { + self.get_local_semantic(&member_ref.class)? + .get_query(member_ref) + }) + } + pub fn remove_query(&mut self, member_ref: &QueryRef) -> Option { + self.query_defs.remove(member_ref); + self.queries.remove(member_ref).or_else(|| { + self.get_local_semantic_mut(&member_ref.class)? + .remove_query(member_ref) + }) + } + + pub fn new_foreignkey( + &mut self, + value: ForeignKey, + member_ref: ForeignKeyRef, + range: Range, + url: Url, + ) { + self.foreignkey_defs.insert( + member_ref, + Self::member_symbol(value.name.clone(), range, url), + ); + self.foreignkeys.insert(member_ref, value); + } + pub fn get_foreignkey(&self, member_ref: &ForeignKeyRef) -> Option<&ForeignKey> { + self.foreignkeys.get(member_ref) + } + pub fn remove_foreignkey(&mut self, member_ref: &ForeignKeyRef) -> Option { + self.foreignkey_defs.remove(member_ref); + self.foreignkeys.remove(member_ref) + } + pub fn new_index(&mut self, value: Index, member_ref: IndexRef, range: Range, url: Url) { + self.index_defs.insert( + member_ref, + Self::member_symbol(value.name.clone(), range, url), + ); + self.indices.insert(member_ref, value); + } + pub fn get_index(&self, member_ref: &IndexRef) -> Option<&Index> { + self.indices.get(member_ref) + } + pub fn remove_index(&mut self, member_ref: &IndexRef) -> Option { + self.index_defs.remove(member_ref); + self.indices.remove(member_ref) + } + pub fn new_trigger(&mut self, value: Trigger, member_ref: TriggerRef, range: Range, url: Url) { + self.trigger_defs.insert( + member_ref, + Self::member_symbol(value.name.clone(), range, url), + ); + self.triggers.insert(member_ref, value); + } + pub fn get_trigger(&self, member_ref: &TriggerRef) -> Option<&Trigger> { + self.triggers.get(member_ref) + } + pub fn remove_trigger(&mut self, member_ref: &TriggerRef) -> Option { + self.trigger_defs.remove(member_ref); + self.triggers.remove(member_ref) + } + pub fn new_xdata(&mut self, value: XData, member_ref: XdataRef, range: Range, url: Url) { + self.xdata_defs.insert( + member_ref, + Self::member_symbol(value.name.clone(), range, url), + ); + self.xdata.insert(member_ref, value); + } + pub fn get_xdata(&self, member_ref: &XdataRef) -> Option<&XData> { + self.xdata.get(member_ref) + } + pub fn remove_xdata(&mut self, member_ref: &XdataRef) -> Option { + self.xdata_defs.remove(member_ref); + self.xdata.remove(member_ref) + } + pub fn new_projection( + &mut self, + value: Projection, + member_ref: ProjectionRef, + range: Range, + url: Url, + ) { + self.projection_defs.insert( + member_ref, + Self::member_symbol(value.name.clone(), range, url), + ); + self.projections.insert(member_ref, value); + } + pub fn get_projection(&self, member_ref: &ProjectionRef) -> Option<&Projection> { + self.projections.get(member_ref) + } + pub fn remove_projection(&mut self, member_ref: &ProjectionRef) -> Option { + self.projection_defs.remove(member_ref); + self.projections.remove(member_ref) + } + pub fn new_storage(&mut self, value: Storage, member_ref: StorageRef, range: Range, url: Url) { + self.storage_defs.insert( + member_ref, + Self::member_symbol(value.name.clone(), range, url), + ); + self.storage.insert(member_ref, value); + } + pub fn get_storage(&self, member_ref: &StorageRef) -> Option<&Storage> { + self.storage.get(member_ref) + } + pub fn remove_storage(&mut self, member_ref: &StorageRef) -> Option { + self.storage_defs.remove(member_ref); + self.storage.remove(member_ref) + } + /// Given a Class, adds the class to the `self.classes` vec, returning ClassId, which /// corresponds to the index that the Class is stored. pub fn new_class(&mut self, class: Class, class_id: ClassId, range: Range, url: Url) { diff --git a/objectscript-lsp/crates/objectscript-core/src/index.rs b/objectscript-lsp/crates/objectscript-core/src/index.rs new file mode 100644 index 0000000..e64d7c9 --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/index.rs @@ -0,0 +1,140 @@ +use crate::common::{ + find_return_type, get_node_children, get_string_at_byte_range, get_tracked_keywords, + parse_return_type, +}; +use crate::parse_structures::{Index, IndexPropertyValue, IndexType, TypeName}; +use tree_sitter::Node; + +impl Default for Index { + fn default() -> Self { + Self { + name: "TODO".to_string(), + properties: Vec::new(), + index_type: IndexType::Index, + return_type: None, + } + } +} +pub fn build_index_struct(index_node: Node, content: &str) -> Option { + if index_node.kind() != "index" { + eprintln!( + "Error: build_index_struct was called for node {:?}, but it can only be called for index nodes", + index_node.kind() + ); + return None; + } + let mut index = Index::default(); + let index_children = get_node_children(index_node); + for index_child in index_children { + match index_child.kind() { + "index_name" => { + if let Some(index_name_node) = index_child.named_child(0) + && let Some(index_name) = + get_string_at_byte_range(content, index_name_node.byte_range()) + { + index.name = index_name; + } else { + eprintln!("Error: failed to get index name."); + return None; + } + } + "return_type" => { + index.return_type = parse_return_type(index_child, content); + } + "index_property_value" => { + let mut property_name = None; + let mut elements = false; + let mut keys = false; + let mut property_return_type = None; + let index_property_value_children = get_node_children(index_child); + for index_property_value_child in index_property_value_children { + match index_property_value_child.kind() { + "property_name" => { + if let Some(property_name_node) = + index_property_value_child.named_child(0) + { + property_name = get_string_at_byte_range( + content, + property_name_node.byte_range(), + ); + } else { + eprintln!("Error: failed to get property name."); + continue; + } + } + "index_type" => { + let mut return_type_parameters = Vec::new(); + let mut return_type_id = None; + let index_type_children = get_node_children(index_property_value_child); + for index_type_child in index_type_children { + match index_type_child.kind() { + "typename" => { + if let Some(typename_identifier) = get_string_at_byte_range( + content, + index_type_child.byte_range(), + ) { + return_type_id = + Some(find_return_type(typename_identifier)); + } + } + "numeric_literal" => { + if let Some(param) = get_string_at_byte_range( + content, + index_type_child.byte_range(), + ) { + return_type_parameters.push(param); + } + } + _ => continue, + } + } + if let Some(return_type_id) = return_type_id { + property_return_type = Some(TypeName { + ret_type: return_type_id, + parameters: return_type_parameters, + }) + } + } + "typename" => { + if let Some(val) = get_string_at_byte_range( + content, + index_property_value_child.byte_range(), + ) { + if val.to_ascii_lowercase() == "elements" { + elements = true; + } + if val.to_ascii_lowercase() == "keys" { + keys = true; + } + } + } + _ => continue, + } + } + if let Some(property_name) = property_name { + index.properties.push(IndexPropertyValue { + name: property_name, + elements, + keys, + return_type: property_return_type, + }) + } + } + "index_keywords" => { + let tracked_keywords = get_tracked_keywords(index_child, content); + index.index_type = tracked_keywords.index_type; + } + "extent_index_keywords" => { + index.index_type = IndexType::Extent; + } + _ => { + continue; + } + } + } + + if &index.name == "TODO" { + return None; + } + Some(index) +} diff --git a/objectscript-lsp/crates/objectscript-core/src/lib.rs b/objectscript-lsp/crates/objectscript-core/src/lib.rs index 26e3e6c..373e084 100644 --- a/objectscript-lsp/crates/objectscript-core/src/lib.rs +++ b/objectscript-lsp/crates/objectscript-core/src/lib.rs @@ -3,16 +3,24 @@ pub mod common; pub mod config; pub mod dependency_tracker; pub mod document; +pub mod foreignkey; pub mod global_semantic; +pub mod index; pub mod local_semantic; pub mod method; pub mod override_index; pub mod parameter; pub mod parse_structures; +pub mod projection; pub mod property; +pub mod query; pub mod refactor; +pub mod relationship; pub mod scope_structures; pub mod scope_tree; +pub mod storage; +pub mod trigger; pub mod variable; pub mod workspace; pub mod workspace_diff; +pub mod xdata; diff --git a/objectscript-lsp/crates/objectscript-core/src/local_semantic.rs b/objectscript-lsp/crates/objectscript-core/src/local_semantic.rs index 61f43b9..475908c 100644 --- a/objectscript-lsp/crates/objectscript-core/src/local_semantic.rs +++ b/objectscript-lsp/crates/objectscript-core/src/local_semantic.rs @@ -1,5 +1,6 @@ use crate::parse_structures::{ - Method, MethodRef, PrivateVarId, Property, PropertyRef, Variable, VariableRef, + Method, MethodRef, PrivateVarId, Property, PropertyRef, Query, QueryRef, Relationship, + RelationshipRef, Variable, VariableRef, }; use crate::scope_structures::ScopeId; use std::collections::{HashMap, HashSet}; @@ -10,6 +11,8 @@ use std::collections::{HashMap, HashSet}; pub struct LocalSemanticModel { pub methods: HashMap, pub properties: HashMap, + pub relationships: HashMap, + pub queries: HashMap, pub variables: HashMap>>, pub active: bool, } @@ -20,6 +23,8 @@ impl LocalSemanticModel { Self { methods: HashMap::new(), properties: HashMap::new(), + relationships: HashMap::new(), + queries: HashMap::new(), variables: HashMap::new(), active: true, } @@ -71,6 +76,8 @@ impl LocalSemanticModel { pub fn clear(&mut self) { self.methods.clear(); self.properties.clear(); + self.relationships.clear(); + self.queries.clear(); self.variables.clear(); self.active = false; } @@ -135,4 +142,23 @@ impl LocalSemanticModel { pub fn new_property(&mut self, property: Property, property_ref: PropertyRef) { self.properties.insert(property_ref, property); } + + pub fn new_relationship(&mut self, value: Relationship, member_ref: RelationshipRef) { + self.relationships.insert(member_ref, value); + } + pub fn get_relationship(&self, member_ref: &RelationshipRef) -> Option<&Relationship> { + self.relationships.get(member_ref) + } + pub fn remove_relationship(&mut self, member_ref: &RelationshipRef) -> Option { + self.relationships.remove(member_ref) + } + pub fn new_query(&mut self, value: Query, member_ref: QueryRef) { + self.queries.insert(member_ref, value); + } + pub fn get_query(&self, member_ref: &QueryRef) -> Option<&Query> { + self.queries.get(member_ref) + } + pub fn remove_query(&mut self, member_ref: &QueryRef) -> Option { + self.queries.remove(member_ref) + } } diff --git a/objectscript-lsp/crates/objectscript-core/src/method.rs b/objectscript-lsp/crates/objectscript-core/src/method.rs index 9b5be11..57c341e 100644 --- a/objectscript-lsp/crates/objectscript-core/src/method.rs +++ b/objectscript-lsp/crates/objectscript-core/src/method.rs @@ -1,10 +1,9 @@ use crate::common::{ - find_return_type, find_var_dependencies, get_keyword_and_value, get_node_children, - get_string_at_byte_range, parse_line_ref, range_within_range, + build_argument, find_var_dependencies, get_node_children, get_string_at_byte_range, + get_tracked_keywords, parse_line_ref, parse_return_type, range_within_range, }; use crate::parse_structures::{ - CodeMode, Language, Method, MethodType, TypeName, UnresolvedMethodRef, Variable, - VariableDefType, + CodeMode, Language, Method, MethodType, UnresolvedMethodRef, Variable, VariableDefType, }; use crate::scope_structures::ScopeId; @@ -15,15 +14,9 @@ use tree_sitter::{Language as TsLanguage, Node, Query, QueryCursor, Range, Strea use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; use tree_sitter_objectscript_routine::LANGUAGE_OBJECTSCRIPT_ROUTINE; -const SET_VARIABLES_QUERY: &str = - "(command_set (set_argument [(set_target) (set_target_list)] @settarget (expression) @value ))"; - -const ROUTINE_ARGUMENT_QUERY: &str = "(tag_parameter (method_arg) @arg)"; - -const CLASS_METHOD_ARGUMENT_QUERY: &str = - "(argument (method_arg) @arg (return_type (typename) @typename)?)"; - -const METHOD_DEPENDENCY_QUERY: &str = r#"[(class_method_call) @classmethodcall +const UDL_METHOD_ANALYSIS_QUERY: &str = r#" + (command_set (set_argument [(set_target) (set_target_list)] @settarget (expression) @value )) + [(class_method_call) @classmethodcall (system_defined_function) @systemfunc (relative_dot_method) @relativemethod (routine_tag_call) @routine @@ -31,13 +24,16 @@ const METHOD_DEPENDENCY_QUERY: &str = r#"[(class_method_call) @classmethodcall (print_argument) @routine ]"#; -const METHOD_KEYWORD_QUERY: &str = r#" - (method_definition ([(method_keyword_codemode_expression) @keyword - (method_keyword_external_language) @keyword - (method_keyword) @keyword - (call_method_keyword) @keyword - (return_type (typename (identifier) @returntype )) - ]))"#; +const ROUTINE_METHOD_ANALYSIS_QUERY: &str = r#" + (tag_parameter (method_arg) @arg) + (command_set (set_argument [(set_target) (set_target_list)] @settarget (expression) @value )) + [(class_method_call) @classmethodcall + (system_defined_function) @systemfunc + (relative_dot_method) @relativemethod + (routine_tag_call) @routine + (goto_argument) @routine + (print_argument) @routine + ]"#; fn cached_query( query: &'static OnceLock, @@ -51,74 +47,199 @@ fn cached_query( }) } -fn udl_set_variables_query() -> &'static Query { +fn udl_method_analysis_query() -> &'static Query { static QUERY: OnceLock = OnceLock::new(); cached_query( &QUERY, LANGUAGE_OBJECTSCRIPT_UDL.into(), - SET_VARIABLES_QUERY, - "UDL set variables", + UDL_METHOD_ANALYSIS_QUERY, + "UDL method analysis", ) } -fn routine_set_variables_query() -> &'static Query { +fn routine_method_analysis_query() -> &'static Query { static QUERY: OnceLock = OnceLock::new(); cached_query( &QUERY, LANGUAGE_OBJECTSCRIPT_ROUTINE.into(), - SET_VARIABLES_QUERY, - "routine set variables", + ROUTINE_METHOD_ANALYSIS_QUERY, + "routine method analysis", ) } -fn routine_argument_query() -> &'static Query { - static QUERY: OnceLock = OnceLock::new(); - cached_query( - &QUERY, - LANGUAGE_OBJECTSCRIPT_ROUTINE.into(), - ROUTINE_ARGUMENT_QUERY, - "routine argument", - ) +#[derive(Clone, Copy)] +enum DependencyKind { + ClassMethod, + SystemFunction, + RelativeMethod, + Routine, } -fn class_method_argument_query() -> &'static Query { - static QUERY: OnceLock = OnceLock::new(); - cached_query( - &QUERY, - LANGUAGE_OBJECTSCRIPT_UDL.into(), - CLASS_METHOD_ARGUMENT_QUERY, - "class method argument", - ) +#[derive(Clone, Copy)] +struct SetFact<'tree> { + target: Node<'tree>, + value: Node<'tree>, } -fn udl_method_dependency_query() -> &'static Query { - static QUERY: OnceLock = OnceLock::new(); - cached_query( - &QUERY, - LANGUAGE_OBJECTSCRIPT_UDL.into(), - METHOD_DEPENDENCY_QUERY, - "UDL method dependency", - ) +struct MethodQueryFacts<'tree> { + routine_args: Vec>, + sets: Vec>, + dependencies: Vec<(DependencyKind, Node<'tree>)>, } -fn routine_method_dependency_query() -> &'static Query { - static QUERY: OnceLock = OnceLock::new(); - cached_query( - &QUERY, - LANGUAGE_OBJECTSCRIPT_ROUTINE.into(), - METHOD_DEPENDENCY_QUERY, - "routine method dependency", - ) +fn collect_method_query_facts<'tree>( + node: Node<'tree>, + content: &str, + is_class_method: bool, + method_range: Range, +) -> MethodQueryFacts<'tree> { + let query = if is_class_method { + udl_method_analysis_query() + } else { + routine_method_analysis_query() + }; + let mut cursor = QueryCursor::new(); + if !is_class_method { + cursor.set_byte_range(method_range.start_byte..method_range.end_byte); + } + let mut matches = cursor.matches(query, node, content.as_bytes()); + let argument_idx = query.capture_index_for_name("arg"); + let set_target_idx = query.capture_index_for_name("settarget"); + let set_value_idx = query.capture_index_for_name("value"); + let class_method_idx = query.capture_index_for_name("classmethodcall"); + let system_function_idx = query.capture_index_for_name("systemfunc"); + let relative_method_idx = query.capture_index_for_name("relativemethod"); + let routine_idx = query.capture_index_for_name("routine"); + let mut facts = MethodQueryFacts { + routine_args: Vec::new(), + sets: Vec::new(), + dependencies: Vec::new(), + }; + + while let Some(query_match) = matches.next() { + let mut argument = None; + let mut set_target = None; + let mut set_value = None; + for capture in query_match.captures { + if argument_idx == Some(capture.index) { + argument = Some(capture.node); + } else if set_target_idx == Some(capture.index) { + set_target = Some(capture.node); + } else if set_value_idx == Some(capture.index) { + set_value = Some(capture.node); + } else if class_method_idx == Some(capture.index) { + facts + .dependencies + .push((DependencyKind::ClassMethod, capture.node)); + } else if system_function_idx == Some(capture.index) { + facts + .dependencies + .push((DependencyKind::SystemFunction, capture.node)); + } else if relative_method_idx == Some(capture.index) { + facts + .dependencies + .push((DependencyKind::RelativeMethod, capture.node)); + } else if routine_idx == Some(capture.index) { + facts + .dependencies + .push((DependencyKind::Routine, capture.node)); + } + } + if let (Some(target), Some(value)) = (set_target, set_value) { + facts.sets.push(SetFact { target, value }); + } + if let Some(argument) = argument { + if !is_class_method { + facts.routine_args.push(argument); + } + } + } + facts } -fn method_keyword_query() -> &'static Query { - static QUERY: OnceLock = OnceLock::new(); - cached_query( - &QUERY, - LANGUAGE_OBJECTSCRIPT_UDL.into(), - METHOD_KEYWORD_QUERY, - "method keyword", - ) +pub fn build_method_struct( + method_node: Node, + method_type: MethodType, + content: &str, +) -> Option { + if method_node.kind() != "method_definition" && method_node.kind() != "clientmethod" { + eprintln!( + "Error: build_method_struct was called for node {:?}, but it can only be called for clientmethod or method_definition nodes", + method_node.kind() + ); + return None; + } + let mut method = Method::default(); + method.method_type = method_type; + let method_children = get_node_children(method_node); + for method_child in method_children { + match method_child.kind() { + "method_name" => { + if let Some(method_name_node) = method_child.named_child(0) + && let Some(name) = + get_string_at_byte_range(content, method_name_node.byte_range()) + { + method.name = name; + } else { + eprintln!("Error: failed to get method name."); + return None; + } + } + "arguments" => { + let argument_children = get_node_children(method_child); + for argument in argument_children { + if let Some((argument_struct, argument_range)) = + build_argument(argument, content) + { + method.arguments.insert( + argument_struct.name.clone(), + (argument_struct, argument_range), + ); + } + } + } + "return_type" => { + method.return_type = parse_return_type(method_child, content); + } + "expression_method_keywords" + | "call_method_keywords" + | "method_keywords" + | "external_method_keywords" => { + let tracked_keywords = get_tracked_keywords(method_child, content); + method.is_final = tracked_keywords.is_final; + method.language = tracked_keywords.language; + method.is_procedure_block = tracked_keywords.procedure_block; + method.is_public = tracked_keywords.is_public; + method.public_variables_declared = tracked_keywords.public_variables_declared; + method.code_mode = tracked_keywords.code_mode; + } + _ => continue, + } + } + if method.name == "TODO".to_string() { + eprintln!("Error: failed to parse method"); + return None; + } + Some(method) +} + +impl Default for Method { + fn default() -> Self { + Self { + method_type: MethodType::ClassMethod, + code_mode: CodeMode::Code, + is_final: None, + variables: HashMap::new(), + arguments: HashMap::new(), + is_public: true, + is_procedure_block: None, + language: None, + next_argument_id: 0, + public_variables_declared: HashSet::new(), + return_type: None, + name: "TODO".to_string(), + } + } } impl Method { @@ -136,55 +257,77 @@ impl Method { return_type: None, name: method_name, variables: HashMap::new(), + arguments: HashMap::new(), is_public: true, is_procedure_block: Some(false), language: None, public_variables_declared: public_variables, code_mode: CodeMode::Code, is_final: Some(true), + next_argument_id: 0, + }, + MethodType::ClientMethod => Self { + method_type, + return_type: None, + name: method_name, + variables: HashMap::new(), + arguments: HashMap::new(), + is_public: true, + is_procedure_block: None, + language: Some(Language::JavaScript), + public_variables_declared: public_variables, + code_mode: CodeMode::Code, + is_final: None, + next_argument_id: 0, }, MethodType::Subroutine(is_public) | MethodType::DottedSubroutine(is_public) => Self { method_type, return_type: None, name: method_name, variables: HashMap::new(), + arguments: HashMap::new(), is_public: is_public, is_procedure_block: Some(false), language: None, public_variables_declared: public_variables, code_mode: CodeMode::Code, is_final: Some(true), + next_argument_id: 0, }, MethodType::Procedure(is_public) => Self { method_type, return_type: None, name: method_name, variables: HashMap::new(), + arguments: HashMap::new(), is_public: is_public, is_procedure_block: Some(true), language: None, public_variables_declared: public_variables, code_mode: CodeMode::Code, is_final: Some(true), + next_argument_id: 0, }, MethodType::ClassMethod | MethodType::InstanceMethod => Self { method_type, return_type: None, name: method_name, variables: HashMap::new(), + arguments: HashMap::new(), is_public: true, is_procedure_block: None, language: None, public_variables_declared: public_variables, code_mode: CodeMode::Code, is_final: None, + next_argument_id: 0, }, }; } fn build_subroutine_set_variables( &self, - node: Node, + sets: &[SetFact<'_>], content: &str, scope_tree: &ScopeTree, variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, @@ -192,19 +335,15 @@ impl Method { class_name: &str, ) { { - let query = routine_set_variables_query(); - let mut cursor = QueryCursor::new(); - cursor.set_byte_range(method_range.start_byte..method_range.end_byte); - let mut iter = cursor.matches(query, node, content.as_bytes()); - while let Some(query_match) = iter.next() { - let set_target_node = query_match.captures[0].node; + for set_fact in sets { + let set_target_node = set_fact.target; if !range_within_range(&set_target_node.range(), &method_range) { continue; } let mut var_defs = Vec::new(); let mut var_deps = Vec::new(); let mut var_type = VariableDefType::VariableDef; - let var_value = query_match.captures[1].node; + let var_value = set_fact.value; let children; if set_target_node.kind() == "set_target_list" { children = get_node_children(set_target_node); @@ -289,7 +428,7 @@ impl Method { fn build_procedure_set_variables( &self, - node: Node, + sets: &[SetFact<'_>], content: &str, scope_tree: &ScopeTree, variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, @@ -299,26 +438,16 @@ impl Method { class_name: &str, ) { { - let query = if is_class_method { - udl_set_variables_query() - } else { - routine_set_variables_query() - }; - let mut cursor = QueryCursor::new(); - if !is_class_method { - cursor.set_byte_range(method_range.start_byte..method_range.end_byte); - } - let mut iter = cursor.matches(query, node, content.as_bytes()); - while let Some(query_match) = iter.next() { + for set_fact in sets { let mut var_defs = Vec::new(); let mut var_deps = Vec::new(); let mut var_type = VariableDefType::VariableDef; - let set_target_node = query_match.captures[0].node; + let set_target_node = set_fact.target; if !is_class_method && !range_within_range(&set_target_node.range(), &method_range) { continue; } - let var_value = query_match.captures[1].node; + let var_value = set_fact.value; let children; if set_target_node.kind() == "set_target_list" { children = get_node_children(set_target_node); @@ -419,7 +548,7 @@ impl Method { /// Given tag node, parse the arguments fn build_routine_method_arguments( &self, - tag_node: Node, + arguments: &[Node<'_>], content: &str, scope_tree: &ScopeTree, variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, @@ -427,12 +556,7 @@ impl Method { method_range: Range, ) { { - let query = routine_argument_query(); - let mut cursor = QueryCursor::new(); - cursor.set_byte_range(method_range.start_byte..method_range.end_byte); - let mut iter = cursor.matches(query, tag_node, content.as_bytes()); - while let Some(query_match) = iter.next() { - let method_arg = query_match.captures[0].node; + for method_arg in arguments.iter().copied() { if !range_within_range(&method_arg.range(), &method_range) { continue; } @@ -479,109 +603,10 @@ impl Method { } } - fn build_class_method_arguments( - &self, - node: Node, - content: &str, - scope_tree: &ScopeTree, - variables_in_method: &mut Vec<(Variable, Range, Vec, ScopeId)>, - class_is_procedure_block: bool, - ) { - { - let query = class_method_argument_query(); - let mut cursor = QueryCursor::new(); - let mut iter = cursor.matches(query, node, content.as_bytes()); - let arg_idx = query.capture_index_for_name("arg"); - let typename_idx = query.capture_index_for_name("typename"); - let mut return_type_parameters = Vec::new(); - let mut return_type_id = None; - let mut var_range = None; - let mut var_name = None; - while let Some(query_match) = iter.next() { - let mut i = 0; - let mut arg_type = None; - while i < query_match.captures.len() { - let capture = &query_match.captures[i]; - if arg_idx == Some(capture.index) { - let method_arg = capture.node; - if let Some(method_arg_type) = method_arg.named_child(0) { - let Some(variable_name_node) = method_arg_type.named_child(0) else { - eprintln!( - "Error: Expression,byref_arg, and variadic_arg nodes all have a child at index 0, but this does not {:?}", - method_arg_type.kind() - ); - break; - }; - var_name = - get_string_at_byte_range(content, variable_name_node.byte_range()); - var_range = Some(variable_name_node.range()); - } else { - eprintln!( - "Error: Method arg node should have named children. This node didn't {:?}", - method_arg.kind() - ); - } - i += 1; - continue; - } else if typename_idx == Some(capture.index) { - let typename = capture.node; - let identifiers = get_node_children(typename); - let mut j = 0; - while j < identifiers.len() { - let identifier_node = &identifiers[j]; - let Some(typename_identifier) = - get_string_at_byte_range(content, identifier_node.byte_range()) - else { - j += 1; - continue; - }; - if j == 0 { - return_type_id = Some(find_return_type(typename_identifier)); - } else { - return_type_parameters.push(typename_identifier); - } - j += 1; - continue; - } - if let Some(typename_id) = &return_type_id { - arg_type = Some(TypeName { - ret_type: typename_id.clone(), - parameters: return_type_parameters.clone(), - }) - } - - i += 1; - continue; - } - i += 1; - } - if let Some(var_name) = &var_name - && let Some(var_range) = var_range - { - let variable_is_public = - self.is_procedure_block.unwrap_or(class_is_procedure_block) == false - || self.public_variables_declared.contains(var_name); - let var = Variable::new( - var_name.clone(), - arg_type, - variable_is_public, - VariableDefType::VariableDef, - ); - if let Some(scope_id) = scope_tree - .find_current_scope_for_range(var_range.start_point, var_range.end_point) - { - variables_in_method.push((var, var_range, Vec::new(), scope_id)); - } - } - } - } - } - fn get_method_dependencies( &mut self, - node: Node, + facts: &MethodQueryFacts<'_>, content: &str, - is_class_method: bool, class_name: &str, method_range: Range, ) -> ( @@ -591,73 +616,68 @@ impl Method { let mut unresolved_method_refs = HashSet::new(); let mut unresolved_oref_method_refs = HashSet::new(); { - let query = if is_class_method { - udl_method_dependency_query() - } else { - routine_method_dependency_query() - }; - let mut cursor = QueryCursor::new(); - if !is_class_method { - cursor.set_byte_range(method_range.start_byte..method_range.end_byte); - } - let mut iter = cursor.matches(query, node, content.as_bytes()); - let classmethod_idx = query.capture_index_for_name("classmethodcall"); - let systemfunc_idx = query.capture_index_for_name("systemfunc"); - let relativemethod_idx = query.capture_index_for_name("relativemethod"); - let routine_idx = query.capture_index_for_name("routine"); - while let Some(query_match) = iter.next() { - let mut i = 0; - while i < query_match.captures.len() { - let capture = &query_match.captures[i]; - let matched_node = capture.node; - if !range_within_range(&matched_node.range(), &method_range) { - i += 1; - continue; + for (kind, matched_node) in &facts.dependencies { + let matched_node = *matched_node; + if !range_within_range(&matched_node.range(), &method_range) { + continue; + } + if matches!(kind, DependencyKind::ClassMethod) { + if let Some(class_ref) = matched_node.named_child(0) + && let Some(method_name_outer) = matched_node.named_child(1) + && let Some(class_name_outer) = class_ref.named_child(1) + && let Some(method_name_node) = method_name_outer.named_child(0) + && let Some(class_name_node) = class_name_outer.named_child(0) + && let Some(method_name) = + get_string_at_byte_range(content, method_name_node.byte_range()) + && let Some(class_name) = + get_string_at_byte_range(content, class_name_node.byte_range()) + { + unresolved_method_refs.insert(UnresolvedMethodRef { + class: class_name, + method: method_name, + offset: None, + method_call_range: matched_node.range(), + }); } - if classmethod_idx == Some(capture.index) { - if let Some(class_ref) = matched_node.named_child(0) - && let Some(method_name_outer) = matched_node.named_child(1) - && let Some(class_name_outer) = class_ref.named_child(1) - && let Some(method_name_node) = method_name_outer.named_child(0) - && let Some(class_name_node) = class_name_outer.named_child(0) - && let Some(method_name) = - get_string_at_byte_range(content, method_name_node.byte_range()) - && let Some(class_name) = - get_string_at_byte_range(content, class_name_node.byte_range()) + } else if matches!(kind, DependencyKind::SystemFunction) { + let Some(node_str) = + get_string_at_byte_range(content, matched_node.byte_range()) + else { + continue; + }; + let (before, method_args) = ( + node_str.split('(').nth(0), + node_str.split('(').nth(1).unwrap_or(""), + ); + if let Some(func_name) = before { + if func_name.eq_ignore_ascii_case("$zobjmethod") + || func_name.eq_ignore_ascii_case("$method") { - unresolved_method_refs.insert(UnresolvedMethodRef { - class: class_name, - method: method_name, - offset: None, - method_call_range: matched_node.range(), - }); - } - } else if systemfunc_idx == Some(capture.index) { - let Some(node_str) = - get_string_at_byte_range(content, matched_node.byte_range()) - else { - i += 1; - continue; - }; - let (before, method_args) = ( - node_str.split('(').nth(0), - node_str.split('(').nth(1).unwrap_or(""), - ); - if let Some(func_name) = before { - if func_name.eq_ignore_ascii_case("$zobjmethod") - || func_name.eq_ignore_ascii_case("$method") + if let Some(oref_method_arg) = matched_node.named_child(0) + && let Some(oref_method_arg_type) = oref_method_arg.named_child(0) + && let Some(oref_name_node) = oref_method_arg_type.named_child(0) + && let Some(oref_var_name) = + get_string_at_byte_range(content, oref_name_node.byte_range()) + && let Some(method_name_method_arg) = matched_node.named_child(1) + && let Some(method_name_arg_type) = + method_name_method_arg.named_child(0) + && let Some(method_name_node) = method_name_arg_type.named_child(0) + && let Some(method_name) = + get_string_at_byte_range(content, method_name_node.byte_range()) { - if let Some(oref_method_arg) = matched_node.named_child(0) - && let Some(oref_method_arg_type) = - oref_method_arg.named_child(0) - && let Some(oref_name_node) = - oref_method_arg_type.named_child(0) - && let Some(oref_var_name) = get_string_at_byte_range( - content, - oref_name_node.byte_range(), - ) - && let Some(method_name_method_arg) = - matched_node.named_child(1) + unresolved_oref_method_refs.insert(( + oref_var_name, + method_name, + matched_node.range(), + self.name.clone(), + )); + } + } else if func_name.eq_ignore_ascii_case("$classmethod") + || func_name.eq_ignore_ascii_case("$zobjclassmethod") + { + if method_args.trim_start().chars().next() == Some(',') { + // class is current one + if let Some(method_name_method_arg) = matched_node.named_child(0) && let Some(method_name_arg_type) = method_name_method_arg.named_child(0) && let Some(method_name_node) = @@ -667,150 +687,119 @@ impl Method { method_name_node.byte_range(), ) { - unresolved_oref_method_refs.insert(( - oref_var_name, - method_name, - matched_node.range(), - self.name.clone(), - )); - } - } else if func_name.eq_ignore_ascii_case("$classmethod") - || func_name.eq_ignore_ascii_case("$zobjclassmethod") - { - if method_args.trim_start().chars().next() == Some(',') { - // class is current one - if let Some(method_name_method_arg) = - matched_node.named_child(0) - && let Some(method_name_arg_type) = - method_name_method_arg.named_child(0) - && let Some(method_name_node) = - method_name_arg_type.named_child(0) - && let Some(method_name) = get_string_at_byte_range( - content, - method_name_node.byte_range(), - ) - { - if method_name_node.kind() == "string_literal" { - unresolved_method_refs.insert(UnresolvedMethodRef { - class: class_name.to_string(), - method: method_name, - offset: None, - method_call_range: matched_node.range(), - }); - } - } - } else { - if let Some(classname_method_arg) = matched_node.named_child(0) - && let Some(classname_method_arg_type) = - classname_method_arg.named_child(0) - && let Some(classname_node) = - classname_method_arg_type.named_child(0) - && let Some(classname_var) = get_string_at_byte_range( - content, - classname_node.byte_range(), - ) - && let Some(method_name_method_arg) = - matched_node.named_child(1) - && let Some(method_name_arg_type) = - method_name_method_arg.named_child(0) - && let Some(method_name_node) = - method_name_arg_type.named_child(0) - && let Some(method_name) = get_string_at_byte_range( - content, - method_name_node.byte_range(), - ) - { - if method_name_node.kind() == "string_literal" { - unresolved_method_refs.insert(UnresolvedMethodRef { - class: classname_var, - method: method_name, - offset: None, - method_call_range: matched_node.range(), - }); - } + if method_name_node.kind() == "string_literal" { + unresolved_method_refs.insert(UnresolvedMethodRef { + class: class_name.to_string(), + method: method_name, + offset: None, + method_call_range: matched_node.range(), + }); } } - } else if func_name.eq_ignore_ascii_case("$system") { - if let Some(class_name_node) = matched_node.named_child(0) - && let Some(method_name_node) = matched_node.named_child(1) - && let Some(classname) = get_string_at_byte_range( + } else { + if let Some(classname_method_arg) = matched_node.named_child(0) + && let Some(classname_method_arg_type) = + classname_method_arg.named_child(0) + && let Some(classname_node) = + classname_method_arg_type.named_child(0) + && let Some(classname_var) = get_string_at_byte_range( content, - class_name_node.byte_range(), + classname_node.byte_range(), ) + && let Some(method_name_method_arg) = + matched_node.named_child(1) + && let Some(method_name_arg_type) = + method_name_method_arg.named_child(0) + && let Some(method_name_node) = + method_name_arg_type.named_child(0) && let Some(method_name) = get_string_at_byte_range( content, method_name_node.byte_range(), ) { - unresolved_method_refs.insert(UnresolvedMethodRef { - class: classname, - method: method_name, - offset: None, - method_call_range: matched_node.range(), - }); - } - } - } - } else if relativemethod_idx == Some(capture.index) { - if let Some(oref_method) = matched_node.named_child(0) - && let Some(method_name_node) = oref_method.named_child(0) - && let Some(method_identifier) = method_name_node.named_child(0) - && let Some(method_name) = - get_string_at_byte_range(content, method_identifier.byte_range()) - { - unresolved_method_refs.insert(UnresolvedMethodRef { - class: class_name.to_string(), - method: method_name, - offset: None, - method_call_range: matched_node.range(), - }); - } - } else if routine_idx == Some(capture.index) { - if let Some(routine_tag_call_child) = matched_node.named_child(0) { - match routine_tag_call_child.kind() { - "method_name" => { - // this version doesn't have wrapped in quotes option - if let Some(method_name) = - get_string_at_byte_range(content, matched_node.byte_range()) - { + if method_name_node.kind() == "string_literal" { unresolved_method_refs.insert(UnresolvedMethodRef { - class: class_name.to_string(), + class: classname_var, method: method_name, offset: None, method_call_range: matched_node.range(), }); } } - "line_ref" => { - let (routine_name, method_name, offset) = parse_line_ref( - routine_tag_call_child, - content, - class_name.to_string(), - ); - + } + } else if func_name.eq_ignore_ascii_case("$system") { + if let Some(class_name_node) = matched_node.named_child(0) + && let Some(method_name_node) = matched_node.named_child(1) + && let Some(classname) = + get_string_at_byte_range(content, class_name_node.byte_range()) + && let Some(method_name) = + get_string_at_byte_range(content, method_name_node.byte_range()) + { + unresolved_method_refs.insert(UnresolvedMethodRef { + class: classname, + method: method_name, + offset: None, + method_call_range: matched_node.range(), + }); + } + } + } + } else if matches!(kind, DependencyKind::RelativeMethod) { + if let Some(oref_method) = matched_node.named_child(0) + && let Some(method_name_node) = oref_method.named_child(0) + && let Some(method_identifier) = method_name_node.named_child(0) + && let Some(method_name) = + get_string_at_byte_range(content, method_identifier.byte_range()) + { + unresolved_method_refs.insert(UnresolvedMethodRef { + class: class_name.to_string(), + method: method_name, + offset: None, + method_call_range: matched_node.range(), + }); + } + } else if matches!(kind, DependencyKind::Routine) { + if let Some(routine_tag_call_child) = matched_node.named_child(0) { + match routine_tag_call_child.kind() { + "method_name" => { + // this version doesn't have wrapped in quotes option + if let Some(method_name) = + get_string_at_byte_range(content, matched_node.byte_range()) + { unresolved_method_refs.insert(UnresolvedMethodRef { - class: routine_name, + class: class_name.to_string(), method: method_name, - offset, + offset: None, method_call_range: matched_node.range(), }); } - _ => { - i += 1; - continue; - } + } + "line_ref" => { + let (routine_name, method_name, offset) = parse_line_ref( + routine_tag_call_child, + content, + class_name.to_string(), + ); + + unresolved_method_refs.insert(UnresolvedMethodRef { + class: routine_name, + method: method_name, + offset, + method_call_range: matched_node.range(), + }); + } + _ => { + continue; } } } - i += 1; - continue; } } } (unresolved_method_refs, unresolved_oref_method_refs) } - /// Build Method Keywords and Body + /// Build Method Body pub fn rebuild_method( &mut self, node: Node, @@ -818,9 +807,9 @@ impl Method { scope_tree: &ScopeTree, method_type: MethodType, method_range: Range, - public_variables_declared: HashSet, // only procedure passes this - class_is_final: bool, - old_class_is_final: bool, + _public_variables_declared: HashSet, // retained for update-call compatibility + _class_is_final: bool, + _old_class_is_final: bool, class_is_procedure_block: bool, class_name: &str, ) -> ( @@ -830,20 +819,29 @@ impl Method { HashSet, HashSet<(String, String, Range, String)>, ) { - self.reset_method_keywords(method_type, public_variables_declared); + // self.reset_method_keywords(method_type, public_variables_declared); let mut variables_in_method = Vec::new(); + if method_type == MethodType::ClientMethod { + return (false, false, Vec::new(), HashSet::new(), HashSet::new()); + } + let is_class_method = matches!( + method_type, + MethodType::ClassMethod | MethodType::InstanceMethod + ); + let facts = collect_method_query_facts(node, content, is_class_method, method_range); match method_type { + MethodType::ClientMethod => unreachable!("client methods return before body analysis"), MethodType::Routine => { - self.build_routine_method_arguments( - node, - content, - scope_tree, - &mut variables_in_method, - false, - method_range, - ); + // self.build_routine_method_arguments( + // &facts.routine_args, + // content, + // scope_tree, + // &mut variables_in_method, + // false, + // method_range, + // ); self.build_subroutine_set_variables( - node, + &facts.sets, content, scope_tree, &mut variables_in_method, @@ -851,7 +849,7 @@ impl Method { class_name, ); let (unresolved_method_refs, unresolved_oref_method_refs) = - self.get_method_dependencies(node, content, false, class_name, method_range); + self.get_method_dependencies(&facts, content, class_name, method_range); ( false, false, @@ -863,7 +861,7 @@ impl Method { MethodType::Subroutine(is_public) | MethodType::DottedSubroutine(is_public) => { let is_public_changed = self.is_public != is_public; self.build_routine_method_arguments( - node, + &facts.routine_args, content, scope_tree, &mut variables_in_method, @@ -871,7 +869,7 @@ impl Method { method_range, ); self.build_subroutine_set_variables( - node, + &facts.sets, content, scope_tree, &mut variables_in_method, @@ -879,7 +877,7 @@ impl Method { class_name, ); let (unresolved_method_refs, unresolved_oref_method_refs) = - self.get_method_dependencies(node, content, false, class_name, method_range); + self.get_method_dependencies(&facts, content, class_name, method_range); ( false, @@ -892,7 +890,7 @@ impl Method { MethodType::Procedure(is_public) => { let is_public_changed = self.is_public != is_public; self.build_procedure_set_variables( - node, + &facts.sets, content, scope_tree, &mut variables_in_method, @@ -902,7 +900,7 @@ impl Method { class_name, ); self.build_routine_method_arguments( - node, + &facts.routine_args, content, scope_tree, &mut variables_in_method, @@ -910,7 +908,7 @@ impl Method { method_range, ); let (unresolved_method_refs, unresolved_oref_method_refs) = - self.get_method_dependencies(node, content, false, class_name, method_range); + self.get_method_dependencies(&facts, content, class_name, method_range); ( false, is_public_changed, @@ -920,17 +918,8 @@ impl Method { ) } MethodType::ClassMethod | MethodType::InstanceMethod => { - let (is_final_changed, is_public_changed) = - self.build_method_keywords(node, content, class_is_final, old_class_is_final); - self.build_class_method_arguments( - node, - content, - scope_tree, - &mut variables_in_method, - class_is_procedure_block, - ); self.build_procedure_set_variables( - node, + &facts.sets, content, scope_tree, &mut variables_in_method, @@ -940,10 +929,10 @@ impl Method { class_name, ); let (unresolved_method_refs, unresolved_oref_method_refs) = - self.get_method_dependencies(node, content, true, class_name, method_range); + self.get_method_dependencies(&facts, content, class_name, method_range); return ( - is_final_changed, - is_public_changed, + false, + false, variables_in_method, unresolved_method_refs, unresolved_oref_method_refs, @@ -952,163 +941,10 @@ impl Method { } } - pub fn reset_method_keywords( - &mut self, - method_type: MethodType, - public_variables_declared: HashSet, - ) { - self.method_type = method_type; - match method_type { - MethodType::Routine => { - self.return_type = None; - self.variables.clear(); - self.public_variables_declared = public_variables_declared; - } - MethodType::Procedure(is_public) - | MethodType::Subroutine(is_public) - | MethodType::DottedSubroutine(is_public) => { - self.return_type = None; - self.variables.clear(); - self.public_variables_declared = public_variables_declared; - self.is_public = is_public; - } - MethodType::ClassMethod | MethodType::InstanceMethod => { - self.return_type = None; - self.variables.clear(); - self.is_public = true; - self.is_procedure_block = None; - self.language = None; - self.public_variables_declared = HashSet::new(); - self.code_mode = CodeMode::Code; - self.is_final = None; - } - } - } - - fn build_method_keywords( - &mut self, - node: Node, - content: &str, - class_is_final: bool, - old_class_is_final: bool, - ) -> (bool, bool) { - // reset keywords to default based on method type - let mut is_final_changed = false; - let mut privacy_changed = false; - { - let query = method_keyword_query(); - let mut cursor = QueryCursor::new(); - let mut iter = cursor.matches(query, node, content.as_bytes()); - let keyword_idx = query.capture_index_for_name("keyword"); - let returntype_idx = query.capture_index_for_name("returntype"); - let old_is_public = self.is_public.clone(); - let old_is_final = self.is_final.clone(); - let mut return_type_parameters = Vec::new(); - let mut saw_first_return_type = false; - let mut return_type_id = None; - while let Some(query_match) = iter.next() { - let mut i = 0; - while i < query_match.captures.len() { - let capture = &query_match.captures[i]; - if keyword_idx == Some(capture.index) { - if let Some(keyword_str) = - get_string_at_byte_range(content, capture.node.byte_range()) - { - let (not, keyword_name, values) = - get_keyword_and_value(keyword_str.as_str()); - if keyword_name == "final" && !class_is_final { - if not { - self.is_final = Some(false); - } else { - self.is_final = Some(true); - } - } else if keyword_name == "private" { - if not { - self.is_public = true; - } else { - self.is_public = false; - } - } else if keyword_name == "procedureblock" { - if let Some(value) = values.get(0).copied() { - if value == "1" { - self.is_procedure_block = Some(true); - } else if value == "0" { - self.is_procedure_block = Some(false); - } - } else { - self.is_procedure_block = Some(true); - } - } else if keyword_name == "codemode" { - let Some(value) = values.get(0).copied() else { - eprintln!("Expected a value for language keyword, got: None"); - i += 1; - continue; - }; - if value == "call" { - self.code_mode = CodeMode::Call; - } else if value == "code" { - self.code_mode = CodeMode::Code; - } else if value == "expression" { - self.code_mode = CodeMode::Expression; - } else if value == "objectgenerator" { - self.code_mode = CodeMode::ObjectGenerator; - } - } else if keyword_name == "publiclist" { - for variable in values { - self.public_variables_declared.insert(variable.to_string()); - } - } else if keyword_name == "language" { - if let Some(value) = values.get(0).copied() { - if value == "objectscript" { - self.language = Some(Language::Objectscript); - } else if value == "tsql" { - self.language = Some(Language::TSql); - } else if value == "ispl" { - self.language = Some(Language::ISpl); - } else if value == "python" { - self.language = Some(Language::Python); - } - } - } - } - i += 1; - continue; - } else if returntype_idx == Some(capture.index) { - let return_type_node = capture.node; - let Some(typename) = - get_string_at_byte_range(content, return_type_node.byte_range()) - else { - i += 1; - continue; - }; - if !saw_first_return_type { - return_type_id = Some(find_return_type(typename)); - saw_first_return_type = true; - } else { - return_type_parameters.push(typename); - } - i += 1; - continue; - } - i += 1; - } - } - if let Some(typename_id) = return_type_id { - let typename = TypeName { - ret_type: typename_id, - parameters: return_type_parameters, - }; - self.return_type = Some(typename); - } - let old_final_keyword_res = old_is_final.unwrap_or(old_class_is_final); - let new_final_keyword = self.is_final.unwrap_or(class_is_final); - if old_final_keyword_res != new_final_keyword { - is_final_changed = true; - } - if old_is_public != self.is_public { - privacy_changed = true; - } - } - (is_final_changed, privacy_changed) + /// Allocates and returns the next sequential argument ID for this class. + pub fn get_next_argument_id(&mut self) -> usize { + let id = self.next_argument_id; + self.next_argument_id += 1; + id } } diff --git a/objectscript-lsp/crates/objectscript-core/src/parameter.rs b/objectscript-lsp/crates/objectscript-core/src/parameter.rs index 0b6a320..3c6b303 100644 --- a/objectscript-lsp/crates/objectscript-core/src/parameter.rs +++ b/objectscript-lsp/crates/objectscript-core/src/parameter.rs @@ -1,93 +1,61 @@ -use crate::common::{find_return_type, get_keyword_and_value, get_string_at_byte_range}; -use crate::parse_structures::{Parameter, TypeName}; -use tree_sitter::{Language as TsLanguage, Node, Query, QueryCursor, StreamingIterator}; -use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; +use crate::common::{ + get_node_children, get_string_at_byte_range, get_tracked_keywords, parse_return_type, +}; +use crate::parse_structures::Parameter; +use tree_sitter::Node; -impl Parameter { - pub fn new(name: String) -> Self { +impl Default for Parameter { + fn default() -> Self { Self { - name, is_final: None, + name: "TODO".to_string(), return_type: None, + default_value: None, } } +} - /// Given a parameter node, queries the keywords and assigns - /// the return type and the keywords - /// Returns (bool, bool) representing (is_public_changed, is_final_changed) - pub fn build_keywords( - &mut self, - node: Node, - content: &str, - old_class_is_final: Option, - class_is_final: Option, - ) -> bool { - let query_str = r#" - [(parameter_keyword) @keyword - (return_type (typename (identifier) @returntype )) - ]"#; - let language: &TsLanguage = &LANGUAGE_OBJECTSCRIPT_UDL.into(); - let mut is_final_changed = false; - if let Ok(query) = Query::new(language, query_str) { - let mut cursor = QueryCursor::new(); - let mut iter = cursor.matches(&query, node, content.as_bytes()); - let keyword_idx = query.capture_index_for_name("keyword"); - let returntype_idx = query.capture_index_for_name("returntype"); - let old_is_final = self.is_final.clone(); - let mut return_type_parameters = Vec::new(); - let mut saw_first_return_type = false; - let mut return_type_id = None; - while let Some(query_match) = iter.next() { - let mut i = 0; - while i < query_match.captures.len() { - let capture = &query_match.captures[i]; - if keyword_idx == Some(capture.index) { - if let Some(keyword_str) = - get_string_at_byte_range(content, capture.node.byte_range()) - { - let (not, keyword_name, _) = - get_keyword_and_value(keyword_str.as_str()); - if keyword_name == "final" { - if not { - self.is_final = Some(false); - } else { - self.is_final = Some(true); - } - } - } - i += 1; - continue; - } else if returntype_idx == Some(capture.index) { - let return_type_node = capture.node; - let Some(typename) = - get_string_at_byte_range(content, return_type_node.byte_range()) - else { - continue; - }; - if !saw_first_return_type { - return_type_id = Some(find_return_type(typename)); - saw_first_return_type = true; - } else { - return_type_parameters.push(typename); - } - i += 1; - continue; - } +pub fn build_parameter_struct(parameter_node: Node, content: &str) -> Option { + if parameter_node.kind() != "parameter" { + eprintln!( + "Error: build_parameter_struct was called for node {:?}, but it can only be called for parameter nodes", + parameter_node.kind() + ); + return None; + } + let mut parameter = Parameter::default(); + let parameter_children = get_node_children(parameter_node); + for parameter_child in parameter_children { + match parameter_child.kind() { + "parameter_name" => { + if let Some(parameter_name_node) = parameter_child.named_child(0) + && let Some(name) = + get_string_at_byte_range(content, parameter_name_node.byte_range()) + { + parameter.name = name; + } else { + eprintln!("Error: failed to get parameter name."); + return None; } } - if let Some(typename_id) = return_type_id { - let typename = TypeName { - ret_type: typename_id, - parameters: return_type_parameters, - }; - self.return_type = Some(typename); + "default_argument_value" => { + if let Some(val) = parameter_child.named_child(0) { + parameter.default_value = get_string_at_byte_range(content, val.byte_range()); + } } - let old_final_keyword_res = old_is_final.unwrap_or(old_class_is_final.unwrap_or(false)); - let new_final_keyword = self.is_final.unwrap_or(class_is_final.unwrap_or(false)); - if old_final_keyword_res != new_final_keyword { - is_final_changed = true; + "return_type" => { + parameter.return_type = parse_return_type(parameter_child, content); } + "parameter_keywords" => { + let tracked_keywords = get_tracked_keywords(parameter_child, content); + parameter.is_final = tracked_keywords.is_final; + } + _ => continue, } - is_final_changed } + if ¶meter.name == "TODO" { + eprintln!("error: failed to parse parameter name"); + return None; + } + Some(parameter) } diff --git a/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs b/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs index 4d2b239..bf5fc64 100644 --- a/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs +++ b/objectscript-lsp/crates/objectscript-core/src/parse_structures.rs @@ -43,6 +43,10 @@ pub struct ForeignKeyId(pub usize); #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct QueryId(pub usize); +/// Stores the Argument Index, which is assigned by `method.get_next_argument_id()`. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct ArgumentId(pub usize); + /// Stores the Index Index, which is assigned by `class.get_next_index_key_id()`. #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] pub struct IndexId(pub usize); @@ -79,6 +83,7 @@ pub enum MemberType { Xdata, Storage, ClassMethodCall, + ClientMethod, RelativeMethodCall, Query, Trigger, @@ -99,6 +104,55 @@ pub enum MemberType { ClassKeyword, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TrackedKeywords { + pub(crate) is_final: Option, + pub(crate) is_public: bool, + pub(crate) is_required: bool, + pub(crate) multidimensional: bool, + pub(crate) inverse: Option, + pub(crate) cardinality: Option, + pub(crate) on_delete: ForeignKeyAction, + pub(crate) on_update: ForeignKeyAction, + pub(crate) requires: Vec, + pub(crate) index_type: IndexType, + pub(crate) trigger_insert: bool, + pub(crate) trigger_delete: bool, + pub(crate) trigger_update: bool, + pub(crate) trigger_time: TriggerFire, + pub(crate) trigger_for_each: TriggerForEach, + pub(crate) language: Option, + pub(crate) code_mode: CodeMode, + pub(crate) procedure_block: Option, + pub(crate) public_variables_declared: HashSet, +} + +impl Default for TrackedKeywords { + fn default() -> Self { + Self { + is_final: None, + is_public: true, + is_required: false, + multidimensional: false, + inverse: None, + cardinality: None, + on_delete: ForeignKeyAction::NoAction, + on_update: ForeignKeyAction::NoAction, + requires: Vec::new(), + index_type: IndexType::Index, + trigger_insert: false, + trigger_update: false, + trigger_delete: false, + trigger_time: TriggerFire::BEFORE, + trigger_for_each: TriggerForEach::Row, + language: None, + code_mode: CodeMode::Code, + procedure_block: None, + public_variables_declared: HashSet::new(), + } + } +} + pub struct IndexParsers { pub cls: Parser, pub routine: Parser, @@ -225,6 +279,13 @@ pub struct StorageRef { pub id: StorageId, } +/// Reference to an argument in a method. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +pub struct ArgumentRef { + pub method: MethodRef, + pub id: ArgumentId, +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct Parameter { /// If true, the Parameter cannot be overwritten by subclasses. @@ -233,6 +294,8 @@ pub struct Parameter { pub name: String, /// Expected return type. pub return_type: Option, + /// Optional Default Value. + pub default_value: Option, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -249,6 +312,161 @@ pub struct Property { pub multidimensional: bool, /// Expected return type. pub return_type: Option, + /// Argument name -> Argument, Range + pub arguments: HashMap, + /// The next Id available for a new argument. + pub next_argument_id: usize, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ForeignKey { + /// ForeignKey Name. + pub name: String, + /// names of properties constrained by this key + pub properties_constrained: Vec, + /// name of class referenced + pub referenced_class: String, + /// name of index within referenced class + pub referenced_index: Option, + /// Specifies action that this foreign key should cause in the table when the key value of a record in the table is updated. + pub on_update: ForeignKeyAction, + /// Specifies action that this foreign key should cause in the table when the key value of a record in the table is deleted. + pub on_delete: ForeignKeyAction, +} + +#[derive(Clone, Debug, Eq, PartialEq, Copy)] +pub enum ForeignKeyAction { + NoAction, + SetDefault, + SetNull, + Cascade, +} + +#[derive(Clone, Debug, Eq, PartialEq, Copy)] +pub enum Cardinality { + Children, + Parent, + Many, + One, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Relationship { + /// Whether Relationship is required or not. + pub required: bool, + /// Whether Relationship is final or not. + pub is_final: Option, + /// Whether Relationship is public or not. + pub is_public: bool, + /// Relationship Name. + pub name: String, + /// Specifies the cardinality. + pub cardinality: Cardinality, + /// Expected return type. + pub return_type: Option, + /// Specifies the inverse side of this relationship. + pub inverse: String, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Query { + /// Specifies a list of privileges a user or process must have to call this query. + pub required_privileges: Vec, + /// Whether Query is final or not. + pub is_final: Option, + /// Whether Query is public or not. + pub is_public: bool, + /// Query Name. + pub name: String, + /// Specifies the query class used by this query. + pub return_type: TypeName, + /// Query argument declarations keyed by argument name. + pub arguments: HashMap, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Projection { + /// Whether Projection is final or not. + pub is_final: Option, + /// Projection Name. + pub name: String, + /// Specifies the query class used by this query. + pub return_type: TypeName, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Index { + /// Index Name. + pub name: String, + /// Properties that the index is based on + pub properties: Vec, + /// Specifies the Index Type, default is Index. + pub index_type: IndexType, + /// Specifies expected return type. + pub return_type: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Trigger { + /// Trigger Name. + pub name: String, + /// Stores CodeMode of Trigger. If None, Trigger defaults to Code. + pub code_mode: CodeMode, + /// If true, this trigger cannot be inherited by subclasses. + pub is_final: Option, + /// Stores language of trigger, defaults to ObjectScript. + pub language: Language, + /// if true, this trigger is fired during an SQL DELETE operation. + pub delete: bool, + /// if true, this trigger is fired during an SQL UPDATE operation. + pub update: bool, + /// if true, this trigger is fired during an SQL INSERT operation. + pub insert: bool, + /// Specifies whether Trigger Fires Before or After Event. Default is BEFORE. + pub time: TriggerFire, + /// Specifies when Trigger is Fired. Default is row. + pub for_each: TriggerForEach, +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct XData { + /// XData Block Name. + pub name: String, + /// Specifies the Language of the Xdata Block. Default is Xml. + pub language: Language, +} + +/// Specifies whether Trigger Fires Before or After Event. +#[derive(Clone, Debug, Eq, PartialEq, Copy)] +pub enum TriggerFire { + BEFORE, + AFTER, +} + +#[derive(Clone, Debug, Eq, PartialEq, Copy)] +pub enum TriggerForEach { + Row, // This trigger is fired by each row affected by the triggering statement. + RowObject, // This trigger is fired by each row affected by the triggering statement or by changes via object access. + Statement, // This trigger is fired once for the whole statement. +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct IndexPropertyValue { + pub name: String, + pub elements: bool, + pub keys: bool, + pub return_type: Option, +} + +#[derive(Clone, Debug, Eq, PartialEq, Copy)] +pub enum IndexType { + CollatedKey, + Bitslice, + Columnar, + Bitmap, + Index, + Key, + Extent, } impl PartialEq for MethodRef { @@ -327,26 +545,33 @@ pub struct Class { pub(crate) next_xdata_id: usize, pub(crate) next_projection_id: usize, pub(crate) next_storage_id: usize, - /// If true, this class and all of its members cannot be overwritten by subclasses. + /// If true, this class and all of its members cannot be inherited by subclasses. pub is_final: bool, } -/// Language keyword values supported for classes/methods. #[derive(Clone, Debug, Eq, PartialEq)] +pub struct Storage { + /// Storage Name + pub name: String, +} + +/// Language keyword values supported for classes/methods. +#[derive(Clone, Debug, Eq, PartialEq, Copy)] pub enum Language { Objectscript, TSql, - Python, ISpl, -} - -/// Semantic representation of a class parameter declaration. -#[derive(Clone, Debug, Eq, PartialEq)] -pub struct ClassParameter { - pub name: String, - pub property_type: Option, - pub default_argument_value: Option, // this can be a numeric literal, string literal, or identifier - pub range: Range, + Basic, + Json, + Html, + JavaScript, + Css, + Sql, + Java, + Python, + Xml, + Yaml, + Markdown, } /// Distinguishes instance methods from class methods. @@ -358,6 +583,7 @@ pub enum MethodType { Subroutine(bool), DottedSubroutine(bool), Routine, + ClientMethod, } /// Reference linking a variable to its public and/or private identifier. @@ -378,6 +604,8 @@ pub struct Method { pub name: String, /// Stores variable name -> VariableRef for all variable definitions in this method. pub variables: HashMap>, + /// Stores argument name -> Argument for all arguments in this method. + pub arguments: HashMap, /// Whether method is public or not. pub is_public: bool, /// Whether method is a procedure block or not. If None, method defaults to procedure block. @@ -390,6 +618,8 @@ pub struct Method { pub public_variables_declared: HashSet, /// If true, this method cannot be overwritten by subclasses. pub is_final: Option, + /// Tracks the next available id for an argument in the method. + pub next_argument_id: usize, } /// CodeMode keyword values supported for methods. @@ -410,6 +640,32 @@ pub struct ClassMethodCall { pub is_public: bool, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct Argument { + /// Argument name. + pub name: String, + /// Return Type + pub return_type: Option, + /// Default Value for Arg + pub default_value: Option, + /// Indicate that an argument should be passed by reference and is intended to have no incoming value. + pub output: bool, + /// If true, the method modifies the value of the variable outside the method. + pub byref: bool, +} + +impl Default for Argument { + fn default() -> Self { + Self { + name: "TODO".to_string(), + return_type: None, + default_value: None, + output: false, + byref: false, + } + } +} + /// Semantic representation of a variable discovered in a method. #[derive(Clone, Debug, Eq, PartialEq)] pub struct Variable { diff --git a/objectscript-lsp/crates/objectscript-core/src/projection.rs b/objectscript-lsp/crates/objectscript-core/src/projection.rs new file mode 100644 index 0000000..ada846b --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/projection.rs @@ -0,0 +1,57 @@ +use crate::common::{ + get_node_children, get_string_at_byte_range, get_tracked_keywords, parse_return_type, +}; +use crate::parse_structures::Projection; +use tree_sitter::Node; + +pub fn build_projection_struct(projection_node: Node, content: &str) -> Option { + if projection_node.kind() != "projection" { + eprintln!( + "Error: build_projection_struct was called for node {:?}, but it can only be called for projection nodes", + projection_node.kind() + ); + return None; + } + let mut is_final = None; + let mut projection_name = None; + let mut return_type = None; + let projection_children = get_node_children(projection_node); + for projection_child in projection_children { + match projection_child.kind() { + "projection_name" => { + if let Some(projection_name_node) = projection_child.named_child(0) { + projection_name = + get_string_at_byte_range(content, projection_name_node.byte_range()); + } else { + eprintln!("Error: failed to get projection name."); + return None; + } + } + "return_type" => { + return_type = parse_return_type(projection_child, content); + } + "projection_keywords" => { + let tracked_keywords = get_tracked_keywords(projection_child, content); + is_final = tracked_keywords.is_final; + } + _ => { + eprintln!( + "Error: Unrecognized projection child node {:?}", + projection_child.kind() + ); + continue; + } + } + } + if let Some(name) = projection_name + && let Some(return_type) = return_type + { + return Some(Projection { + name, + is_final, + return_type, + }); + } + + return None; +} diff --git a/objectscript-lsp/crates/objectscript-core/src/property.rs b/objectscript-lsp/crates/objectscript-core/src/property.rs index 2cd58ac..9b36635 100644 --- a/objectscript-lsp/crates/objectscript-core/src/property.rs +++ b/objectscript-lsp/crates/objectscript-core/src/property.rs @@ -1,119 +1,78 @@ -use crate::common::{find_return_type, get_keyword_and_value, get_string_at_byte_range}; -use crate::parse_structures::{Property, TypeName}; -use tree_sitter::{Language as TsLanguage, Node, Query, QueryCursor, StreamingIterator}; -use tree_sitter_objectscript::LANGUAGE_OBJECTSCRIPT_UDL; -impl Property { - pub fn new(name: String) -> Self { +use crate::common::{ + build_argument, get_node_children, get_string_at_byte_range, get_tracked_keywords, + parse_return_type, +}; +use crate::parse_structures::Property; +use std::collections::HashMap; +use tree_sitter::Node; + +impl Default for Property { + fn default() -> Self { Self { - name, required: false, + is_final: None, is_public: true, + name: "TODO".to_string(), multidimensional: false, return_type: None, - is_final: Some(false), + arguments: HashMap::new(), + next_argument_id: 0, } } +} - /// Given a property node, queries the keywords and assigns - /// the return type and the keywords - /// Returns (bool, bool) representing (is_public_changed, is_final_changed) - pub fn build_keywords( - &mut self, - node: Node, - content: &str, - old_class_is_final: Option, - class_is_final: Option, - ) -> (bool, bool) { - let query_str = r#" - [(property_keyword) @keyword - (return_type (typename (identifier) @returntype )) - ]"#; - let language: &TsLanguage = &LANGUAGE_OBJECTSCRIPT_UDL.into(); - let mut is_final_changed = false; - let mut privacy_changed = false; - if let Ok(query) = Query::new(language, query_str) { - let mut cursor = QueryCursor::new(); - let mut iter = cursor.matches(&query, node, content.as_bytes()); - let keyword_idx = query.capture_index_for_name("keyword"); - let returntype_idx = query.capture_index_for_name("returntype"); - let old_is_public = self.is_public.clone(); - let old_is_final = self.is_final.clone(); - let mut return_type_parameters = Vec::new(); - let mut saw_first_return_type = false; - let mut return_type_id = None; - while let Some(query_match) = iter.next() { - let mut i = 0; - while i < query_match.captures.len() { - let capture = &query_match.captures[i]; - if keyword_idx == Some(capture.index) { - if let Some(keyword_str) = - get_string_at_byte_range(content, capture.node.byte_range()) - { - let (not, keyword_name, _) = - get_keyword_and_value(keyword_str.as_str()); - if keyword_name == "final" { - if not { - self.is_final = Some(false); - } else { - self.is_final = Some(true); - } - } else if keyword_name == "private" { - if not { - self.is_public = true; - } else { - self.is_public = false; - } - } else if keyword_name == "required" { - if not { - self.required = true; - } else { - self.required = false; - } - } else if keyword_name == "multidimensional" { - if not { - self.multidimensional = true; - } else { - self.multidimensional = false; - } - } - } - i += 1; - continue; - } else if returntype_idx == Some(capture.index) { - let return_type_node = capture.node; - let Some(typename) = - get_string_at_byte_range(content, return_type_node.byte_range()) - else { - i += 1; - continue; - }; - if !saw_first_return_type { - return_type_id = Some(find_return_type(typename)); - saw_first_return_type = true; - } else { - return_type_parameters.push(typename); - } - i += 1; - continue; - } +pub fn build_property_struct(property_node: Node, content: &str) -> Option { + if property_node.kind() != "property" { + eprintln!( + "Error: build_property_struct was called for node {:?}, but it can only be called for property nodes", + property_node.kind() + ); + return None; + } + let mut property = Property::default(); + let property_children = get_node_children(property_node); + for property_child in property_children { + match property_child.kind() { + "property_name" => { + if let Some(property_name_node) = property_child.named_child(0) + && let Some(name) = + get_string_at_byte_range(content, property_name_node.byte_range()) + { + property.name = name; + } else { + eprintln!("Error: failed to get property name."); + return None; } } - if let Some(typename_id) = return_type_id { - let typename = TypeName { - ret_type: typename_id, - parameters: return_type_parameters, - }; - self.return_type = Some(typename); + "arguments" => { + let argument_children = get_node_children(property_child); + for argument in argument_children { + if let Some((argument_struct, argument_range)) = + build_argument(argument, content) + { + property.arguments.insert( + argument_struct.name.clone(), + (argument_struct, argument_range), + ); + } + } } - let old_final_keyword_res = old_is_final.unwrap_or(old_class_is_final.unwrap_or(false)); - let new_final_keyword = self.is_final.unwrap_or(class_is_final.unwrap_or(false)); - if old_final_keyword_res != new_final_keyword { - is_final_changed = true; + "return_type" => { + property.return_type = parse_return_type(property_child, content); } - if old_is_public != self.is_public { - privacy_changed = true; + "property_keywords" => { + let tracked_keywords = get_tracked_keywords(property_child, content); + property.is_final = tracked_keywords.is_final; + property.multidimensional = tracked_keywords.multidimensional; + property.required = tracked_keywords.is_required; + property.is_public = tracked_keywords.is_public; } + _ => continue, } - (is_final_changed, privacy_changed) } + if &property.name == "TODO" { + eprintln!("error: failed to parse property name"); + return None; + } + Some(property) } diff --git a/objectscript-lsp/crates/objectscript-core/src/query.rs b/objectscript-lsp/crates/objectscript-core/src/query.rs new file mode 100644 index 0000000..610bce4 --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/query.rs @@ -0,0 +1,80 @@ +use crate::common::{ + build_argument, get_node_children, get_string_at_byte_range, get_tracked_keywords, + parse_return_type, +}; +use crate::parse_structures::Query; +use std::collections::HashMap; +use tree_sitter::Node; + +pub fn build_query_struct(query_node: Node, content: &str) -> Option { + if query_node.kind() != "query" { + eprintln!( + "Error: build_query_struct was called for node {:?}, but it can only be called for query nodes", + query_node.kind() + ); + return None; + } + let mut required_privileges = Vec::new(); + let mut is_final = None; + let mut is_public = true; + let mut query_name = None; + let mut return_type = None; + let mut arguments = HashMap::new(); + let query_children = get_node_children(query_node); + for query_child in query_children { + match query_child.kind() { + "query_name" => { + if let Some(query_name_node) = query_child.named_child(0) { + query_name = get_string_at_byte_range(content, query_name_node.byte_range()); + } else { + eprintln!("Error: failed to get query name."); + return None; + } + } + "return_type" => { + return_type = parse_return_type(query_child, content); + } + "query_keywords" => { + let tracked_keywords = get_tracked_keywords(query_child, content); + is_final = tracked_keywords.is_final; + required_privileges = tracked_keywords.requires; + is_public = tracked_keywords.is_public; + } + "keyword_query" | "external_method_body_content" => {} + "arguments" => { + let argument_children = get_node_children(query_child); + for argument in argument_children { + if let Some((argument_struct, argument_range)) = + build_argument(argument, content) + { + arguments.insert( + argument_struct.name.clone(), + (argument_struct, argument_range), + ); + } + } + } + _ => { + eprintln!( + "Error: Unrecognized query child node {:?}", + query_child.kind() + ); + continue; + } + } + } + if let Some(name) = query_name + && let Some(return_type) = return_type + { + return Some(Query { + required_privileges, + is_final, + is_public, + name, + return_type, + arguments, + }); + } + + return None; +} diff --git a/objectscript-lsp/crates/objectscript-core/src/relationship.rs b/objectscript-lsp/crates/objectscript-core/src/relationship.rs new file mode 100644 index 0000000..8f8aef2 --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/relationship.rs @@ -0,0 +1,81 @@ +use crate::common::{ + get_node_children, get_string_at_byte_range, get_tracked_keywords, parse_return_type, +}; +use crate::parse_structures::{Cardinality, Relationship}; +use tree_sitter::Node; + +impl Default for Relationship { + fn default() -> Self { + Self { + required: false, + is_final: None, + is_public: true, + name: "TODO".to_string(), + cardinality: Cardinality::One, + return_type: None, + inverse: "TODO".to_string(), + } + } +} +pub fn build_relationship_struct(relationship_node: Node, content: &str) -> Option { + if relationship_node.kind() != "relationship" { + eprintln!( + "Error: build_relationship_struct was called for node {:?}, but it can only be called for relationship nodes", + relationship_node.kind() + ); + return None; + } + let mut relationship = Relationship::default(); + let relationship_children = get_node_children(relationship_node); + for relationship_child in relationship_children { + match relationship_child.kind() { + "relationship_name" => { + if let Some(relationship_name_node) = relationship_child.named_child(0) + && let Some(name) = + get_string_at_byte_range(content, relationship_name_node.byte_range()) + { + relationship.name = name; + } else { + eprintln!("Error: failed to get relationship name."); + return None; + } + } + "return_type" => { + relationship.return_type = parse_return_type(relationship_child, content); + } + "relationship_keywords" => { + let tracked_keywords = get_tracked_keywords(relationship_child, content); + if let Some(cardinality) = tracked_keywords.cardinality + && let Some(inverse) = tracked_keywords.inverse + { + relationship.cardinality = cardinality; + relationship.inverse = inverse; + } else { + eprintln!( + "Error: Relationship requires cardinality and inverse keywords to be defined" + ); + return None; + } + relationship.is_final = tracked_keywords.is_final; + relationship.required = tracked_keywords.is_required; + relationship.is_public = tracked_keywords.is_public; + } + "keyword_relationship" => {} + _ => { + eprintln!( + "Error: Unrecognized relationship child node {:?}", + relationship_child.kind() + ); + continue; + } + } + } + if &relationship.name == "TODO" || &relationship.inverse == "TODO" { + eprintln!( + "error: failed to parse relationship name {:?} or inverse keyword {:?}", + &relationship.name, &relationship.inverse + ); + return None; + } + Some(relationship) +} diff --git a/objectscript-lsp/crates/objectscript-core/src/scope_structures.rs b/objectscript-lsp/crates/objectscript-core/src/scope_structures.rs index c5cd97a..bbceffe 100644 --- a/objectscript-lsp/crates/objectscript-core/src/scope_structures.rs +++ b/objectscript-lsp/crates/objectscript-core/src/scope_structures.rs @@ -43,6 +43,24 @@ pub struct PropertySymbol { pub references: Vec, } +/// Definition site for a named class member. +#[derive(Clone, Debug)] +pub struct MemberSymbol { + pub name: String, + pub url: Url, + pub location: Range, + pub references: Vec, +} + +pub type RelationshipSymbol = MemberSymbol; +pub type ForeignKeySymbol = MemberSymbol; +pub type QuerySymbol = MemberSymbol; +pub type IndexSymbol = MemberSymbol; +pub type TriggerSymbol = MemberSymbol; +pub type XdataSymbol = MemberSymbol; +pub type ProjectionSymbol = MemberSymbol; +pub type StorageSymbol = MemberSymbol; + /// A class definition symbol (definition site + liveness flag). #[derive(Clone, Debug)] pub struct ClassGlobalSymbol { diff --git a/objectscript-lsp/crates/objectscript-core/src/scope_tree.rs b/objectscript-lsp/crates/objectscript-core/src/scope_tree.rs index eb330d1..4a4c9ce 100644 --- a/objectscript-lsp/crates/objectscript-core/src/scope_tree.rs +++ b/objectscript-lsp/crates/objectscript-core/src/scope_tree.rs @@ -1,5 +1,5 @@ use crate::common::{generic_exit_statements, point_in_range}; -use crate::parse_structures::{MethodRef, PropertyRef, VariableRef}; +use crate::parse_structures::{MethodRef, PropertyRef, QueryRef, RelationshipRef, VariableRef}; use crate::scope_structures::*; use std::collections::{HashMap, HashSet}; use tower_lsp::lsp_types::Url; @@ -151,6 +151,8 @@ pub struct ScopeTree { pub private_method_defs: HashMap, /// Stores PropertyRef -> Property Symbol for all private properties in the document. pub private_property_defs: HashMap, + pub private_relationship_defs: HashMap, + pub private_query_defs: HashMap, } impl Clone for ScopeTree { @@ -162,6 +164,8 @@ impl Clone for ScopeTree { next_scope_id: self.next_scope_id, private_method_defs: self.private_method_defs.clone(), private_property_defs: self.private_property_defs.clone(), + private_relationship_defs: self.private_relationship_defs.clone(), + private_query_defs: self.private_query_defs.clone(), } } } @@ -188,6 +192,8 @@ impl ScopeTree { next_scope_id: 1, private_method_defs: HashMap::new(), private_property_defs: HashMap::new(), + private_relationship_defs: HashMap::new(), + private_query_defs: HashMap::new(), } } @@ -248,6 +254,17 @@ impl ScopeTree { self.private_method_defs.get(method_ref) } + pub fn get_private_relationship_symbol( + &self, + member_ref: &RelationshipRef, + ) -> Option<&RelationshipSymbol> { + self.private_relationship_defs.get(member_ref) + } + + pub fn get_private_query_symbol(&self, member_ref: &QueryRef) -> Option<&QuerySymbol> { + self.private_query_defs.get(member_ref) + } + /// Add a new child scope to `parent`, returning the new `ScopeId`. pub fn add_scope( &mut self, @@ -412,6 +429,42 @@ impl ScopeTree { self.private_property_defs .insert(property_ref, property_symbol); } + + pub fn new_relationship_symbol( + &mut self, + name: String, + location: Range, + member_ref: RelationshipRef, + url: Url, + ) { + self.private_relationship_defs.insert( + member_ref, + MemberSymbol { + name, + url, + location, + references: Vec::new(), + }, + ); + } + + pub fn new_query_symbol( + &mut self, + name: String, + location: Range, + member_ref: QueryRef, + url: Url, + ) { + self.private_query_defs.insert( + member_ref, + MemberSymbol { + name, + url, + location, + references: Vec::new(), + }, + ); + } /// Get an immutable reference to the innermost scope containing `point`. /// /// Logs a warning and returns `None` if no containing scope is found. diff --git a/objectscript-lsp/crates/objectscript-core/src/storage.rs b/objectscript-lsp/crates/objectscript-core/src/storage.rs new file mode 100644 index 0000000..532562a --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/storage.rs @@ -0,0 +1,20 @@ +use crate::common::get_string_at_byte_range; +use crate::parse_structures::Storage; +use tree_sitter::Node; +pub fn build_storage_struct(storage_node: Node, content: &str) -> Option { + if storage_node.kind() != "storage" { + eprintln!( + "Error: build_storage_struct was called for node {:?}, but it can only be called for storage nodes", + storage_node.kind() + ); + return None; + } + if let Some(storage_name_outer_node) = storage_node.named_child(1) + && storage_name_outer_node.kind() == "storage_name" + && let Some(storage_name_node) = storage_name_outer_node.named_child(0) + && let Some(name) = get_string_at_byte_range(content, storage_name_node.byte_range()) + { + return Some(Storage { name }); + } + None +} diff --git a/objectscript-lsp/crates/objectscript-core/src/trigger.rs b/objectscript-lsp/crates/objectscript-core/src/trigger.rs new file mode 100644 index 0000000..7ccacc6 --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/trigger.rs @@ -0,0 +1,68 @@ +use crate::common::{get_node_children, get_string_at_byte_range, get_tracked_keywords}; +use crate::parse_structures::{CodeMode, Language, Trigger, TriggerFire, TriggerForEach}; +use tree_sitter::Node; +impl Default for Trigger { + fn default() -> Self { + Self { + name: "TODO".to_string(), + code_mode: CodeMode::Code, + is_final: None, + language: Language::Objectscript, + delete: false, + update: false, + insert: false, + time: TriggerFire::BEFORE, + for_each: TriggerForEach::Row, + } + } +} + +pub fn build_trigger_struct(trigger_node: Node, content: &str) -> Option { + if trigger_node.kind() != "trigger" { + eprintln!( + "Error: build_trigger_struct was called for node {:?}, but it can only be called for trigger nodes", + trigger_node.kind() + ); + return None; + } + let mut trigger = Trigger::default(); + let trigger_children = get_node_children(trigger_node); + for trigger_child in trigger_children { + match trigger_child.kind() { + "trigger_name" => { + if let Some(trigger_name_node) = trigger_child.named_child(0) + && let Some(name) = + get_string_at_byte_range(content, trigger_name_node.byte_range()) + { + trigger.name = name; + } else { + eprintln!("Error: failed to get trigger name."); + return None; + } + } + "trigger_keywords" | "external_trigger_keywords" => { + let tracked_keywords = get_tracked_keywords(trigger_child, content); + trigger.update = tracked_keywords.trigger_update; + trigger.delete = tracked_keywords.trigger_delete; + trigger.insert = tracked_keywords.trigger_insert; + trigger.for_each = tracked_keywords.trigger_for_each; + trigger.is_final = tracked_keywords.is_final; + trigger.language = tracked_keywords.language.unwrap_or(Language::Objectscript); + trigger.time = tracked_keywords.trigger_time; + trigger.code_mode = tracked_keywords.code_mode; + } + _ => { + // eprintln!( + // "Error: Unrecognized trigger child node {:?}", + // trigger_child.kind() + // ); + continue; + } + } + } + if &trigger.name == "TODO" { + eprintln!("error: failed to parse trigger name"); + return None; + } + Some(trigger) +} diff --git a/objectscript-lsp/crates/objectscript-core/src/workspace.rs b/objectscript-lsp/crates/objectscript-core/src/workspace.rs index 26621e6..4c4d39c 100644 --- a/objectscript-lsp/crates/objectscript-core/src/workspace.rs +++ b/objectscript-lsp/crates/objectscript-core/src/workspace.rs @@ -9,9 +9,11 @@ use crate::global_semantic::GlobalSemanticModel; use crate::local_semantic::LocalSemanticModel; use crate::override_index::OverrideIndex; use crate::parse_structures::{ - Class, ClassId, FileType, InheritanceDirection, Method, MethodRef, MethodType, Parameter, - ParameterRef, Property, PropertyRef, RefactorLevel, UnresolvedMethodRef, Variable, - VariableDefType, VariableRef, + Class, ClassId, FileType, ForeignKey, ForeignKeyRef, Index, IndexRef, InheritanceDirection, + Method, MethodRef, MethodType, Parameter, ParameterRef, Projection, ProjectionRef, Property, + PropertyRef, Query, QueryRef, RefactorLevel, Relationship, RelationshipRef, Storage, + StorageRef, Trigger, TriggerRef, UnresolvedMethodRef, Variable, VariableDefType, VariableRef, + XData, XdataRef, }; use crate::refactor::{ refactor_conditionals_in_document, refactor_for_statements, refactor_legacy_do_statements, @@ -124,6 +126,14 @@ pub struct ProjectData { pub property_defs: HashMap>, /// Maps Class Name -> another hashmap which maps Parameter Name -> ParameterRef for all Parameters Accessible from the class. pub parameter_defs: HashMap>, + pub relationship_defs: HashMap>, + pub foreignkey_defs: HashMap>, + pub query_defs: HashMap>, + pub index_defs: HashMap>, + pub trigger_defs: HashMap>, + pub xdata_defs: HashMap>, + pub projection_defs: HashMap>, + pub storage_defs: HashMap>, /// Maps Var Name -> another hashmap which maps MethodRef -> HashMap of ScopeId -> Vec for that variable. pub pub_var_defs: HashMap>>>, /// Holds the OverrideIndex for the workspace. @@ -185,6 +195,14 @@ struct PendingBulkClass { class: Class, properties: HashMap, parameters: HashMap, + relationships: HashMap, + foreignkeys: HashMap, + queries: HashMap, + indices: HashMap, + triggers: HashMap, + xdata: HashMap, + projections: HashMap, + storage: HashMap, methods: HashMap, diagnostics: Vec, } @@ -253,7 +271,24 @@ impl<'a> BulkWorkspaceIndex<'a> { .expect("registered class document must contain a class definition") }; let mut class = Class::new(class_name.clone(), is_rtn); - let (_, _, methods, properties, parameters, _, _, diagnostics) = class.build_class( + let ( + _, + _, + methods, + properties, + parameters, + relationships, + foreignkeys, + queries, + indices, + triggers, + xdata, + projections, + storage, + _, + _, + diagnostics, + ) = class.build_class( starting_node, &document.content, is_rtn, @@ -316,6 +351,14 @@ impl<'a> BulkWorkspaceIndex<'a> { class, properties, parameters, + relationships, + foreignkeys, + queries, + indices, + triggers, + xdata, + projections, + storage, methods, diagnostics, } @@ -392,6 +435,86 @@ impl<'a> BulkWorkspaceIndex<'a> { .or_default() .insert(name, parameter_ref); } + for (name, (value, range, member_ref)) in prepared.relationships { + if !value.is_public { + prepared.document.scope_tree.new_relationship_symbol( + name.clone(), + range, + member_ref, + url.clone(), + ); + } + data.global_semantic_model + .new_relationship(value, member_ref, range, url.clone()); + data.relationship_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in prepared.foreignkeys { + data.global_semantic_model + .new_foreignkey(value, member_ref, range, url.clone()); + data.foreignkey_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in prepared.queries { + if !value.is_public { + prepared.document.scope_tree.new_query_symbol( + name.clone(), + range, + member_ref, + url.clone(), + ); + } + data.global_semantic_model + .new_query(value, member_ref, range, url.clone()); + data.query_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in prepared.indices { + data.global_semantic_model + .new_index(value, member_ref, range, url.clone()); + data.index_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in prepared.triggers { + data.global_semantic_model + .new_trigger(value, member_ref, range, url.clone()); + data.trigger_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in prepared.xdata { + data.global_semantic_model + .new_xdata(value, member_ref, range, url.clone()); + data.xdata_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in prepared.projections { + data.global_semantic_model + .new_projection(value, member_ref, range, url.clone()); + data.projection_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in prepared.storage { + data.global_semantic_model + .new_storage(value, member_ref, range, url.clone()); + data.storage_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } let mut calls = Vec::new(); let mut oref_calls = Vec::new(); for (name, mut method) in prepared.methods { @@ -1130,6 +1253,46 @@ impl ProjectData { .remove_parameter(&stale_parameter_ref); } } + if let Some(values) = self.relationship_defs.remove(&class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_relationship(&member_ref); + } + } + if let Some(values) = self.foreignkey_defs.remove(&class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_foreignkey(&member_ref); + } + } + if let Some(values) = self.query_defs.remove(&class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_query(&member_ref); + } + } + if let Some(values) = self.index_defs.remove(&class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_index(&member_ref); + } + } + if let Some(values) = self.trigger_defs.remove(&class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_trigger(&member_ref); + } + } + if let Some(values) = self.xdata_defs.remove(&class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_xdata(&member_ref); + } + } + if let Some(values) = self.projection_defs.remove(&class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_projection(&member_ref); + } + } + if let Some(values) = self.storage_defs.remove(&class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_storage(&member_ref); + } + } if let Some(class) = self.global_semantic_model.get_mut_class(class_id) { class.clear(class_name, false); @@ -1258,15 +1421,31 @@ impl ProjectData { node }; // this is a new class, so some things returned from this function are not applicable - let (_, _, methods, properties, parameters, inherited_classes, _, class_diagnostics) = - class.build_class( - starting_node, - content, - is_rtn, - &class_id, - class_range, - &class_name, - ); + let ( + _, + _, + methods, + properties, + parameters, + relationships, + foreignkeys, + queries, + indices, + triggers, + xdata, + projections, + storage, + inherited_classes, + _, + class_diagnostics, + ) = class.build_class( + starting_node, + content, + is_rtn, + &class_id, + class_range, + &class_name, + ); class.build_imports(tree, content); @@ -1307,6 +1486,9 @@ impl ProjectData { let mut unresolved_method_refs = HashSet::new(); let mut unresolved_oref_method_refs = HashSet::new(); match method_type { + MethodType::ClientMethod => { + continue; + } MethodType::ClassMethod | MethodType::InstanceMethod => { if let Some(method_definition_node) = tree.root_node().named_descendant_for_byte_range( @@ -1576,6 +1758,86 @@ impl ProjectData { class_is_final, ); } + for (name, (value, range, member_ref)) in relationships { + if !value.is_public { + document.scope_tree.new_relationship_symbol( + name.clone(), + range, + member_ref, + url.clone(), + ); + } + self.global_semantic_model + .new_relationship(value, member_ref, range, url.clone()); + self.relationship_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in foreignkeys { + self.global_semantic_model + .new_foreignkey(value, member_ref, range, url.clone()); + self.foreignkey_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in queries { + if !value.is_public { + document.scope_tree.new_query_symbol( + name.clone(), + range, + member_ref, + url.clone(), + ); + } + self.global_semantic_model + .new_query(value, member_ref, range, url.clone()); + self.query_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in indices { + self.global_semantic_model + .new_index(value, member_ref, range, url.clone()); + self.index_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in triggers { + self.global_semantic_model + .new_trigger(value, member_ref, range, url.clone()); + self.trigger_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in xdata { + self.global_semantic_model + .new_xdata(value, member_ref, range, url.clone()); + self.xdata_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in projections { + self.global_semantic_model + .new_projection(value, member_ref, range, url.clone()); + self.projection_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in storage { + self.global_semantic_model + .new_storage(value, member_ref, range, url.clone()); + self.storage_defs + .entry(class_name.clone()) + .or_default() + .insert(name, member_ref); + } self.documents.insert(url.clone(), document); } } @@ -2290,6 +2552,46 @@ impl ProjectData { &subclasses_to_recompute_inheritance, old_class_is_final, ); + if let Some(values) = self.relationship_defs.remove(old_class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_relationship(&member_ref); + } + } + if let Some(values) = self.foreignkey_defs.remove(old_class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_foreignkey(&member_ref); + } + } + if let Some(values) = self.query_defs.remove(old_class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_query(&member_ref); + } + } + if let Some(values) = self.index_defs.remove(old_class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_index(&member_ref); + } + } + if let Some(values) = self.trigger_defs.remove(old_class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_trigger(&member_ref); + } + } + if let Some(values) = self.xdata_defs.remove(old_class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_xdata(&member_ref); + } + } + if let Some(values) = self.projection_defs.remove(old_class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_projection(&member_ref); + } + } + if let Some(values) = self.storage_defs.remove(old_class_name) { + for (_, member_ref) in values { + self.global_semantic_model.remove_storage(&member_ref); + } + } } /// Returns true if successful, false otherwise @@ -2459,6 +2761,14 @@ impl ProjectData { new_methods, properties_already_rebuilt, parameters_already_rebuilt, + relationships_already_rebuilt, + foreignkeys_already_rebuilt, + queries_already_rebuilt, + indices_already_rebuilt, + triggers_already_rebuilt, + xdata_already_rebuilt, + projections_already_rebuilt, + storage_already_rebuilt, new_inherited_classes, all_methods, class_diagnostics, @@ -2582,6 +2892,9 @@ impl ProjectData { self.dependency_graph.get_or_add_node(method_ref); let method_type = method.method_type.clone(); match method_type { + MethodType::ClientMethod => { + continue; + } MethodType::ClassMethod | MethodType::InstanceMethod => { if let Some(method_definition_node) = tree.root_node().named_descendant_for_byte_range( @@ -2859,6 +3172,81 @@ impl ProjectData { new_class_is_final, ); } + for (name, (value, range, member_ref)) in relationships_already_rebuilt { + if !value.is_public { + scope_tree.new_relationship_symbol( + name.clone(), + range, + member_ref, + url.clone(), + ); + } + self.global_semantic_model + .new_relationship(value, member_ref, range, url.clone()); + self.relationship_defs + .entry(new_class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in foreignkeys_already_rebuilt { + self.global_semantic_model + .new_foreignkey(value, member_ref, range, url.clone()); + self.foreignkey_defs + .entry(new_class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in queries_already_rebuilt { + if !value.is_public { + scope_tree.new_query_symbol(name.clone(), range, member_ref, url.clone()); + } + self.global_semantic_model + .new_query(value, member_ref, range, url.clone()); + self.query_defs + .entry(new_class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in indices_already_rebuilt { + self.global_semantic_model + .new_index(value, member_ref, range, url.clone()); + self.index_defs + .entry(new_class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in triggers_already_rebuilt { + self.global_semantic_model + .new_trigger(value, member_ref, range, url.clone()); + self.trigger_defs + .entry(new_class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in xdata_already_rebuilt { + self.global_semantic_model + .new_xdata(value, member_ref, range, url.clone()); + self.xdata_defs + .entry(new_class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in projections_already_rebuilt { + self.global_semantic_model + .new_projection(value, member_ref, range, url.clone()); + self.projection_defs + .entry(new_class_name.clone()) + .or_default() + .insert(name, member_ref); + } + for (name, (value, range, member_ref)) in storage_already_rebuilt { + self.global_semantic_model + .new_storage(value, member_ref, range, url.clone()); + self.storage_defs + .entry(new_class_name.clone()) + .or_default() + .insert(name, member_ref); + } let scope_tree_snapshot = scope_tree.clone(); let mut curr_class_hash = HashSet::new(); @@ -2907,6 +3295,7 @@ impl ProjectData { if let Some(method) = self.global_semantic_model.get_mut_method(&method_ref) { match method_type { + MethodType::ClientMethod => {} MethodType::ClassMethod | MethodType::InstanceMethod => { if let Some(method_definition_node) = tree.root_node().named_descendant_for_byte_range( @@ -5197,6 +5586,14 @@ impl ProjectState { method_defs: HashMap::new(), pub_var_defs: HashMap::new(), parameter_defs: HashMap::new(), + relationship_defs: HashMap::new(), + foreignkey_defs: HashMap::new(), + query_defs: HashMap::new(), + index_defs: HashMap::new(), + trigger_defs: HashMap::new(), + xdata_defs: HashMap::new(), + projection_defs: HashMap::new(), + storage_defs: HashMap::new(), property_defs: HashMap::new(), override_index: OverrideIndex::new(), dependent_class_index: Dependents::new(), diff --git a/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs b/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs index 7725ea4..cef8f29 100644 --- a/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs +++ b/objectscript-lsp/crates/objectscript-core/src/workspace_diff.rs @@ -1,7 +1,8 @@ use crate::document::Document; use crate::parse_structures::{ - CodeMode, InheritanceDirection, Language, Method, MethodType, Parameter, Property, TypeName, - VariableDefType, + Argument, CodeMode, ForeignKey, Index, InheritanceDirection, Language, Method, MethodType, + Parameter, Projection, Property, Query, Relationship, Storage, Trigger, TypeName, + VariableDefType, XData, }; use crate::workspace::ProjectData; use rayon::prelude::*; @@ -13,13 +14,23 @@ pub struct ValueChange { pub after: T, } -#[derive(Clone, Debug, Default, Eq, PartialEq)] +#[derive(Clone, Debug, Eq, PartialEq)] pub struct MemberChanges { pub added: Vec, pub removed: Vec, pub changed: BTreeMap, } +impl Default for MemberChanges { + fn default() -> Self { + Self { + added: Vec::new(), + removed: Vec::new(), + changed: BTreeMap::new(), + } + } +} + #[derive(Clone, Debug, Eq, PartialEq)] pub struct CollectionChanges { pub added: Vec, @@ -43,6 +54,33 @@ pub struct VariableSnapshot { pub variable_type: VariableDefType, } +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct ArgumentSnapshot { + pub name: String, + pub return_type: Option, + pub default_value: Option, + pub output: bool, + pub byref: bool, +} + +pub type ArgumentDiff = ValueChange; +pub type RelationshipSnapshot = Relationship; +pub type ForeignKeySnapshot = ForeignKey; +pub type QuerySnapshot = Query; +pub type IndexSnapshot = Index; +pub type TriggerSnapshot = Trigger; +pub type XDataSnapshot = XData; +pub type ProjectionSnapshot = Projection; +pub type StorageSnapshot = Storage; +pub type RelationshipDiff = ValueChange; +pub type ForeignKeyDiff = ValueChange; +pub type QueryDiff = ValueChange; +pub type IndexDiff = ValueChange; +pub type TriggerDiff = ValueChange; +pub type XDataDiff = ValueChange; +pub type ProjectionDiff = ValueChange; +pub type StorageDiff = ValueChange; + #[derive(Clone, Debug, Eq, PartialEq)] pub struct MethodSnapshot { pub method_type: MethodType, @@ -54,6 +92,7 @@ pub struct MethodSnapshot { pub public_variables_declared: Vec, pub final_keyword: Option, pub variables: Vec, + pub arguments: BTreeMap, /// Exact declaration/body source for implementation-level comparison. pub implementation: Option, } @@ -65,6 +104,7 @@ pub struct PropertySnapshot { pub final_keyword: Option, pub multidimensional: bool, pub return_type: Option, + pub arguments: BTreeMap, } #[derive(Clone, Debug, Eq, PartialEq)] @@ -87,6 +127,14 @@ pub struct ClassSnapshot { pub methods: BTreeMap, pub properties: BTreeMap, pub parameters: BTreeMap, + pub relationships: BTreeMap, + pub foreign_keys: BTreeMap, + pub queries: BTreeMap, + pub indices: BTreeMap, + pub triggers: BTreeMap, + pub xdata: BTreeMap, + pub projections: BTreeMap, + pub storage: BTreeMap, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -100,6 +148,7 @@ pub struct MethodDiff { pub public_variables_declared: Option>>, pub final_keyword: Option>>, pub variables: CollectionChanges, + pub arguments: MemberChanges, /// Exact method declaration/body source changed. pub source_changed: bool, } @@ -111,6 +160,7 @@ pub struct PropertyDiff { pub final_keyword: Option>>, pub multidimensional: Option>, pub return_type: Option>>, + pub arguments: MemberChanges, } #[derive(Clone, Debug, Default, Eq, PartialEq)] @@ -132,6 +182,14 @@ pub struct ClassDiff { pub methods: MemberChanges, pub properties: MemberChanges, pub parameters: MemberChanges, + pub relationships: MemberChanges, + pub foreign_keys: MemberChanges, + pub queries: MemberChanges, + pub indices: MemberChanges, + pub triggers: MemberChanges, + pub xdata: MemberChanges, + pub projections: MemberChanges, + pub storage: MemberChanges, /// Source changed, but no difference is represented by the current model. pub unmodeled_source_change: bool, } @@ -322,6 +380,19 @@ pub fn snapshot_class(data: &ProjectData, class_name: &str) -> Option { + class + .$field + .iter() + .filter_map(|(name, member_ref)| { + data.global_semantic_model + .$getter(member_ref) + .map(|member| (name.clone(), member.clone())) + }) + .collect() + }; + } Some(ClassSnapshot { name: class.name.clone(), @@ -335,6 +406,14 @@ pub fn snapshot_class(data: &ProjectData, class_name: &str) -> Option PropertySnapshot { final_keyword: property.is_final, multidimensional: property.multidimensional, return_type: property.return_type.clone(), + arguments: snapshot_arguments(&property.arguments), } } +fn snapshot_arguments( + arguments: &std::collections::HashMap, +) -> BTreeMap { + arguments + .iter() + .map(|(name, (argument, _))| { + ( + name.clone(), + ArgumentSnapshot { + name: argument.name.clone(), + return_type: argument.return_type.clone(), + default_value: argument.default_value.clone(), + output: argument.output, + byref: argument.byref, + }, + ) + }) + .collect() +} + fn snapshot_parameter(parameter: &Parameter) -> ParameterSnapshot { ParameterSnapshot { final_keyword: parameter.is_final, @@ -412,6 +513,11 @@ fn diff_class(before: ClassSnapshot, after: ClassSnapshot) -> ClassDiff { let methods = diff_members(&before.methods, &after.methods, diff_method); let properties = diff_members(&before.properties, &after.properties, diff_property); let parameters = diff_members(&before.parameters, &after.parameters, diff_parameter); + macro_rules! value_member_diff { + ($field:ident) => { + diff_members(&before.$field, &after.$field, |a, b| value_change(a, b)) + }; + } let mut result = ClassDiff { class_name: before.name.clone(), imports: value_change(&before.imports, &after.imports), @@ -430,6 +536,14 @@ fn diff_class(before: ClassSnapshot, after: ClassSnapshot) -> ClassDiff { methods, properties, parameters, + relationships: value_member_diff!(relationships), + foreign_keys: value_member_diff!(foreign_keys), + queries: value_member_diff!(queries), + indices: value_member_diff!(indices), + triggers: value_member_diff!(triggers), + xdata: value_member_diff!(xdata), + projections: value_member_diff!(projections), + storage: value_member_diff!(storage), unmodeled_source_change: false, }; result.unmodeled_source_change = !result.has_semantic_changes(); @@ -448,11 +562,22 @@ impl ClassDiff { || has_member_changes(&self.methods) || has_member_changes(&self.properties) || has_member_changes(&self.parameters) + || has_member_changes(&self.relationships) + || has_member_changes(&self.foreign_keys) + || has_member_changes(&self.queries) + || has_member_changes(&self.indices) + || has_member_changes(&self.triggers) + || has_member_changes(&self.xdata) + || has_member_changes(&self.projections) + || has_member_changes(&self.storage) } } fn diff_method(before: &MethodSnapshot, after: &MethodSnapshot) -> Option { let variables = multiset_changes(&before.variables, &after.variables); + let arguments = diff_members(&before.arguments, &after.arguments, |a, b| { + value_change(a, b) + }); let result = MethodDiff { method_type: value_change(&before.method_type, &after.method_type), return_type: value_change(&before.return_type, &after.return_type), @@ -466,6 +591,7 @@ fn diff_method(before: &MethodSnapshot, after: &MethodSnapshot) -> Option Option Option { + let arguments = diff_members(&before.arguments, &after.arguments, |a, b| { + value_change(a, b) + }); let result = PropertyDiff { required: value_change(&before.required, &after.required), is_public: value_change(&before.is_public, &after.is_public), final_keyword: value_change(&before.final_keyword, &after.final_keyword), multidimensional: value_change(&before.multidimensional, &after.multidimensional), return_type: value_change(&before.return_type, &after.return_type), + arguments, }; (result.required.is_some() || result.is_public.is_some() || result.final_keyword.is_some() || result.multidimensional.is_some() - || result.return_type.is_some()) + || result.return_type.is_some() + || has_member_changes(&result.arguments)) .then_some(result) } diff --git a/objectscript-lsp/crates/objectscript-core/src/xdata.rs b/objectscript-lsp/crates/objectscript-core/src/xdata.rs new file mode 100644 index 0000000..af1a960 --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/src/xdata.rs @@ -0,0 +1,51 @@ +use crate::common::{get_node_children, get_string_at_byte_range, get_tracked_keywords}; +use crate::parse_structures::{Language, XData}; +use tree_sitter::Node; + +impl Default for XData { + fn default() -> Self { + Self { + name: "TODO".to_string(), + language: Language::Xml, + } + } +} + +pub fn build_xdata_struct(xdata_node: Node, content: &str) -> Option { + if xdata_node.kind() != "xdata" { + eprintln!( + "Error: build_xdata_struct was called for node {:?}, but it can only be called for xdata nodes", + xdata_node.kind() + ); + return None; + } + let mut xdata = XData::default(); + let xdata_children = get_node_children(xdata_node); + for xdata_child in xdata_children { + match xdata_child.kind() { + "xdata_name" => { + if let Some(xdata_name_node) = xdata_child.named_child(0) + && let Some(name) = + get_string_at_byte_range(content, xdata_name_node.byte_range()) + { + xdata.name = name; + } else { + eprintln!("Error: failed to get xdata name."); + return None; + } + } + "xdata_keywords" => { + let tracked_keywords = get_tracked_keywords(xdata_child, content); + xdata.language = tracked_keywords.language.unwrap_or(Language::Xml); + } + _ => { + continue; + } + } + } + if &xdata.name == "TODO" { + eprintln!("error: failed to parse xdata name {:?}", &xdata.name); + return None; + } + Some(xdata) +} diff --git a/objectscript-lsp/crates/objectscript-core/tests/class_comparison.rs b/objectscript-lsp/crates/objectscript-core/tests/class_comparison.rs new file mode 100644 index 0000000..4ea079d --- /dev/null +++ b/objectscript-lsp/crates/objectscript-core/tests/class_comparison.rs @@ -0,0 +1,1049 @@ +//! Class comparison test plan: `documentation/class-comparison-test-plan.md`. +//! +//! Each test name starts with its plan ID. Tests assert the behavior described in +//! `documentation/class-comparison-expected-behavior.md`; tests for known gaps are +//! `#[ignore]`d with the reason so `cargo test -- --ignored` lists them. + +use objectscript_core::parse_structures::{ + Cardinality, CodeMode, ForeignKeyAction, IndexType, InheritanceDirection, Language, + MethodType, ReturnType, TriggerFire, TriggerForEach, TypeName, +}; +use objectscript_core::parse_structures::FileType; +use objectscript_core::workspace::ProjectState; +use objectscript_core::workspace_diff::{ + ClassComparison, ClassDiff, MethodDiff, ValueChange, compare_class, +}; +use tower_lsp::lsp_types::Url; + +const CLASS: &str = "Demo.T"; + +fn project(sources: &[(&str, &str)]) -> ProjectState { + let project = ProjectState::new(); + for (class_name, source) in sources { + project.handle_document_opened( + Url::parse(&format!("file:///workspace/{class_name}.cls")).unwrap(), + source.to_string(), + FileType::Cls, + 1, + ); + } + project +} + +/// Wraps member definitions in `Class Demo.T Extends %Persistent`. +fn cls(body: &str) -> String { + format!("Class {CLASS} Extends %Persistent\n{{\n\n{body}\n\n}}\n") +} + +fn compare_sources(before: &str, after: &str) -> ClassComparison { + let baseline = project(&[(CLASS, before)]); + let target = project(&[(CLASS, after)]); + let baseline = baseline.data.read(); + let target = target.data.read(); + compare_class(&baseline, &target, CLASS) +} + +fn class_diff(before: &str, after: &str) -> ClassDiff { + match compare_sources(before, after) { + ClassComparison::Changed(diff) => diff, + other => panic!("expected Changed, got {other:?}"), + } +} + +/// Compares two class bodies built with `cls`. +fn diff(before: &str, after: &str) -> ClassDiff { + class_diff(&cls(before), &cls(after)) +} + +fn change(before: T, after: T) -> Option> { + Some(ValueChange { before, after }) +} + +fn ty(ret_type: ReturnType) -> TypeName { + TypeName { + ret_type, + parameters: Vec::new(), + } +} + +fn names(values: &[&str]) -> Vec { + values.iter().map(|value| value.to_string()).collect() +} + +/// No class-level field and no member collection changed. +fn assert_no_semantic_change(diff: &ClassDiff) { + assert!( + !diff.has_semantic_changes(), + "expected no semantic change, got {diff:#?}" + ); + assert!(diff.unmodeled_source_change); +} + +macro_rules! assert_added_removed { + ($field:ident, $before:expr, $after:expr, $name:expr) => {{ + let added = diff($before, $after); + assert_eq!(added.$field.added, vec![$name.to_string()], "{added:#?}"); + assert!(added.$field.removed.is_empty() && added.$field.changed.is_empty()); + assert!(!added.unmodeled_source_change); + + let removed = diff($after, $before); + assert_eq!(removed.$field.removed, vec![$name.to_string()], "{removed:#?}"); + assert!(removed.$field.added.is_empty() && removed.$field.changed.is_empty()); + }}; +} + +/// Returns the single changed member diff and asserts nothing else in the class changed. +macro_rules! only_changed { + ($diff:expr, $field:ident, $name:expr) => {{ + let diff = &$diff; + assert!(diff.$field.added.is_empty(), "{diff:#?}"); + assert!(diff.$field.removed.is_empty(), "{diff:#?}"); + assert_eq!( + diff.$field.changed.keys().cloned().collect::>(), + vec![$name.to_string()], + "{diff:#?}" + ); + assert!(!diff.unmodeled_source_change); + diff.$field.changed[$name].clone() + }}; +} + +fn method_diff(before: &str, after: &str) -> MethodDiff { + only_changed!(diff(before, after), methods, "Run") +} + +/// A method diff that changes only the method source. +fn assert_source_only(method: &MethodDiff) { + assert_eq!( + method, + &MethodDiff { + source_changed: true, + variables: method.variables.clone(), + ..Default::default() + }, + "expected only a source change" + ); +} + +// --------------------------------------------------------------------------- +// C: class-level results and fields +// --------------------------------------------------------------------------- + +#[test] +fn c01_identical_source_is_unchanged() { + let source = cls("Property Name As %String;"); + assert_eq!( + compare_sources(&source, &source), + ClassComparison::Unchanged { + class_name: CLASS.to_string() + } + ); +} + +#[test] +fn c02_class_only_in_target_is_added() { + let baseline = project(&[]); + let target = project(&[(CLASS, &cls(""))]); + assert_eq!( + compare_class(&baseline.data.read(), &target.data.read(), CLASS), + ClassComparison::Added { + class_name: CLASS.to_string() + } + ); +} + +#[test] +fn c03_class_only_in_baseline_is_removed() { + let baseline = project(&[(CLASS, &cls(""))]); + let target = project(&[]); + assert_eq!( + compare_class(&baseline.data.read(), &target.data.read(), CLASS), + ClassComparison::Removed { + class_name: CLASS.to_string() + } + ); +} + +#[test] +fn c04_final_keyword() { + let diff = class_diff( + "Class Demo.T Extends %Persistent\n{\n}\n", + "Class Demo.T Extends %Persistent [ Final ]\n{\n}\n", + ); + assert_eq!(diff.final_keyword, change(false, true)); + assert!(!diff.unmodeled_source_change); +} + +#[test] +fn c05_procedure_block_keyword() { + let diff = class_diff( + "Class Demo.T Extends %Persistent\n{\n}\n", + "Class Demo.T Extends %Persistent [ Not ProcedureBlock ]\n{\n}\n", + ); + assert_eq!(diff.procedure_block, change(true, false)); +} + +#[test] +fn c06_language_keyword() { + let diff = class_diff( + "Class Demo.T Extends %Persistent\n{\n}\n", + "Class Demo.T Extends %Persistent [ Language = tsql ]\n{\n}\n", + ); + assert_eq!(diff.language, change(Language::Objectscript, Language::TSql)); +} + +#[test] +fn c07_inheritance_keyword() { + let diff = class_diff( + "Class Demo.T Extends (%Persistent, %Populate)\n{\n}\n", + "Class Demo.T Extends (%Persistent, %Populate) [ Inheritance = right ]\n{\n}\n", + ); + assert_eq!( + diff.inheritance_direction, + change(InheritanceDirection::Left, InheritanceDirection::Right) + ); +} + +#[test] +fn c08_superclass_change() { + let diff = class_diff( + "Class Demo.T Extends %Persistent\n{\n}\n", + "Class Demo.T Extends %RegisteredObject\n{\n}\n", + ); + assert_eq!( + diff.inherited_classes, + change(names(&["%Persistent"]), names(&["%RegisteredObject"])) + ); +} + +#[test] +fn c09_superclass_order_is_significant() { + let diff = class_diff( + "Class Demo.T Extends (%Persistent, %Populate)\n{\n}\n", + "Class Demo.T Extends (%Populate, %Persistent)\n{\n}\n", + ); + assert_eq!( + diff.inherited_classes, + change( + names(&["%Persistent", "%Populate"]), + names(&["%Populate", "%Persistent"]) + ) + ); +} + +#[test] +fn c10_import_change() { + let diff = class_diff( + "Import Demo.A\n\nClass Demo.T Extends %Persistent\n{\n}\n", + "Import Demo.B\n\nClass Demo.T Extends %Persistent\n{\n}\n", + ); + assert_eq!(diff.imports, change(names(&["Demo.A"]), names(&["Demo.B"]))); +} + +#[test] +fn c11_import_order_is_not_significant() { + let diff = class_diff( + "Import (Demo.A, Demo.B)\n\nClass Demo.T Extends %Persistent\n{\n}\n", + "Import (Demo.B, Demo.A)\n\nClass Demo.T Extends %Persistent\n{\n}\n", + ); + assert_no_semantic_change(&diff); +} + +#[test] +fn c12_untracked_class_keyword_is_reported_as_unmodeled() { + let diff = class_diff( + "Class Demo.T Extends %Persistent\n{\n}\n", + "Class Demo.T Extends %Persistent [ Abstract ]\n{\n}\n", + ); + assert_no_semantic_change(&diff); +} + +#[test] +fn c13_comment_only_change_is_reported_as_unmodeled() { + let diff = diff( + "Property Name As %String;", + "/// The name.\nProperty Name As %String;", + ); + assert_no_semantic_change(&diff); +} + +#[test] +fn c14_keyword_case_is_not_significant() { + let diff = class_diff( + "Class Demo.T Extends %Persistent [ Final, Language = TSQL ]\n{\n}\n", + "Class Demo.T Extends %Persistent [ final, language = tsql ]\n{\n}\n", + ); + assert_no_semantic_change(&diff); +} + +#[test] +#[ignore = "gap: same as q06, query argument Ranges shift when members move"] +fn c15_reordering_members_is_not_a_member_change() { + let diff = diff( + "Property A As %String;\n\nParameter P = 1;\n\nMethod Run()\n{\n quit\n}\n\n\ + Index AIdx On A;\n\nQuery Q(x As %String) As %SQLQuery\n{\nSELECT ID FROM Demo.T\n}", + "Query Q(x As %String) As %SQLQuery\n{\nSELECT ID FROM Demo.T\n}\n\nIndex AIdx On A;\n\n\ + Method Run()\n{\n quit\n}\n\nParameter P = 1;\n\nProperty A As %String;", + ); + assert_no_semantic_change(&diff); +} + +// --------------------------------------------------------------------------- +// M: methods +// --------------------------------------------------------------------------- + +const RUN: &str = "Method Run(a As %String) As %Status\n{\n quit 1\n}"; + +#[test] +fn m01_method_added_and_removed() { + assert_added_removed!(methods, "", RUN, "Run"); +} + +#[test] +fn m02_return_type() { + let method = method_diff(RUN, &RUN.replace("As %Status", "As %Integer")); + assert_eq!( + method.return_type, + change(Some(ty(ReturnType::Status)), Some(ty(ReturnType::Integer))) + ); + assert!(method.source_changed); +} + +#[test] +fn m03_argument_added() { + let method = method_diff(RUN, &RUN.replace("(a As %String)", "(a As %String, b)")); + assert_eq!(method.arguments.added, names(&["b"])); +} + +#[test] +fn m04_argument_removed() { + let method = method_diff(RUN, &RUN.replace("(a As %String)", "()")); + assert_eq!(method.arguments.removed, names(&["a"])); +} + +#[test] +fn m05_argument_type() { + let method = method_diff(RUN, &RUN.replace("(a As %String)", "(a As %Integer)")); + let argument = &method.arguments.changed["a"]; + assert_eq!(argument.before.return_type, Some(ty(ReturnType::String))); + assert_eq!(argument.after.return_type, Some(ty(ReturnType::Integer))); +} + +#[test] +fn m06_argument_default_value() { + let method = method_diff( + &RUN.replace("(a As %String)", "(a As %String = 1)"), + &RUN.replace("(a As %String)", "(a As %String = 2)"), + ); + let argument = &method.arguments.changed["a"]; + assert_eq!(argument.before.default_value.as_deref(), Some("1")); + assert_eq!(argument.after.default_value.as_deref(), Some("2")); +} + +#[test] +fn m07_argument_byref_and_output() { + let byref = method_diff(RUN, &RUN.replace("(a As", "(ByRef a As")); + let argument = &byref.arguments.changed["a"]; + assert!(!argument.before.byref && argument.after.byref); + + let output = method_diff(RUN, &RUN.replace("(a As", "(Output a As")); + let argument = &output.arguments.changed["a"]; + assert!(!argument.before.output && argument.after.output); +} + +#[test] +#[ignore = "gap: arguments are keyed by name, so a reordered signature is only a source change"] +fn m08_argument_order() { + let method = method_diff( + "Method Run(a, b)\n{\n quit\n}", + "Method Run(b, a)\n{\n quit\n}", + ); + assert!( + method != MethodDiff { + source_changed: true, + ..Default::default() + }, + "argument order is an API change, not just a source change" + ); +} + +#[test] +fn m09_public_list() { + let method = method_diff( + "Method Run() [ PublicList = (x, y) ]\n{\n quit\n}", + "Method Run() [ PublicList = (x, z) ]\n{\n quit\n}", + ); + assert_eq!( + method.public_variables_declared, + change(names(&["x", "y"]), names(&["x", "z"])) + ); +} + +#[test] +fn m10_private_keyword() { + let method = method_diff(RUN, &RUN.replace("As %Status", "As %Status [ Private ]")); + assert_eq!(method.is_public, change(true, false)); +} + +#[test] +fn m11_final_keyword() { + let method = method_diff(RUN, &RUN.replace("As %Status", "As %Status [ Final ]")); + assert_eq!(method.final_keyword, change(None, Some(true))); +} + +#[test] +fn m12_language_keyword() { + let method = method_diff(RUN, &RUN.replace("As %Status", "As %Status [ Language = tsql ]")); + assert_eq!(method.language, change(None, Some(Language::TSql))); +} + +#[test] +fn m13_codemode_keyword() { + let method = method_diff( + RUN, + &RUN.replace("As %Status", "As %Status [ CodeMode = objectgenerator ]"), + ); + assert_eq!( + method.code_mode, + change(CodeMode::Code, CodeMode::ObjectGenerator) + ); +} + +#[test] +fn m14_procedure_block_keyword() { + let method = method_diff( + RUN, + &RUN.replace("As %Status", "As %Status [ ProcedureBlock = 0 ]"), + ); + assert_eq!(method.procedure_block, change(None, Some(false))); +} + +#[test] +fn m15_instance_to_class_method() { + let method = method_diff(RUN, &format!("Class{RUN}")); + assert_eq!( + method.method_type, + change(MethodType::InstanceMethod, MethodType::ClassMethod) + ); +} + +#[test] +fn m16_body_change_is_source_only() { + let method = method_diff(RUN, &RUN.replace("quit 1", "quit 2")); + assert_source_only(&method); +} + +#[test] +fn m17_untracked_keyword_is_source_only() { + let method = method_diff(RUN, &RUN.replace("As %Status", "As %Status [ SqlProc ]")); + assert_source_only(&method); +} + +#[test] +fn m18_variable_changes() { + let method = method_diff( + "Method Run()\n{\n set oldVar = 1\n}", + "Method Run()\n{\n set newVar = 1\n}", + ); + assert!(method.variables.removed.iter().any(|v| v.name == "oldVar")); + assert!(method.variables.added.iter().any(|v| v.name == "newVar")); + assert!(method.source_changed); +} + +#[test] +fn m19_moving_method_is_not_a_change() { + let diff = diff(RUN, &format!("\n\n\n{RUN}")); + assert_no_semantic_change(&diff); +} + +// --------------------------------------------------------------------------- +// P: properties +// --------------------------------------------------------------------------- + +#[test] +fn p01_property_added_and_removed() { + assert_added_removed!(properties, "", "Property Name As %String;", "Name"); +} + +#[test] +fn p02_return_type() { + let property = only_changed!( + diff("Property Name As %String;", "Property Name As %Integer;"), + properties, + "Name" + ); + assert_eq!( + property.return_type, + change(Some(ty(ReturnType::String)), Some(ty(ReturnType::Integer))) + ); +} + +#[test] +#[ignore = "gap: type parameter values (MAXLEN = 50) are dropped; only names are kept"] +fn p03_type_parameter_value() { + let property = only_changed!( + diff( + "Property Name As %String(MAXLEN = 50);", + "Property Name As %String(MAXLEN = 100);" + ), + properties, + "Name" + ); + assert!(property.return_type.is_some()); +} + +#[test] +fn p04_type_parameter_added() { + let property = only_changed!( + diff( + "Property Name As %String;", + "Property Name As %String(MAXLEN = 50);" + ), + properties, + "Name" + ); + assert!(property.return_type.is_some()); +} + +#[test] +#[ignore = "bug: get_tracked_keywords inverts Required (bare keyword sets false)"] +fn p05_required_keyword() { + let property = only_changed!( + diff( + "Property Name As %String;", + "Property Name As %String [ Required ];" + ), + properties, + "Name" + ); + assert_eq!(property.required, change(false, true)); +} + +#[test] +fn p06_private_keyword() { + let property = only_changed!( + diff( + "Property Name As %String;", + "Property Name As %String [ Private ];" + ), + properties, + "Name" + ); + assert_eq!(property.is_public, change(true, false)); +} + +#[test] +fn p07_final_keyword() { + let property = only_changed!( + diff( + "Property Name As %String;", + "Property Name As %String [ Final ];" + ), + properties, + "Name" + ); + assert_eq!(property.final_keyword, change(None, Some(true))); +} + +#[test] +#[ignore = "bug: get_tracked_keywords inverts MultiDimensional (bare keyword sets false)"] +fn p08_multidimensional_keyword() { + let property = only_changed!( + diff( + "Property Name As %String;", + "Property Name As %String [ MultiDimensional ];" + ), + properties, + "Name" + ); + assert_eq!(property.multidimensional, change(false, true)); +} + +#[test] +#[ignore = "gap: untracked property keywords are only visible as a class-level unmodeled change"] +fn p09_untracked_keyword_is_reported_on_member() { + let _ = only_changed!( + diff( + "Property Name As %String;", + "Property Name As %String [ InitialExpression = \"x\" ];" + ), + properties, + "Name" + ); +} + +#[test] +fn p10_moving_property_is_not_a_change() { + let diff = diff( + "Property Name As %String(MAXLEN = 50) [ Required ];", + "\n\n\nProperty Name As %String(MAXLEN = 50) [ Required ];", + ); + assert_no_semantic_change(&diff); +} + +// --------------------------------------------------------------------------- +// PA: parameters +// --------------------------------------------------------------------------- + +#[test] +fn pa01_parameter_added_and_removed() { + assert_added_removed!(parameters, "", "Parameter VERSION = 1;", "VERSION"); +} + +#[test] +#[ignore = "gap: ParameterSnapshot does not include default_value"] +fn pa02_default_value() { + let _ = only_changed!( + diff("Parameter VERSION = 1;", "Parameter VERSION = 2;"), + parameters, + "VERSION" + ); +} + +#[test] +#[ignore = "bug: build_parameter_struct matches \"return_type\", grammar emits \"parameter_type\""] +fn pa03_return_type() { + let parameter = only_changed!( + diff( + "Parameter VERSION As %String = 1;", + "Parameter VERSION As %Integer = 1;" + ), + parameters, + "VERSION" + ); + assert_eq!( + parameter.return_type, + change(Some(ty(ReturnType::String)), Some(ty(ReturnType::Integer))) + ); +} + +#[test] +fn pa04_final_keyword() { + let parameter = only_changed!( + diff("Parameter VERSION = 1;", "Parameter VERSION [ Final ] = 1;"), + parameters, + "VERSION" + ); + assert_eq!(parameter.final_keyword, change(None, Some(true))); +} + +#[test] +#[ignore = "gap: untracked parameter keywords are only visible as a class-level unmodeled change"] +fn pa05_untracked_keyword_is_reported_on_member() { + let _ = only_changed!( + diff("Parameter VERSION = 1;", "Parameter VERSION [ Internal ] = 1;"), + parameters, + "VERSION" + ); +} + +// --------------------------------------------------------------------------- +// R: relationships +// --------------------------------------------------------------------------- + +const REL: &str = "Relationship Items As Demo.Item [ Cardinality = many, Inverse = Owner ];"; + +#[test] +fn r01_relationship_added_and_removed() { + assert_added_removed!(relationships, "", REL, "Items"); +} + +#[test] +fn r02_cardinality() { + let relationship = only_changed!( + diff(REL, &REL.replace("many", "children")), + relationships, + "Items" + ); + assert_eq!(relationship.before.cardinality, Cardinality::Many); + assert_eq!(relationship.after.cardinality, Cardinality::Children); +} + +#[test] +fn r03_inverse() { + let relationship = only_changed!( + diff(REL, &REL.replace("Owner", "Parent")), + relationships, + "Items" + ); + assert_ne!(relationship.before.inverse, relationship.after.inverse); +} + +#[test] +fn r04_return_type() { + let relationship = only_changed!( + diff(REL, &REL.replace("Demo.Item", "Demo.Other")), + relationships, + "Items" + ); + assert_ne!(relationship.before.return_type, relationship.after.return_type); +} + +#[test] +#[ignore = "bug: get_tracked_keywords inverts Required (bare keyword sets false)"] +fn r05_required_private_final_keywords() { + let relationship = only_changed!( + diff(REL, &REL.replace(" ];", ", Required, Private, Final ];")), + relationships, + "Items" + ); + assert!(!relationship.before.required && relationship.after.required); + assert!(relationship.before.is_public && !relationship.after.is_public); + assert_eq!( + (relationship.before.is_final, relationship.after.is_final), + (None, Some(true)) + ); +} + +#[test] +fn r06_keyword_value_case_is_not_significant() { + let diff = diff(REL, &REL.replace("many", "Many")); + assert_no_semantic_change(&diff); +} + +// --------------------------------------------------------------------------- +// FK: foreign keys +// --------------------------------------------------------------------------- + +const FK: &str = "ForeignKey OwnerFK(Name) References Demo.Owner(NameIdx);"; + +#[test] +fn fk01_foreign_key_added_and_removed() { + assert_added_removed!(foreign_keys, "", FK, "OwnerFK"); +} + +#[test] +fn fk02_properties_constrained() { + let key = only_changed!( + diff(FK, &FK.replace("(Name)", "(Name, Code)")), + foreign_keys, + "OwnerFK" + ); + assert_eq!(key.before.properties_constrained, names(&["Name"])); + assert_eq!(key.after.properties_constrained, names(&["Name", "Code"])); +} + +#[test] +fn fk03_referenced_class() { + let key = only_changed!( + diff(FK, &FK.replace("Demo.Owner", "Demo.Other")), + foreign_keys, + "OwnerFK" + ); + assert_eq!(key.before.referenced_class, "Demo.Owner"); + assert_eq!(key.after.referenced_class, "Demo.Other"); +} + +#[test] +fn fk04_referenced_index() { + let key = only_changed!( + diff(FK, &FK.replace("(NameIdx)", "(CodeIdx)")), + foreign_keys, + "OwnerFK" + ); + assert_eq!(key.before.referenced_index.as_deref(), Some("NameIdx")); + assert_eq!(key.after.referenced_index.as_deref(), Some("CodeIdx")); +} + +#[test] +fn fk05_on_delete_and_on_update_keywords() { + let key = only_changed!( + diff( + FK, + &FK.replace(";", " [ OnDelete = cascade, OnUpdate = setnull ];") + ), + foreign_keys, + "OwnerFK" + ); + assert_eq!( + (key.before.on_delete, key.after.on_delete), + (ForeignKeyAction::NoAction, ForeignKeyAction::Cascade) + ); + assert_eq!( + (key.before.on_update, key.after.on_update), + (ForeignKeyAction::NoAction, ForeignKeyAction::SetNull) + ); +} + +#[test] +#[ignore = "gap: untracked foreign key keywords are only visible as a class-level unmodeled change"] +fn fk06_untracked_keyword_is_reported_on_member() { + let _ = only_changed!( + diff(FK, &FK.replace(";", " [ Internal ];")), + foreign_keys, + "OwnerFK" + ); +} + +// --------------------------------------------------------------------------- +// Q: queries +// --------------------------------------------------------------------------- + +const QUERY: &str = + "Query ByName(name As %String) As %SQLQuery\n{\nSELECT ID FROM Demo.T WHERE Name = :name\n}"; + +#[test] +fn q01_query_added_and_removed() { + assert_added_removed!(queries, "", QUERY, "ByName"); +} + +#[test] +fn q02_arguments() { + let query = only_changed!( + diff(QUERY, &QUERY.replace("name As %String", "name As %Integer")), + queries, + "ByName" + ); + assert_ne!( + query.before.arguments["name"].0, + query.after.arguments["name"].0 + ); +} + +#[test] +fn q03_return_type() { + let query = only_changed!( + diff(QUERY, &QUERY.replace("%SQLQuery", "%Query")), + queries, + "ByName" + ); + assert_ne!(query.before.return_type, query.after.return_type); +} + +#[test] +#[ignore = "gap: Query has no body content field"] +fn q04_body_content() { + let _ = only_changed!( + diff(QUERY, &QUERY.replace("SELECT ID", "SELECT ID, Name")), + queries, + "ByName" + ); +} + +#[test] +fn q05_keywords() { + let query = only_changed!( + diff( + QUERY, + &QUERY.replace("%SQLQuery", "%SQLQuery [ Final, Private, Requires = \"R:U\" ]") + ), + queries, + "ByName" + ); + assert_eq!((query.before.is_final, query.after.is_final), (None, Some(true))); + assert!(query.before.is_public && !query.after.is_public); + assert!(query.before.required_privileges.is_empty()); + assert!(!query.after.required_privileges.is_empty()); +} + +#[test] +#[ignore = "gap: Query snapshots include argument source Ranges, so moving a query reports a change"] +fn q06_moving_query_is_not_a_change() { + let diff = diff(QUERY, &format!("\n\n\n{QUERY}")); + assert_no_semantic_change(&diff); +} + +// --------------------------------------------------------------------------- +// I: indexes +// --------------------------------------------------------------------------- + +#[test] +fn i01_index_added_and_removed() { + assert_added_removed!(indices, "", "Index NameIdx On Name;", "NameIdx"); +} + +#[test] +fn i02_properties() { + let index = only_changed!( + diff("Index NameIdx On Name;", "Index NameIdx On (Name, Code);"), + indices, + "NameIdx" + ); + assert_eq!(index.before.properties.len(), 1); + assert_eq!(index.after.properties.len(), 2); +} + +#[test] +fn i03_index_type() { + let index = only_changed!( + diff( + "Index NameIdx On Name;", + "Index NameIdx On Name [ Type = bitmap ];" + ), + indices, + "NameIdx" + ); + assert_eq!( + (index.before.index_type, index.after.index_type), + (IndexType::Index, IndexType::Bitmap) + ); +} + +#[test] +fn i04_property_collation() { + let index = only_changed!( + diff( + "Index NameIdx On Name As SQLUPPER;", + "Index NameIdx On Name As EXACT;" + ), + indices, + "NameIdx" + ); + assert_ne!( + index.before.properties[0].return_type, + index.after.properties[0].return_type + ); +} + +#[test] +#[ignore = "gap: Unique/IdKey/PrimaryKey are not modeled and not reported on the member"] +fn i05_untracked_keyword_is_reported_on_member() { + let _ = only_changed!( + diff("Index NameIdx On Name;", "Index NameIdx On Name [ Unique ];"), + indices, + "NameIdx" + ); +} + +// --------------------------------------------------------------------------- +// T: triggers +// --------------------------------------------------------------------------- + +const TRIGGER: &str = "Trigger Log [ Event = INSERT ]\n{\n set ^Log = 1\n}"; + +#[test] +fn t01_trigger_added_and_removed() { + assert_added_removed!(triggers, "", TRIGGER, "Log"); +} + +#[test] +fn t02_event() { + let trigger = only_changed!( + diff(TRIGGER, &TRIGGER.replace("INSERT", "INSERT/UPDATE")), + triggers, + "Log" + ); + assert!(!trigger.before.update && trigger.after.update); +} + +#[test] +fn t03_time() { + let trigger = only_changed!( + diff(TRIGGER, &TRIGGER.replace(" ]", ", Time = AFTER ]")), + triggers, + "Log" + ); + assert_eq!( + (trigger.before.time, trigger.after.time), + (TriggerFire::BEFORE, TriggerFire::AFTER) + ); +} + +#[test] +fn t04_foreach() { + let trigger = only_changed!( + diff(TRIGGER, &TRIGGER.replace(" ]", ", Foreach = row/object ]")), + triggers, + "Log" + ); + assert_eq!( + (trigger.before.for_each, trigger.after.for_each), + (TriggerForEach::Row, TriggerForEach::RowObject) + ); +} + +#[test] +fn t05_final_language_codemode() { + let trigger = only_changed!( + diff( + TRIGGER, + &TRIGGER.replace(" ]", ", Final, Language = tsql, CodeMode = objectgenerator ]") + ), + triggers, + "Log" + ); + assert_eq!( + (trigger.before.is_final, trigger.after.is_final), + (None, Some(true)) + ); + assert_eq!(trigger.after.language, Language::TSql); + assert_eq!(trigger.after.code_mode, CodeMode::ObjectGenerator); +} + +// --------------------------------------------------------------------------- +// X: XData +// --------------------------------------------------------------------------- + +const XDATA: &str = "XData Config [ MimeType = application/json ]\n{\n{\"a\": 1}\n}"; + +#[test] +fn x01_xdata_added_and_removed() { + assert_added_removed!(xdata, "", XDATA, "Config"); +} + +#[test] +fn x02_mimetype() { + let xdata = only_changed!( + diff(XDATA, &XDATA.replace("application/json", "text/markdown")), + xdata, + "Config" + ); + assert_eq!( + (xdata.before.language, xdata.after.language), + (Language::Json, Language::Markdown) + ); +} + +#[test] +fn x03_body_change_is_reported_as_unmodeled() { + let diff = diff(XDATA, &XDATA.replace("1", "2")); + assert_no_semantic_change(&diff); +} + +// --------------------------------------------------------------------------- +// PR: projections +// --------------------------------------------------------------------------- + +const PROJECTION: &str = "Projection JavaProj As %Projection.Java;"; + +#[test] +fn pr01_projection_added_and_removed() { + assert_added_removed!(projections, "", PROJECTION, "JavaProj"); +} + +#[test] +fn pr02_return_type() { + let projection = only_changed!( + diff(PROJECTION, &PROJECTION.replace("%Projection.Java", "%Projection.Monitor")), + projections, + "JavaProj" + ); + assert_ne!(projection.before.return_type, projection.after.return_type); +} + +#[test] +#[ignore = "gap: untracked projection keywords are only visible as a class-level unmodeled change"] +fn pr03_untracked_keyword_is_reported_on_member() { + let _ = only_changed!( + diff(PROJECTION, &PROJECTION.replace(";", " [ Internal ];")), + projections, + "JavaProj" + ); +} + +// --------------------------------------------------------------------------- +// S: storage +// --------------------------------------------------------------------------- + +const STORAGE: &str = "Storage Default\n{\n%Storage.Persistent\n}"; + +#[test] +fn s01_storage_added_and_removed() { + assert_added_removed!(storage, "", STORAGE, "Default"); +} + +#[test] +fn s02_body_change_is_reported_as_unmodeled() { + let diff = diff(STORAGE, &STORAGE.replace("Persistent", "SQL")); + assert_no_semantic_change(&diff); +} diff --git a/objectscript-lsp/documentation/class-comparison.md b/objectscript-lsp/documentation/class-comparison.md deleted file mode 100644 index f55bb9b..0000000 --- a/objectscript-lsp/documentation/class-comparison.md +++ /dev/null @@ -1,313 +0,0 @@ -# Granular Class Comparison - -`objectscript_core::workspace_diff` compares classes from independently indexed -workspaces without comparing workspace-local IDs. - -This is intended for the IRIS upgrade workflow: - -1. Index SYS version 1 and the customer workspace together. -2. Compute the transitive SYS dependency-name set from version 1. -3. Index SYS version 2 independently. -4. Compare only those dependent class names between the two projects. - -## Implementation status - -### Implemented - -- [x] ID-independent class snapshots. -- [x] ID-independent method, variable, property, and parameter snapshots. -- [x] Exact source-equality short-circuit for unchanged classes. -- [x] Added, removed, unchanged, unavailable, and granular changed results. -- [x] Parallel comparison across requested class names. -- [x] Parallel comparison across the union of all classes in two workspaces. -- [x] Field-level class and member changes. -- [x] Unresolved-versus-resolved direct inheritance changes, even when class - source is identical. -- [x] Variable comparison as a multiset rather than by workspace-local IDs. -- [x] Reporting of source differences not represented by the current semantic - model. -- [x] Unit tests for the unchanged fast path and granular class/member/variable - changes. -- [x] Public export through `objectscript_core::workspace_diff`. - -### Remaining integration work - -- [x] Compute the transitive customer-to-SYS dependency-name closure from the - baseline workspace. -- [x] Add direct superclass names recursively to that closure. -- [x] Invoke `compare_classes_parallel` from the development two-version - workflow in `main`. -- [x] Write a deterministic human-readable comparison report containing direct - and transitive dependency names, summary counts, and granular differences. -- [ ] Decide whether the text report is the final supported output boundary or - add JSON, LSP, or MCP presentation. -- [ ] Add integration tests that independently index two fixture workspaces and - compare a realistic dependency closure. -- [ ] Add explicit tests for added classes, removed classes, unavailable - snapshots, multiple inheritance ordering, and repeated equivalent - variable definitions. -- [ ] Benchmark comparison over the real dependent SYS class set and report the - unchanged-source hit rate. -- [ ] Decide whether source-only changes such as comments and formatting should - appear in the final user-facing report or be optionally suppressed. -- [ ] Add typed snapshots for relationships, indexes, queries, triggers, XData, - storage definitions, and include/macro dependencies as those constructs - enter the semantic model. -- [ ] Consider serializable result types after the output boundary is chosen. - Do not add serialization solely for internal comparison. - -The comparison engine and development file-report workflow are implemented. -Production LSP/MCP exposure and broader integration coverage remain. - -## Development workflow integration - -The development `main` now: - -1. indexes the baseline SYS root; -2. marks those classes as SYS; -3. appends and indexes the customer root in the same `ProjectData`; -4. indexes the target SYS root independently; -5. computes direct and transitive SYS dependency names from the baseline; -6. compares those names between baseline and target in parallel; -7. writes a human-readable report file. - -Default paths match the local upgrade workspace. Override them without editing -source: - -```sh -OBJECTSCRIPT_BASELINE_SYS_ROOT=/path/to/old/sys \ -OBJECTSCRIPT_TARGET_SYS_ROOT=/path/to/new/sys \ -OBJECTSCRIPT_CUSTOMER_ROOT=/path/to/customer \ -OBJECTSCRIPT_COMPARISON_OUTPUT=/path/to/report.txt \ -OBJECTSCRIPT_INDEX_WORKERS=8 \ -cargo run --release --bin objectscript-lsp -``` - -By default, the development workflow compares only customer SYS dependencies. -To compare every class in the baseline SYS root with every class in the target -SYS root, omit customer indexing and use: - -```sh -OBJECTSCRIPT_COMPARE_ALL=true \ -OBJECTSCRIPT_BASELINE_SYS_ROOT=/path/to/old/sys \ -OBJECTSCRIPT_TARGET_SYS_ROOT=/path/to/new/sys \ -OBJECTSCRIPT_COMPARISON_OUTPUT=/path/to/all-classes-report.txt \ -OBJECTSCRIPT_INDEX_WORKERS=8 \ -cargo run --release --bin objectscript-lsp -``` - -The report contains: - -- sorted direct SYS dependency names; -- sorted transitive-only SYS dependency names; -- unchanged/changed/added/removed/unavailable counts; -- detailed `ClassComparison` debug output for every non-unchanged class. - -The normal LSP transport still starts with `--server` in this development -executable. - -## API - -```rust -use objectscript_core::workspace_diff::{ - ClassComparison, - compare_class, - compare_classes_parallel, - compare_workspaces_parallel, -}; -``` - -Compare one class: - -```rust -let comparison = compare_class( - &baseline_project_data, - &target_project_data, - "%Library.Example", -); -``` - -Compare a dependency-name set in parallel: - -```rust -let comparisons = compare_classes_parallel( - &dependent_sys_class_names, - &baseline_project_data, - &target_project_data, -); -``` - -Compare the union of all class names in two workspaces: - -```rust -let comparisons = compare_workspaces_parallel( - &baseline_project_data, - &target_project_data, -); -``` - -This reports classes found in only the target as `Added`, classes found only in -the baseline as `Removed`, and compares classes present in both. Result order is -unspecified; sort by class name only at presentation boundaries that require -stable output. - -Both projects are read-only during comparison. The outer operation parallelizes -by class; member comparison inside one class remains serial to avoid nested -Rayon work. - -## Fast path - -The comparator first looks up each class's defining `Document`. - -- Equal source strings return `ClassComparison::Unchanged` immediately only - when both workspaces also have the same unresolved-parent set. -- A missing target class returns `Removed`. -- A missing baseline class returns `Added`. -- Different source strings trigger semantic snapshot construction and granular - comparison. - -This means identical child source is still reported as changed when a parent is -missing in one workspace and resolves in the other. - -This avoids allocating snapshots for unchanged SYS classes. - -## ID-independent snapshots - -`ClassId`, `MethodRef`, `PropertyRef`, `ParameterRef`, `VariableRef`, and -`ScopeId` are local to one workspace and are never compared directly. - -The comparator dereferences them and builds value snapshots keyed by class and -member names. - -### Class fields - -- direct imports, normalized by sorting; -- direct inherited class names, preserving parent order; -- unresolved direct inherited class names; -- inheritance direction; -- `ProcedureBlock`; -- `Language`; -- `Final`; -- own methods; -- own properties; -- own parameters. - -Inherited effective-member maps are not compared as if they were declarations. -They can be derived after direct class changes are known. - -### Method fields - -- method kind; -- return type; -- visibility; -- `ProcedureBlock`; -- `Language`; -- `CodeMode`; -- `Final`; -- sorted `PublicList` names; -- variables; -- exact method declaration/body source. - -The current method symbol range includes the method source represented by the -semantic model. `MethodDiff::source_changed` is deliberately named “source,” -not “body,” because a signature/keyword edit can also change this range. - -### Variable fields - -- name; -- argument type, when represented; -- public/private status; -- inferred `VariableDefType`. - -Variables are compared as multisets. Workspace-local variable and scope IDs are -ignored, and repeated equivalent definitions retain occurrence counts. - -### Property fields - -- return type; -- required; -- visibility; -- final; -- multidimensional. - -### Parameter fields - -- return type; -- final. - -## Result model - -```rust -pub enum ClassComparison { - Unchanged { class_name: String }, - Added { class_name: String }, - Removed { class_name: String }, - SnapshotUnavailable { - class_name: String, - baseline_available: bool, - target_available: bool, - }, - Changed(ClassDiff), -} -``` - -`ClassDiff` reports changed class fields and added, removed, or changed methods, -properties, and parameters. Each changed member contains field-level -`ValueChange { before, after }` values. - -`unmodeled_source_change` is true when class source differs but none of the -currently modeled semantic fields differ. This preserves visibility into -unsupported UDL constructs, comments, or formatting changes. - -## Performance characteristics - -- Source equality is $O(n)$ in class source bytes and allocates nothing. -- Snapshots are built only for changed classes. -- Class comparisons run through Rayon. -- Maps in snapshots use `BTreeMap` for readable stable reports. -- Variable multiset comparison is quadratic in the number of definitions in one - method. Methods normally contain few definitions; replace it with counted - hash keys only if profiling identifies it as significant. -- Exact method source is cloned only for changed classes while their snapshots - are compared. - -The expensive operation remains workspace indexing. Comparison should be run -after both workspace indexes are complete. - -## Current limitations - -The report can only compare facts retained by the semantic model. It does not -yet fully model: - -- ordered method argument signatures and every argument modifier/default; -- relationships; -- indexes; -- queries; -- triggers; -- XData; -- storage definitions; -- include and macro dependencies. - -Exact class and method source-change reporting prevents these differences from -being silently treated as unchanged. Add typed snapshots as these constructs -enter the semantic model. - -## Dependency closure - -The comparison module accepts class names; it does not decide which SYS classes -the customer depends on. - -`ProjectData::get_sys_dependencies` computes that selection separately. It: - -1. Starts from all customer-owned methods in the baseline workspace. -2. Traverses outgoing ordinary and oref call edges transitively. -3. Records SYS classes directly called or inherited by customer classes. -4. Records additional SYS classes reached through SYS-to-SYS calls. -5. Recursively adds direct SYS superclass names. -6. Returns disjoint direct and transitive class-name sets. - -The upgrade workflow should combine the returned names and pass them to -`compare_classes_parallel`. - -Use class names as cross-workspace identity. Never pass baseline IDs into the -target workspace. diff --git a/objectscript-lsp/src/test.rs b/objectscript-lsp/src/test.rs index 3bcc2f8..4f071c1 100644 --- a/objectscript-lsp/src/test.rs +++ b/objectscript-lsp/src/test.rs @@ -140,6 +140,58 @@ mod tests { assert!(data.classes.contains_key("hksubclass")); } + #[tokio::test] + async fn test_relationship_and_query_members_are_stored_semantically() { + let test_root = env::current_dir().unwrap().join("objectscript-tests"); + + let relationship_root = test_root.join("gotodef").join("relative-method-call"); + let (relationship_backend, relationship_uri) = + setup_backend_and_workspace(relationship_root).await; + let relationship_project = relationship_backend + .get_project(&relationship_uri) + .expect("missing relationship project"); + let relationship_data = relationship_project.data.read(); + let hk_id = relationship_data.classes["hk"]; + let hk = relationship_data + .global_semantic_model + .get_class(&hk_id) + .expect("hk should be indexed"); + let relationship_ref = hk.relationships["manyProp"]; + let relationship = relationship_data + .global_semantic_model + .get_relationship(&relationship_ref) + .expect("manyProp relationship should be stored"); + assert_eq!(relationship.name, "manyProp"); + assert_eq!( + relationship_data.relationship_defs["hk"]["manyProp"], + relationship_ref + ); + drop(relationship_data); + + let query_root = test_root.join("dependencies"); + let (query_backend, query_uri) = setup_backend_and_workspace(query_root).await; + let query_project = query_backend + .get_project(&query_uri) + .expect("missing query project"); + let query_data = query_project.data.read(); + let class_id = query_data.classes["Demo.QueryExamples"]; + let class = query_data + .global_semantic_model + .get_class(&class_id) + .expect("Demo.QueryExamples should be indexed"); + let query_ref = class.queries["EmployeesByCity"]; + let query = query_data + .global_semantic_model + .get_query(&query_ref) + .expect("EmployeesByCity query should be stored"); + assert_eq!(query.name, "EmployeesByCity"); + assert!(query.arguments.contains_key("pCity")); + assert_eq!( + query_data.query_defs["Demo.QueryExamples"]["EmployeesByCity"], + query_ref + ); + } + #[test] fn test_sys_dependencies_include_calls_and_superclasses() { let project = ProjectState::new(); @@ -263,17 +315,17 @@ mod tests { #[test] fn test_get_keyword_and_value() { let (not, keyword, values) = get_keyword_and_value("ClientDataType = longvarchar"); - let value = values.get(0).copied(); + let value = values.first().map(String::as_str); assert!(!not); assert_eq!(keyword, "clientdatatype"); assert_eq!(value, Some("longvarchar")); let (not, keyword, values) = get_keyword_and_value("ClientDataType=longvarchar"); - let value = values.get(0).copied(); + let value = values.first().map(String::as_str); assert!(!not); assert_eq!(keyword, "clientdatatype"); assert_eq!(value, Some("longvarchar")); let (not, keyword, values) = get_keyword_and_value("ProcedureBlock = 1"); - let value = values.get(0).copied(); + let value = values.first().map(String::as_str); assert!(!not); assert_eq!(keyword, "procedureblock"); assert_eq!(value, Some("1")); @@ -322,13 +374,19 @@ mod tests { let (backend, uri) = setup_backend_and_workspace(project_root).await; let project_state = backend.get_project(&uri).unwrap(); let project_data = project_state.data.read(); - + // println!("PROJECT DATA {:#?}", project_data); let superclass_id = project_data.classes.get("hk").unwrap(); let superclass = project_data .global_semantic_model .get_class(superclass_id) .unwrap(); let superclass_method_ref = superclass.get_method_ref("print2").unwrap(); + // println!("OVERRIDE: {:#?}", project_data.override_index); + let id = project_data.classes.get("hksubclass").unwrap(); + println!( + "HKSUBCLASS {:#?}", + project_data.global_semantic_model.get_class(id) + ); let methods = project_data .override_index .effective_methods @@ -575,6 +633,114 @@ mod tests { } } + #[test] + fn test_udl_query_captures_each_inherited_class_once() { + let project = ProjectState::new(); + project.handle_document_opened( + Url::parse("file:///workspace/Demo.Child.cls").unwrap(), + r#" +Class Demo.Child Extends Demo.Parent +{ + Parameter Value = 1; + Property Name As %String; + ClassMethod Run() { Quit } +} +"# + .to_string(), + FileType::Cls, + 1, + ); + + let data = project.data.read(); + let class_id = data.classes["Demo.Child"]; + let class = data + .global_semantic_model + .get_class(&class_id) + .expect("Demo.Child should be indexed"); + assert_eq!( + class + .inherited_classes + .iter() + .map(|(name, _)| name.as_str()) + .collect::>(), + vec!["Demo.Parent"] + ); + } + + #[test] + fn test_combined_udl_method_query_collects_all_facts() { + let project = ProjectState::new(); + project.handle_document_opened( + Url::parse("file:///workspace/Demo.Combined.cls").unwrap(), + r#" +Class Demo.Combined +{ + ClassMethod Target() { Quit } + + ClassMethod Analyze(untyped, typed As %String) As %Integer + [ Private, ProcedureBlock = 1, PublicList = (shared) ] + { + Set local = 1, shared = 2 + Do ..Target() + Quit local + } +} +"# + .to_string(), + FileType::Cls, + 1, + ); + + let data = project.data.read(); + let snapshot = objectscript_core::workspace_diff::snapshot_class(&data, "Demo.Combined") + .expect("Demo.Combined should have a semantic snapshot"); + let analyze = snapshot + .methods + .get("Analyze") + .expect("Analyze should be indexed"); + assert!(!analyze.is_public); + assert_eq!(analyze.procedure_block, Some(true)); + assert!(analyze.return_type.is_some()); + assert!( + analyze + .public_variables_declared + .contains(&"shared".to_string()) + ); + assert!( + analyze + .arguments + .get("untyped") + .is_some_and(|argument| argument.return_type.is_none()) + ); + assert!( + analyze + .arguments + .get("typed") + .is_some_and(|argument| argument.return_type.is_some()) + ); + assert!( + analyze + .variables + .iter() + .any(|variable| variable.name == "local" && !variable.is_public) + ); + assert!( + analyze + .variables + .iter() + .any(|variable| variable.name == "shared" && variable.is_public) + ); + + let class_id = data.classes["Demo.Combined"]; + let class = data + .global_semantic_model + .get_class(&class_id) + .expect("Demo.Combined class should exist"); + let analyze_ref = class.methods["Analyze"]; + let target_ref = class.methods["Target"]; + assert!(data.dependency_graph.is_ancestor(analyze_ref, target_ref)); + } + #[tokio::test] // EC-GDEF-001 async fn test_goto_definition_private_variable_procedure_block() { @@ -1242,7 +1408,6 @@ Method Test() let (backend, uri) = setup_backend_and_workspace(project_root.clone()).await; let project_state = backend.get_project(&uri).expect("missing project state"); let project_data = project_state.data.read(); - let method_ref = project_data .method_defs .get("Demo.Utility") From d35fe390dc8a61940868a569674d6daa3204a007 Mon Sep 17 00:00:00 2001 From: Hannah Date: Thu, 1 Oct 2026 11:08:58 -0400 Subject: [PATCH 6/8] fix: for now, stick with language server impl --- objectscript-lsp/src/main.rs | 411 ++++++++++++++++++----------------- 1 file changed, 214 insertions(+), 197 deletions(-) diff --git a/objectscript-lsp/src/main.rs b/objectscript-lsp/src/main.rs index 97f969d..d38fb7b 100644 --- a/objectscript-lsp/src/main.rs +++ b/objectscript-lsp/src/main.rs @@ -1,216 +1,233 @@ use crate::server::BackendWrapper; -use objectscript_core::workspace::ProjectState; -use objectscript_core::workspace_diff::{ - ClassComparison, compare_classes_parallel, compare_workspaces_parallel, -}; -use std::collections::BTreeSet; -use std::io::{BufWriter, Write}; -use std::path::PathBuf; use tower_lsp::{LspService, Server}; -// #[cfg(test)] +#[cfg(test)] mod backend_testing; -use crate::backend_testing::BackendTester; -use tower_lsp::lsp_types::Url; mod common; mod lsp; mod server; -use rayon::ThreadPoolBuilder; -use std::str::FromStr; #[cfg(test)] mod test; -async fn setup_backend_and_workspace(project_root: PathBuf) -> (BackendTester, Url) { - let state = ProjectState::new(); - if state - .project_root_path - .set(Some(project_root.clone())) - .is_err() - { - eprintln!("failed to set the root path"); - } - let backend = BackendTester::new(); - let uri = Url::from_file_path(&project_root).unwrap(); - backend.add_project(uri.clone(), state); - let _ = backend.index_workspace_root(&uri, project_root).await; - (backend, uri) +#[tokio::main] +async fn main() { + let stdin = tokio::io::stdin(); + let stdout = tokio::io::stdout(); + let (service, socket) = LspService::build(|client| BackendWrapper::new(client)).finish(); + Server::new(stdin, stdout, socket).serve(service).await; } +// use crate::server::BackendWrapper; +// use objectscript_core::workspace::ProjectState; +// use objectscript_core::workspace_diff::{ +// ClassComparison, compare_classes_parallel, compare_workspaces_parallel, +// }; +// use std::collections::BTreeSet; +// use std::io::{BufWriter, Write}; +// use std::path::PathBuf; +// use tower_lsp::{LspService, Server}; +// // #[cfg(test)] +// mod backend_testing; +// use crate::backend_testing::BackendTester; +// use tower_lsp::lsp_types::Url; +// mod common; +// mod lsp; +// mod server; +// use rayon::ThreadPoolBuilder; +// use std::str::FromStr; +// #[cfg(test)] +// mod test; -fn write_comparison_report( - output_path: &PathBuf, - baseline_root: &PathBuf, - target_root: &PathBuf, - customer_root: &PathBuf, - comparison_scope: &str, - direct_dependencies: &BTreeSet, - transitive_dependencies: &BTreeSet, - comparisons: &[ClassComparison], -) -> std::io::Result<()> { - let file = std::fs::File::create(output_path)?; - let mut output = BufWriter::new(file); - let mut unchanged = 0; - let mut added = 0; - let mut removed = 0; - let mut changed = 0; - let mut unavailable = 0; - for comparison in comparisons { - match comparison { - ClassComparison::Unchanged { .. } => unchanged += 1, - ClassComparison::Added { .. } => added += 1, - ClassComparison::Removed { .. } => removed += 1, - ClassComparison::Changed(_) => changed += 1, - ClassComparison::SnapshotUnavailable { .. } => unavailable += 1, - } - } +// async fn setup_backend_and_workspace(project_root: PathBuf) -> (BackendTester, Url) { +// let state = ProjectState::new(); +// if state +// .project_root_path +// .set(Some(project_root.clone())) +// .is_err() +// { +// eprintln!("failed to set the root path"); +// } +// let backend = BackendTester::new(); +// let uri = Url::from_file_path(&project_root).unwrap(); +// backend.add_project(uri.clone(), state); +// let _ = backend.index_workspace_root(&uri, project_root).await; +// (backend, uri) +// } - writeln!(output, "ObjectScript SYS dependency comparison")?; - writeln!(output, "baseline SYS: {}", baseline_root.display())?; - writeln!(output, "target SYS: {}", target_root.display())?; - writeln!(output, "customer: {}", customer_root.display())?; - writeln!(output, "scope: {comparison_scope}")?; - writeln!(output)?; - writeln!(output, "direct dependencies: {}", direct_dependencies.len())?; - for class_name in direct_dependencies { - writeln!(output, " {class_name}")?; - } - writeln!( - output, - "transitive-only dependencies: {}", - transitive_dependencies.len() - )?; - for class_name in transitive_dependencies { - writeln!(output, " {class_name}")?; - } - writeln!(output)?; - writeln!(output, "comparison summary")?; - writeln!(output, " unchanged: {unchanged}")?; - writeln!(output, " changed: {changed}")?; - writeln!(output, " added: {added}")?; - writeln!(output, " removed: {removed}")?; - writeln!(output, " snapshot unavailable: {unavailable}")?; - writeln!(output)?; - writeln!(output, "differences")?; - for comparison in comparisons { - if !matches!(comparison, ClassComparison::Unchanged { .. }) { - writeln!(output, "{comparison:#?}")?; - } - } - output.flush() -} +// fn write_comparison_report( +// output_path: &PathBuf, +// baseline_root: &PathBuf, +// target_root: &PathBuf, +// customer_root: &PathBuf, +// comparison_scope: &str, +// direct_dependencies: &BTreeSet, +// transitive_dependencies: &BTreeSet, +// comparisons: &[ClassComparison], +// ) -> std::io::Result<()> { +// let file = std::fs::File::create(output_path)?; +// let mut output = BufWriter::new(file); +// let mut unchanged = 0; +// let mut added = 0; +// let mut removed = 0; +// let mut changed = 0; +// let mut unavailable = 0; +// for comparison in comparisons { +// match comparison { +// ClassComparison::Unchanged { .. } => unchanged += 1, +// ClassComparison::Added { .. } => added += 1, +// ClassComparison::Removed { .. } => removed += 1, +// ClassComparison::Changed(_) => changed += 1, +// ClassComparison::SnapshotUnavailable { .. } => unavailable += 1, +// } +// } -#[tokio::main] -async fn main() { - let worker_count = std::env::var("OBJECTSCRIPT_INDEX_WORKERS") - .ok() - .and_then(|value| value.parse::().ok()) - .filter(|count| *count > 0) - .unwrap_or_else(|| { - std::thread::available_parallelism() - .map(|count| count.get()) - .unwrap_or(4) - .min(8) - }); - ThreadPoolBuilder::new() - .num_threads(worker_count) - .build_global() - .expect("Rayon pool must be configured once"); - if std::env::args().any(|arg| arg == "--server") { - let stdin = tokio::io::stdin(); - let stdout = tokio::io::stdout(); - let (service, socket) = LspService::build(|client| BackendWrapper::new(client)).finish(); - Server::new(stdin, stdout, socket).serve(service).await; - return; - } +// writeln!(output, "ObjectScript SYS dependency comparison")?; +// writeln!(output, "baseline SYS: {}", baseline_root.display())?; +// writeln!(output, "target SYS: {}", target_root.display())?; +// writeln!(output, "customer: {}", customer_root.display())?; +// writeln!(output, "scope: {comparison_scope}")?; +// writeln!(output)?; +// writeln!(output, "direct dependencies: {}", direct_dependencies.len())?; +// for class_name in direct_dependencies { +// writeln!(output, " {class_name}")?; +// } +// writeln!( +// output, +// "transitive-only dependencies: {}", +// transitive_dependencies.len() +// )?; +// for class_name in transitive_dependencies { +// writeln!(output, " {class_name}")?; +// } +// writeln!(output)?; +// writeln!(output, "comparison summary")?; +// writeln!(output, " unchanged: {unchanged}")?; +// writeln!(output, " changed: {changed}")?; +// writeln!(output, " added: {added}")?; +// writeln!(output, " removed: {removed}")?; +// writeln!(output, " snapshot unavailable: {unavailable}")?; +// writeln!(output)?; +// writeln!(output, "differences")?; +// for comparison in comparisons { +// if !matches!(comparison, ClassComparison::Unchanged { .. }) { +// writeln!(output, "{comparison:#?}")?; +// } +// } +// output.flush() +// } - let iris_current_version_project_root = std::env::var("OBJECTSCRIPT_BASELINE_SYS_ROOT") - .map(PathBuf::from) - .unwrap_or_else(|_| { - PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202502/").unwrap() - }); - let iris_new_version_project_root = std::env::var("OBJECTSCRIPT_TARGET_SYS_ROOT") - .map(PathBuf::from) - .unwrap_or_else(|_| { - PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202602/").unwrap() - }); - let customer_project_root = std::env::var("OBJECTSCRIPT_CUSTOMER_ROOT") - .map(PathBuf::from) - .unwrap_or_else(|_| PathBuf::from("/Users/hkimura/objectscript-dependencies/customer/")); - let comparison_output = std::env::var("OBJECTSCRIPT_COMPARISON_OUTPUT") - .map(PathBuf::from) - .unwrap_or_else(|_| { - PathBuf::from("/Users/hkimura/objectscript-dependencies/class-comparison-report.txt") - }); - let compare_all_classes = std::env::var("OBJECTSCRIPT_COMPARE_ALL") - .map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES")) - .unwrap_or(false); +// #[tokio::main] +// async fn main() { +// let worker_count = std::env::var("OBJECTSCRIPT_INDEX_WORKERS") +// .ok() +// .and_then(|value| value.parse::().ok()) +// .filter(|count| *count > 0) +// .unwrap_or_else(|| { +// std::thread::available_parallelism() +// .map(|count| count.get()) +// .unwrap_or(4) +// .min(8) +// }); +// ThreadPoolBuilder::new() +// .num_threads(worker_count) +// .build_global() +// .expect("Rayon pool must be configured once"); +// if std::env::args().any(|arg| arg == "--server") { +// let stdin = tokio::io::stdin(); +// let stdout = tokio::io::stdout(); +// let (service, socket) = LspService::build(|client| BackendWrapper::new(client)).finish(); +// Server::new(stdin, stdout, socket).serve(service).await; +// return; +// } - let baseline_sys_root = iris_current_version_project_root.clone(); - let baseline_customer_root = customer_project_root.clone(); - let baseline_compare_all = compare_all_classes; - let baseline_index = async { - let (backend, uri) = setup_backend_and_workspace(baseline_sys_root).await; - if !baseline_compare_all { - let project = backend - .get_project(&uri) - .expect("missing baseline IRIS project"); - project.data.write().mark_current_classes_as_sys(); - backend - .index_workspace_root(&uri, baseline_customer_root) - .await; - } - (backend, uri) - }; +// let iris_current_version_project_root = std::env::var("OBJECTSCRIPT_BASELINE_SYS_ROOT") +// .map(PathBuf::from) +// .unwrap_or_else(|_| { +// PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202502/").unwrap() +// }); +// let iris_new_version_project_root = std::env::var("OBJECTSCRIPT_TARGET_SYS_ROOT") +// .map(PathBuf::from) +// .unwrap_or_else(|_| { +// PathBuf::from_str("/Users/hkimura/objectscript-dependencies/IRIS/202602/").unwrap() +// }); +// let customer_project_root = std::env::var("OBJECTSCRIPT_CUSTOMER_ROOT") +// .map(PathBuf::from) +// .unwrap_or_else(|_| PathBuf::from("/Users/hkimura/objectscript-dependencies/customer/")); +// let comparison_output = std::env::var("OBJECTSCRIPT_COMPARISON_OUTPUT") +// .map(PathBuf::from) +// .unwrap_or_else(|_| { +// PathBuf::from("/Users/hkimura/objectscript-dependencies/class-comparison-report.txt") +// }); +// let compare_all_classes = std::env::var("OBJECTSCRIPT_COMPARE_ALL") +// .map(|value| matches!(value.as_str(), "1" | "true" | "TRUE" | "yes" | "YES")) +// .unwrap_or(false); - // Index the baseline SYS + customer project and target SYS project concurrently. - let ((old_iris_backend, old_iris_uri), (new_iris_backend, new_iris_uri)) = tokio::join!( - baseline_index, - setup_backend_and_workspace(iris_new_version_project_root.clone()), - ); +// let baseline_sys_root = iris_current_version_project_root.clone(); +// let baseline_customer_root = customer_project_root.clone(); +// let baseline_compare_all = compare_all_classes; +// let baseline_index = async { +// let (backend, uri) = setup_backend_and_workspace(baseline_sys_root).await; +// if !baseline_compare_all { +// let project = backend +// .get_project(&uri) +// .expect("missing baseline IRIS project"); +// project.data.write().mark_current_classes_as_sys(); +// backend +// .index_workspace_root(&uri, baseline_customer_root) +// .await; +// } +// (backend, uri) +// }; - let old_iris_workspace = old_iris_backend - .get_project(&old_iris_uri) - .expect("missing project for old iris version"); +// // Index the baseline SYS + customer project and target SYS project concurrently. +// let ((old_iris_backend, old_iris_uri), (new_iris_backend, new_iris_uri)) = tokio::join!( +// baseline_index, +// setup_backend_and_workspace(iris_new_version_project_root.clone()), +// ); - let new_iris_workspace = new_iris_backend - .get_project(&new_iris_uri) - .expect("missing project for new iris version"); +// let old_iris_workspace = old_iris_backend +// .get_project(&old_iris_uri) +// .expect("missing project for old iris version"); - let old_data = old_iris_workspace.data.read(); - let new_data = new_iris_workspace.data.read(); - let (direct, transitive) = old_data.get_sys_dependencies(); - let direct: BTreeSet = direct.into_iter().collect(); - let transitive: BTreeSet = transitive.into_iter().collect(); - let dependency_names: Vec = direct.iter().chain(transitive.iter()).cloned().collect(); - let (comparison_scope, mut comparisons) = if compare_all_classes { - ( - "all classes in either workspace", - compare_workspaces_parallel(&old_data, &new_data), - ) - } else { - ( - "customer SYS dependencies", - compare_classes_parallel(&dependency_names, &old_data, &new_data), - ) - }; - comparisons.sort_by(|left, right| left.class_name().cmp(right.class_name())); - write_comparison_report( - &comparison_output, - &iris_current_version_project_root, - &iris_new_version_project_root, - &customer_project_root, - comparison_scope, - &direct, - &transitive, - &comparisons, - ) - .unwrap_or_else(|error| { - panic!( - "failed to write comparison report {}: {error}", - comparison_output.display() - ) - }); - eprintln!( - "[compare] wrote {} class comparisons to {}", - comparisons.len(), - comparison_output.display() - ); -} +// let new_iris_workspace = new_iris_backend +// .get_project(&new_iris_uri) +// .expect("missing project for new iris version"); + +// let old_data = old_iris_workspace.data.read(); +// let new_data = new_iris_workspace.data.read(); +// let (direct, transitive) = old_data.get_sys_dependencies(); +// let direct: BTreeSet = direct.into_iter().collect(); +// let transitive: BTreeSet = transitive.into_iter().collect(); +// let dependency_names: Vec = direct.iter().chain(transitive.iter()).cloned().collect(); +// let (comparison_scope, mut comparisons) = if compare_all_classes { +// ( +// "all classes in either workspace", +// compare_workspaces_parallel(&old_data, &new_data), +// ) +// } else { +// ( +// "customer SYS dependencies", +// compare_classes_parallel(&dependency_names, &old_data, &new_data), +// ) +// }; +// comparisons.sort_by(|left, right| left.class_name().cmp(right.class_name())); +// write_comparison_report( +// &comparison_output, +// &iris_current_version_project_root, +// &iris_new_version_project_root, +// &customer_project_root, +// comparison_scope, +// &direct, +// &transitive, +// &comparisons, +// ) +// .unwrap_or_else(|error| { +// panic!( +// "failed to write comparison report {}: {error}", +// comparison_output.display() +// ) +// }); +// eprintln!( +// "[compare] wrote {} class comparisons to {}", +// comparisons.len(), +// comparison_output.display() +// ); +// } From 89b812923be04c6db118da3e0e6019ff7dcfbc1c Mon Sep 17 00:00:00 2001 From: Hannah Date: Thu, 1 Oct 2026 11:50:32 -0400 Subject: [PATCH 7/8] chore: point to newest tree-sitter grammars --- languages/objectscript/highlights.scm | 5 +- languages/objectscript/injections.scm | 92 ++++++++++++++----- languages/objectscript_udl/highlights.scm | 3 +- languages/objectscript_udl/injections.scm | 92 ++++++++++++++----- objectscript-lsp/Cargo.toml | 6 +- objectscript-lsp/README.md | 6 +- .../crates/objectscript-core/Cargo.toml | 6 +- 7 files changed, 148 insertions(+), 62 deletions(-) diff --git a/languages/objectscript/highlights.scm b/languages/objectscript/highlights.scm index 92739c8..6976fb5 100644 --- a/languages/objectscript/highlights.scm +++ b/languages/objectscript/highlights.scm @@ -230,9 +230,9 @@ (keyword_xecute) (keyword_view) (keyword_zremove) + (keyword_zsave) (command_keyword) (keyword_zload) - (keyword_zsave) (keyword_for) (keyword_while) (keyword_continue) @@ -337,7 +337,7 @@ (class_keyword) (query_keyword) (trigger_keyword) - (method_keyword_external_language) + (keyword_external_language) (relationship_keyword) (foreignkey_keyword) (parameter_keyword) @@ -359,7 +359,6 @@ (xdata_name) (storage_name) (xml_identifier) - (column_name) ] @variant [ diff --git a/languages/objectscript/injections.scm b/languages/objectscript/injections.scm index 887d020..492759c 100644 --- a/languages/objectscript/injections.scm +++ b/languages/objectscript/injections.scm @@ -27,40 +27,45 @@ ; Keywords, one of type language = "python", none of type codemode ; External method body injection based on [ Language = ... ] (method_definition - (method_keyword_external_language - (typename) @_lang) + (external_method_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "python" "Python" "PYTHON") (#set! injection.language "python")) (method_definition - (method_keyword_external_language - (typename) @_lang) + (external_method_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "tsql" "TSQL" "tSQL" "tSql" "TSql") (#set! injection.language "tsql")) (method_definition - (method_keyword_external_language - (typename) @_lang) + (external_method_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "ispl" "ISPL" "ISpl" "iSpl") (#set! injection.language "ispl")) (trigger - (method_keyword_external_language - (typename) @_lang) + (external_trigger_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "python" "Python" "PYTHON") (#set! injection.language "python")) (trigger - (method_keyword_external_language - (typename) @_lang) + (external_trigger_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "tsql" "TSQL" "tSQL" "tSql" "TSql") @@ -86,61 +91,95 @@ ; text/markdown ; (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/markdown" "\"text/markdown\"") (#set! injection.language "markdown")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) + (external_method_body_content) @injection.content + (#set! injection.include-children "true") + (#any-of? @_mt "text/javascript" "\"text/javascript\"") + (#set! injection.language "javascript")) + +(xdata + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/x-python" "\"text/x-python\"" "application/python" "\"application/python\"") (#set! injection.language "python")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/xml" "\"text/xml\"" "application/xml" "\"application/xml\"") (#set! injection.language "xml")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/html" "\"text/html\"") (#set! injection.language "html")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "application/json" "\"application/json\"") (#set! injection.language "json")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/yaml" "\"text/yaml\"" "application/yaml" "\"application/yaml\"") (#set! injection.language "yaml")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/css" "\"text/css\"") (#set! injection.language "css")) +(xdata + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) + (external_method_body_content) @injection.content + (#set! injection.include-children "true") + (#any-of? @_mt "application/sql" "\"application/sql\"") + (#set! injection.language "sql")) + +(xdata + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) + (external_method_body_content) @injection.content + (#set! injection.include-children "true") + (#any-of? @_mt "text/x-java-source" "\"text/x-java-source\"") + (#set! injection.language "java")) + ; -------------------------------------------- ; XDATA default (no MimeType header): XML fallback ; -------------------------------------------- @@ -168,4 +207,9 @@ ] @injection.content (#set! injection.language "comment")) +(clientmethod + (external_method_body_content) @injection.content + (#set! injection.language "javascript") + (#set! injection.include-children "true")) + ; === END LOCAL === diff --git a/languages/objectscript_udl/highlights.scm b/languages/objectscript_udl/highlights.scm index 1b839fb..753cfbb 100644 --- a/languages/objectscript_udl/highlights.scm +++ b/languages/objectscript_udl/highlights.scm @@ -342,7 +342,7 @@ (class_keyword) (query_keyword) (trigger_keyword) - (method_keyword_external_language) + (keyword_external_language) (relationship_keyword) (foreignkey_keyword) (parameter_keyword) @@ -364,7 +364,6 @@ (xdata_name) (storage_name) (xml_identifier) - (column_name) ] @variant [ diff --git a/languages/objectscript_udl/injections.scm b/languages/objectscript_udl/injections.scm index 887d020..492759c 100644 --- a/languages/objectscript_udl/injections.scm +++ b/languages/objectscript_udl/injections.scm @@ -27,40 +27,45 @@ ; Keywords, one of type language = "python", none of type codemode ; External method body injection based on [ Language = ... ] (method_definition - (method_keyword_external_language - (typename) @_lang) + (external_method_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "python" "Python" "PYTHON") (#set! injection.language "python")) (method_definition - (method_keyword_external_language - (typename) @_lang) + (external_method_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "tsql" "TSQL" "tSQL" "tSql" "TSql") (#set! injection.language "tsql")) (method_definition - (method_keyword_external_language - (typename) @_lang) + (external_method_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "ispl" "ISPL" "ISpl" "iSpl") (#set! injection.language "ispl")) (trigger - (method_keyword_external_language - (typename) @_lang) + (external_trigger_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "python" "Python" "PYTHON") (#set! injection.language "python")) (trigger - (method_keyword_external_language - (typename) @_lang) + (external_trigger_keywords + (keyword_external_language + (typename) @_lang)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_lang "tsql" "TSQL" "tSQL" "tSql" "TSql") @@ -86,61 +91,95 @@ ; text/markdown ; (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/markdown" "\"text/markdown\"") (#set! injection.language "markdown")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) + (external_method_body_content) @injection.content + (#set! injection.include-children "true") + (#any-of? @_mt "text/javascript" "\"text/javascript\"") + (#set! injection.language "javascript")) + +(xdata + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/x-python" "\"text/x-python\"" "application/python" "\"application/python\"") (#set! injection.language "python")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/xml" "\"text/xml\"" "application/xml" "\"application/xml\"") (#set! injection.language "xml")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/html" "\"text/html\"") (#set! injection.language "html")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "application/json" "\"application/json\"") (#set! injection.language "json")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/yaml" "\"text/yaml\"" "application/yaml" "\"application/yaml\"") (#set! injection.language "yaml")) (xdata - (xdata_keyword_mimetype - (typename) @_mt) + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) (external_method_body_content) @injection.content (#set! injection.include-children "true") (#any-of? @_mt "text/css" "\"text/css\"") (#set! injection.language "css")) +(xdata + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) + (external_method_body_content) @injection.content + (#set! injection.include-children "true") + (#any-of? @_mt "application/sql" "\"application/sql\"") + (#set! injection.language "sql")) + +(xdata + (xdata_keywords + (xdata_keyword_mimetype + (typename) @_mt)) + (external_method_body_content) @injection.content + (#set! injection.include-children "true") + (#any-of? @_mt "text/x-java-source" "\"text/x-java-source\"") + (#set! injection.language "java")) + ; -------------------------------------------- ; XDATA default (no MimeType header): XML fallback ; -------------------------------------------- @@ -168,4 +207,9 @@ ] @injection.content (#set! injection.language "comment")) +(clientmethod + (external_method_body_content) @injection.content + (#set! injection.language "javascript") + (#set! injection.include-children "true")) + ; === END LOCAL === diff --git a/objectscript-lsp/Cargo.toml b/objectscript-lsp/Cargo.toml index 1b4ae41..b762e2e 100644 --- a/objectscript-lsp/Cargo.toml +++ b/objectscript-lsp/Cargo.toml @@ -13,9 +13,9 @@ objectscript-core = { path = "crates/objectscript-core" } tower-lsp = "0.20.0" tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "fs"] } tree-sitter = "0.26.6" -tree-sitter-objectscript = "1.10.0" -tree-sitter-objectscript-routine = "1.10.0" -tree-sitter-objectscript-playground = "1.10.0" +tree-sitter-objectscript = "1.10.1" +tree-sitter-objectscript-routine = "1.10.1" +tree-sitter-objectscript-playground = "1.10.1" tree-sitter-xml = "0.7.0" serde = "1.0.228" serde_json = "1.0.145" diff --git a/objectscript-lsp/README.md b/objectscript-lsp/README.md index de5cf59..fa9e634 100644 --- a/objectscript-lsp/README.md +++ b/objectscript-lsp/README.md @@ -83,9 +83,9 @@ In this case, the `DependencyGraph` is used to determine all possible paths to t ## Grammar Baseline - `tree-sitter = 0.26.6` -- `tree-sitter-objectscript = 1.10.0` -- `tree-sitter-objectscript-routine = 1.10.0` -- `tree-sitter-objectscript-playground = 1.10.0` +- `tree-sitter-objectscript = 1.10.1` +- `tree-sitter-objectscript-routine = 1.10.1` +- `tree-sitter-objectscript-playground = 1.10.1` - `tree-sitter-xml = 0.7.0` ## Roadmap diff --git a/objectscript-lsp/crates/objectscript-core/Cargo.toml b/objectscript-lsp/crates/objectscript-core/Cargo.toml index 4fb7540..b45baab 100644 --- a/objectscript-lsp/crates/objectscript-core/Cargo.toml +++ b/objectscript-lsp/crates/objectscript-core/Cargo.toml @@ -18,9 +18,9 @@ required-features = ["update-bench"] tower-lsp = "0.20.0" tokio = { version = "1", features = ["rt-multi-thread", "macros", "io-std", "fs"] } tree-sitter = "0.26.6" -tree-sitter-objectscript = "1.10.0" -tree-sitter-objectscript-routine = "1.10.0" -tree-sitter-objectscript-playground = "1.10.0" +tree-sitter-objectscript = "1.10.1" +tree-sitter-objectscript-routine = "1.10.1" +tree-sitter-objectscript-playground = "1.10.1" tree-sitter-xml = "0.7.0" serde = "1.0.228" serde_json = "1.0.145" From b0d0c7f02576eb420f83871fb76a3c88501bc5d4 Mon Sep 17 00:00:00 2001 From: Hannah Date: Thu, 1 Oct 2026 11:54:41 -0400 Subject: [PATCH 8/8] fix query --- languages/objectscript/highlights.scm | 1 - 1 file changed, 1 deletion(-) diff --git a/languages/objectscript/highlights.scm b/languages/objectscript/highlights.scm index b8551c4..6976fb5 100644 --- a/languages/objectscript/highlights.scm +++ b/languages/objectscript/highlights.scm @@ -233,7 +233,6 @@ (keyword_zsave) (command_keyword) (keyword_zload) - (keyword_zsave) (keyword_for) (keyword_while) (keyword_continue)