diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index be7ef0377..d1479fb48 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -24,30 +24,10 @@ jobs: with: go-version-file: go.mod cache: true - - name: 🔨 Install Tools - run: | - # Pin gotestsum to v1.12.0 to support Go 1.23.4 (v1.13.0 requires Go 1.24) - go install gotest.tools/gotestsum@v1.12.0 - go install github.com/ctrf-io/go-ctrf-json-reporter/cmd/go-ctrf-json-reporter@latest - name: 🔨 Build run: go build -v -o ./build/tavern ./tavern - name: 🔎 Test - # Use gotestsum for human-readable output to stdout, and capture JSON to file for reporting - shell: bash - run: | - $(go env GOPATH)/bin/gotestsum --jsonfile test-output.json -- \ - -v -race -coverprofile='coverage.out' -covermode=atomic ./tavern/... - - name: 🔄 Convert to CTRF - if: always() # Run even if tests fail so we report the failures - shell: bash - run: | - if [ -f test-output.json ]; then - cat test-output.json | $(go env GOPATH)/bin/go-ctrf-json-reporter -o ctrf-tavern-${{ matrix.os }}.json - # Normalize timestamps to avoid 50+ year durations when merging with reports lacking timestamps - jq '.results.summary.stop = (.results.summary.stop - .results.summary.start) | .results.summary.start = 0' ctrf-tavern-${{ matrix.os }}.json > ctrf-tavern-${{ matrix.os }}.json.tmp && mv ctrf-tavern-${{ matrix.os }}.json.tmp ctrf-tavern-${{ matrix.os }}.json - else - echo "test-output.json not found, skipping CTRF conversion" - fi + run: go test -v -race -coverprofile='coverage.out' -covermode=atomic ./tavern/... - name: 📶 Upload Coverage Results uses: codecov/codecov-action@v5 with: @@ -55,20 +35,12 @@ jobs: files: ./coverage.out flags: tavern name: tavern-coverage - - name: 📤 Upload CTRF Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: ctrf-tavern-${{ matrix.os }} - path: ctrf-tavern-${{ matrix.os }}.json - implants: runs-on: ${{ matrix.os }} timeout-minutes: 60 env: IMIX_SERVER_PUBKEY: "pR56vDJZb9b3BL3ZvCXIvgK0r2vCk7FiZ1RjeEhJVyU=" strategy: - fail-fast: false matrix: os: - ubuntu-latest @@ -109,32 +81,11 @@ jobs: uses: taiki-e/install-action@v2 with: tool: nextest,cargo-llvm-cov - - name: âš¡ Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - name: 🔎 Run tests - # nextest configured via nextest.toml to output junit.xml run: | cd ./implants/ && cargo fmt --check && cargo llvm-cov nextest --lcov --output-path lcov.info - - name: 🔄 Convert to CTRF - if: always() - shell: bash # Explicitly use bash to support standard shell syntax on Windows - run: | - cd ./implants/ - # Move junit report if it exists - JUNIT_PATH=$(find . -name junit.xml | head -n 1) - if [ -n "$JUNIT_PATH" ]; then - mv "$JUNIT_PATH" junit-implants-${{ matrix.os }}.xml - else - echo "No JUnit report generated" - fi - - if [ -f junit-implants-${{ matrix.os }}.xml ]; then - npx junit-to-ctrf junit-implants-${{ matrix.os }}.xml -o ctrf-implants-${{ matrix.os }}.json - fi - name: 📶 Upload Coverage Results uses: codecov/codecov-action@v5 with: @@ -142,13 +93,6 @@ jobs: files: ./implants/lcov.info flags: implants name: implants-coverage-${{ matrix.os }} - - name: 📤 Upload CTRF Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: ctrf-implants-${{ matrix.os }} - path: implants/ctrf-implants-${{ matrix.os }}.json - ui-tests: runs-on: ${{ matrix.os }} strategy: @@ -169,11 +113,7 @@ jobs: - name: 📦 Install dependencies run: npm ci - name: 🔎 Run vitest with coverage - # Generate JUnit XML - run: npm test -- --run --coverage --reporter=junit --output-file=junit-ui-${{ matrix.os }}.xml - - name: 🔄 Convert to CTRF - if: always() - run: npx junit-to-ctrf junit-ui-${{ matrix.os }}.xml -o ctrf-ui-${{ matrix.os }}.json + run: npm test -- --run --coverage - name: 📶 Upload Coverage Results if: always() uses: codecov/codecov-action@v5 @@ -182,46 +122,3 @@ jobs: files: ./tavern/internal/www/coverage/lcov.info flags: ui-tests name: ui-tests-coverage - - name: 📤 Upload CTRF Results - if: always() - uses: actions/upload-artifact@v4 - with: - name: ctrf-ui-${{ matrix.os }} - path: tavern/internal/www/ctrf-ui-${{ matrix.os }}.json - - flaky-monitor: - needs: [tavern, implants, ui-tests] - runs-on: ubuntu-latest - if: always() - steps: - - name: 📥 Download CTRF Results - uses: actions/download-artifact@v4 - with: - pattern: ctrf-* - path: ctrf-reports - merge-multiple: true - - - name: 📊 Publish Test Report - uses: ctrf-io/github-test-reporter@v1 - if: always() - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - with: - report-path: 'ctrf-reports/*.json' - upload-artifact: true - fetch-previous-results: true - pull-request: true - overwrite-comment: true - - # Report Config - # https://github.com/ctrf-io/github-test-reporter/blob/main/docs/report-showcase.md - summary-delta-report: true - tests-changed-report: true - insights-report: true - slowest-report: true - failed-folded-report: true - flaky-rate-report: true - fail-rate-report: true - previous-results-report: true - previous-results-max: 100 - artifact-name: 'flaky-report' diff --git a/.gitignore b/.gitignore index bad2e4f26..cf06e57c0 100644 --- a/.gitignore +++ b/.gitignore @@ -46,4 +46,3 @@ implants/golem/embed_files_golem_prod/* .venv *.tfvars -node_modules/ diff --git a/AGENTS.md b/AGENTS.md index a6128a78d..53f7e0ef7 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,10 +8,6 @@ Welcome to our repository! Most commands need to be run from the root of the pro * Our user interface is located in `tavern/internal/www` and we managed dependencies within that directory using `npm` * `implants/` contains Rust code that is deployed to target machines, such as our agent located in `implants/imix`. -## Rust - -The majority of our rust codebase is in the `implants/` directory & workspaces. When adding dependencies to crates within this workspace, please add them to the workspace root and have the crate use the workspace dependency, in order to centralize version management. ALWAYS RUN `cargo fmt` WHEN MAKING RUST CHANGES, OR CI WILL FAIL. - ## Golang Tests To run all Golang tests in our repository, please run `go test ./...` from the project root. diff --git a/implants/.config/nextest.toml b/implants/.config/nextest.toml deleted file mode 100644 index 88457dc15..000000000 --- a/implants/.config/nextest.toml +++ /dev/null @@ -1,2 +0,0 @@ -[profile.default.junit] -path = "junit.xml" diff --git a/implants/Cargo.toml b/implants/Cargo.toml index 32d1f3a3c..f34acdb56 100644 --- a/implants/Cargo.toml +++ b/implants/Cargo.toml @@ -11,6 +11,7 @@ members = [ "lib/eldritchv2/eldritch-macros", "lib/eldritchv2/eldritch-repl", "lib/eldritchv2/eldritch-agent", + "lib/eldritchv2/eldritch-lsp", "lib/eldritchv2/stdlib/eldritch-libagent", "lib/eldritchv2/stdlib/eldritch-libassets", "lib/eldritchv2/stdlib/eldritch-libcrypto", diff --git a/implants/lib/eldritchv2/eldritch-core/benches/interpreter.rs b/implants/lib/eldritchv2/eldritch-core/benches/interpreter.rs index b9e812255..ad0c0bd7b 100644 --- a/implants/lib/eldritchv2/eldritch-core/benches/interpreter.rs +++ b/implants/lib/eldritchv2/eldritch-core/benches/interpreter.rs @@ -38,7 +38,7 @@ fn bench_function_call(c: &mut Criterion) { b.iter(|| { let mut interpreter = Interpreter::new(); let code = " -def add(a, b): +fn add(a, b): return a + b add(5, 10) diff --git a/implants/lib/eldritchv2/eldritch-core/src/ast.rs b/implants/lib/eldritchv2/eldritch-core/src/ast.rs index 67892cbcf..dfdf23e39 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/ast.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/ast.rs @@ -11,6 +11,7 @@ use spin::RwLock; // Resolve circular reference for ForeignValue signature use crate::interpreter::Interpreter; +use crate::interpreter::signature::MethodSignature; #[derive(Debug)] pub struct Environment { @@ -59,6 +60,9 @@ pub type BuiltinFnWithKwargs = pub trait ForeignValue: fmt::Debug + Send + Sync { fn type_name(&self) -> &str; fn method_names(&self) -> Vec; + fn get_method_signature(&self, _name: &str) -> Option { + None + } fn call_method( &self, interp: &mut Interpreter, diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/builtins/chr.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/builtins/chr.rs index 675c855b5..2063b707d 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/builtins/chr.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/builtins/chr.rs @@ -1,7 +1,7 @@ use crate::ast::{Environment, Value}; use crate::interpreter::introspection::get_type_name; use alloc::format; -use alloc::string::String; +use alloc::string::{String, ToString}; use alloc::sync::Arc; use core::char; use spin::RwLock; diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs index 078cf5b6b..a6b8da867 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/core.rs @@ -351,21 +351,29 @@ impl Interpreter { let mut lexer = Lexer::new(line_up_to_cursor.to_string()); let tokens = match lexer.scan_tokens() { Ok(t) => t, - // If scanning fails (e.g. open string), we might still want to try? - // For now, if lexer fails, we fallback to simple word splitting or empty. + // If scanning fails (e.g. due to syntax error earlier in the file), + // we try to be resilient and just scan the current line. Err(_) => { - // Fallback: Check if we are typing a simple identifier at end - let last_word = line_up_to_cursor - .split_terminator(|c: char| !c.is_alphanumeric() && c != '_') - .next_back() - .unwrap_or(""); - if !last_word.is_empty() { - prefix = last_word.to_string(); + // Attempt to scan just the last line + let last_line = line_up_to_cursor.lines().last().unwrap_or(""); + let mut fallback_lexer = Lexer::new(last_line.to_string()); + match fallback_lexer.scan_tokens() { + Ok(t) => t, + Err(_) => { + // Fallback: Check if we are typing a simple identifier at end + let last_word = line_up_to_cursor + .split_terminator(|c: char| !c.is_alphanumeric() && c != '_') + .next_back() + .unwrap_or(""); + if !last_word.is_empty() { + prefix = last_word.to_string(); + } + // If we can't tokenize, we assume we can't do dot-access analysis safely. + // But we can still try to suggest globals matching the prefix. + // We return empty tokens list, but allow execution to proceed to prefix filtering. + Vec::new() + } } - // If we can't tokenize, we assume we can't do dot-access analysis safely. - // But we can still try to suggest globals matching the prefix. - // We return empty tokens list, but allow execution to proceed to prefix filtering. - Vec::new() } }; @@ -373,7 +381,12 @@ impl Interpreter { let mut target_val: Option = None; let meaningful_tokens: Vec<&super::super::token::Token> = tokens .iter() - .filter(|t| t.kind != TokenKind::Eof && t.kind != TokenKind::Newline) + .filter(|t| { + t.kind != TokenKind::Eof + && t.kind != TokenKind::Newline + && t.kind != TokenKind::Indent + && t.kind != TokenKind::Dedent + }) .collect(); if !meaningful_tokens.is_empty() { @@ -401,6 +414,19 @@ impl Interpreter { TokenKind::String(s) => { target_val = Some(Value::String(s.clone())); } + TokenKind::RBracket => { + // Assume List + target_val = Some(Value::List(Arc::new(RwLock::new(Vec::new())))); + } + TokenKind::RBrace => { + // Assume Dictionary (could be Set, but Dict is safer default) + target_val = + Some(Value::Dictionary(Arc::new(RwLock::new(BTreeMap::new())))); + } + TokenKind::RParen => { + // Assume Tuple + target_val = Some(Value::Tuple(Vec::new())); + } _ => {} } } @@ -446,6 +472,12 @@ impl Interpreter { match &val { Value::Foreign(obj) => { for m in obj.method_names() { + #[allow(clippy::collapsible_if)] + if let Some(sig) = obj.get_method_signature(&m) { + if sig.deprecated.is_some() { + continue; + } + } candidates.insert(m); } } diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/access.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/access.rs index 84afa9202..bc8347401 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/access.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/access.rs @@ -107,6 +107,28 @@ pub(crate) fn evaluate_index( } Ok(Value::String(chars[true_idx as usize].to_string())) } + Value::Bytes(b) => { + let idx_int = match idx_val { + Value::Int(i) => i, + _ => { + return interp.error( + EldritchErrorKind::TypeError, + "bytes indices must be integers", + index.span, + ); + } + }; + let len = b.len() as i64; + let true_idx = if idx_int < 0 { len + idx_int } else { idx_int }; + if true_idx < 0 || true_idx as usize >= b.len() { + return interp.error( + EldritchErrorKind::IndexError, + "Bytes index out of range", + span, + ); + } + Ok(Value::Int(b[true_idx as usize] as i64)) + } _ => interp.error( EldritchErrorKind::TypeError, &format!("'{}' object is not subscriptable", get_type_name(&obj_val)), @@ -249,6 +271,28 @@ pub(crate) fn evaluate_slice( } Ok(Value::String(result_chars.into_iter().collect())) } + Value::Bytes(b) => { + let len = b.len() as i64; + let (i, j) = adjust_slice_indices(len, &start_val_opt, &stop_val_opt, step_val); + let mut result_bytes = Vec::new(); + let mut curr = i; + if step_val > 0 { + while curr < j { + if curr >= 0 && curr < len { + result_bytes.push(b[curr as usize]); + } + curr += step_val; + } + } else { + while curr > j { + if curr >= 0 && curr < len { + result_bytes.push(b[curr as usize]); + } + curr += step_val; + } + } + Ok(Value::Bytes(result_bytes)) + } _ => interp.error( EldritchErrorKind::TypeError, &format!("'{}' object is not subscriptable", get_type_name(&obj_val)), diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/functions.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/functions.rs index 504eac5ef..aceb1a527 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/functions.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/eval/functions.rs @@ -15,7 +15,7 @@ use super::utils::parse_error_kind; use super::{MAX_RECURSION_DEPTH, evaluate}; use alloc::collections::{BTreeMap, BTreeSet}; use alloc::format; -use alloc::string::ToString; +use alloc::string::{String, ToString}; use alloc::sync::Arc; use alloc::vec::Vec; use spin::RwLock; diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/dict.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/dict.rs index 720302c31..fb41fb7f0 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/dict.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/dict.rs @@ -1,6 +1,7 @@ use super::ArgCheck; use crate::ast::Value; use alloc::collections::BTreeMap; +use alloc::format; use alloc::sync::Arc; use alloc::vec; use alloc::vec::Vec; diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/mod.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/mod.rs index 9e6dddf92..a1c6663bb 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/mod.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/mod.rs @@ -8,11 +8,13 @@ use alloc::vec::Vec; mod dict; mod list; mod set; +mod signatures; mod str; use dict::handle_dict_methods; use list::handle_list_methods; use set::handle_set_methods; +pub use signatures::get_native_method_signature; use str::handle_string_methods; // --- Argument Validation Helper --- diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/signatures.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/signatures.rs new file mode 100644 index 000000000..7777ad9ce --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/methods/signatures.rs @@ -0,0 +1,450 @@ +use crate::ast::Value; +use crate::interpreter::signature::{MethodSignature, ParameterSignature}; +use alloc::string::String; +use alloc::vec; +use alloc::vec::Vec; + +fn param(name: &str, type_name: Option<&str>, is_optional: bool) -> ParameterSignature { + ParameterSignature { + name: String::from(name), + type_name: type_name.map(String::from), + is_optional, + is_variadic: false, + is_kwargs: false, + } +} + +fn sig( + name: &str, + params: Vec, + ret: Option<&str>, + doc: Option<&str>, +) -> MethodSignature { + MethodSignature { + name: String::from(name), + params, + return_type: ret.map(String::from), + doc: doc.map(String::from), + deprecated: None, // No native methods are deprecated + } +} + +pub fn get_native_method_signature(value: &Value, method: &str) -> Option { + match value { + Value::List(_) => get_list_signature(method), + Value::Dictionary(_) => get_dict_signature(method), + Value::Set(_) => get_set_signature(method), + Value::String(_) => get_string_signature(method), + _ => None, + } +} + +fn get_list_signature(method: &str) -> Option { + match method { + "append" => Some(sig( + "append", + vec![param("item", Some("any"), false)], + Some("None"), + Some("Appends an item to the end of the list."), + )), + "extend" => Some(sig( + "extend", + vec![param("iterable", Some("iterable"), false)], + Some("None"), + Some("Extends the list by appending elements from the iterable."), + )), + "insert" => Some(sig( + "insert", + vec![ + param("index", Some("int"), false), + param("item", Some("any"), false), + ], + Some("None"), + Some("Inserts an item at a given position."), + )), + "remove" => Some(sig( + "remove", + vec![param("item", Some("any"), false)], + Some("None"), + Some("Removes the first item from the list whose value is equal to x."), + )), + "index" => Some(sig( + "index", + vec![param("item", Some("any"), false)], + Some("int"), + Some("Returns the index of the first item whose value is equal to x."), + )), + "pop" => Some(sig( + "pop", + vec![], // Native pop implementation in list.rs ignores arguments for now (based on my read) but python supports pop([i]) + // Wait, list.rs says: args.require(0, "pop")? -> It requires 0 arguments! + // So our signature should be empty. + Some("any"), + Some("Removes and returns the last item in the list."), + )), + "sort" => Some(sig( + "sort", + vec![], // list.rs requires 0 arguments. No key= or reverse= support yet in built-in list.sort? + // "args.require(0, "sort")?" -> Yes, no args. + Some("None"), + Some("Sorts the items of the list in place."), + )), + _ => None, + } +} + +fn get_dict_signature(method: &str) -> Option { + match method { + "clear" => Some(sig( + "clear", + vec![], + Some("None"), + Some("Removes all items from the dictionary."), + )), + "keys" => Some(sig( + "keys", + vec![], + Some("list"), + Some("Returns a list of the dictionary's keys."), + )), + "values" => Some(sig( + "values", + vec![], + Some("list"), + Some("Returns a list of the dictionary's values."), + )), + "items" => Some(sig( + "items", + vec![], + Some("list"), + Some("Returns a list of the dictionary's (key, value) pairs."), + )), + "get" => Some(sig( + "get", + vec![ + param("key", Some("any"), false), + param("default", Some("any"), true), + ], + Some("any"), + Some("Returns the value for key if key is in the dictionary, else default."), + )), + "update" => Some(sig( + "update", + vec![param("other", Some("dict"), false)], + Some("None"), + Some("Updates the dictionary with the key/value pairs from other."), + )), + "pop" => Some(sig( + "pop", + vec![ + param("key", Some("any"), false), + param("default", Some("any"), true), + ], + Some("any"), + Some("Removes the specified key and returns the corresponding value."), + )), + "popitem" => Some(sig( + "popitem", + vec![], + Some("tuple"), + Some("Removes and returns a (key, value) pair from the dictionary."), + )), + "setdefault" => Some(sig( + "setdefault", + vec![ + param("key", Some("any"), false), + param("default", Some("any"), true), + ], + Some("any"), + Some( + "If key is in the dictionary, return its value. If not, insert key with a value of default and return default.", + ), + )), + _ => None, + } +} + +fn get_set_signature(method: &str) -> Option { + match method { + "add" => Some(sig( + "add", + vec![param("item", Some("any"), false)], + Some("None"), + Some("Adds an element to the set."), + )), + "clear" => Some(sig( + "clear", + vec![], + Some("None"), + Some("Removes all elements from the set."), + )), + "contains" => Some(sig( + "contains", + vec![param("item", Some("any"), false)], + Some("bool"), + Some("Returns True if the set contains the item."), + )), + "difference" => Some(sig( + "difference", + vec![param("other", Some("iterable"), false)], + Some("set"), + Some("Returns a new set with elements in the set that are not in the others."), + )), + "discard" => Some(sig( + "discard", + vec![param("item", Some("any"), false)], + Some("None"), + Some("Removes an element from a set if it is a member."), + )), + "intersection" => Some(sig( + "intersection", + vec![param("other", Some("iterable"), false)], + Some("set"), + Some("Returns a new set with elements common to the set and all others."), + )), + "isdisjoint" => Some(sig( + "isdisjoint", + vec![param("other", Some("iterable"), false)], + Some("bool"), + Some("Returns True if two sets have a null intersection."), + )), + "issubset" => Some(sig( + "issubset", + vec![param("other", Some("iterable"), false)], + Some("bool"), + Some("Returns True if another set contains this set."), + )), + "issuperset" => Some(sig( + "issuperset", + vec![param("other", Some("iterable"), false)], + Some("bool"), + Some("Returns True if this set contains another set."), + )), + "pop" => Some(sig( + "pop", + vec![], + Some("any"), + Some("Removes and returns an arbitrary set element."), + )), + "remove" => Some(sig( + "remove", + vec![param("item", Some("any"), false)], + Some("None"), + Some("Removes an element from a set; it must be a member."), + )), + "symmetric_difference" => Some(sig( + "symmetric_difference", + vec![param("other", Some("iterable"), false)], + Some("set"), + Some("Returns a new set with elements in either the set or other but not both."), + )), + "union" => Some(sig( + "union", + vec![param("other", Some("iterable"), false)], + Some("set"), + Some("Returns a new set with elements from the set and all others."), + )), + "update" => Some(sig( + "update", + vec![param("other", Some("iterable"), false)], + Some("None"), + Some("Update the set, adding elements from all others."), + )), + _ => None, + } +} + +fn get_string_signature(method: &str) -> Option { + match method { + "split" => Some(sig( + "split", + vec![param("sep", Some("str"), true)], // Actually args.require_range(0, 1) in code? Let's check str.rs + Some("list"), + Some("Returns a list of the words in the string, using sep as the delimiter string."), + )), + // Checking str.rs for arguments... + // args.require_range(0, 1, "strip") + "strip" => Some(sig( + "strip", + vec![param("chars", Some("str"), true)], + Some("str"), + Some("Returns a copy of the string with leading and trailing characters removed."), + )), + "lstrip" => Some(sig( + "lstrip", + vec![param("chars", Some("str"), true)], + Some("str"), + Some("Returns a copy of the string with leading characters removed."), + )), + "rstrip" => Some(sig( + "rstrip", + vec![param("chars", Some("str"), true)], + Some("str"), + Some("Returns a copy of the string with trailing characters removed."), + )), + "lower" => Some(sig( + "lower", + vec![], + Some("str"), + Some("Returns a copy of the string converted to lowercase."), + )), + "upper" => Some(sig( + "upper", + vec![], + Some("str"), + Some("Returns a copy of the string converted to uppercase."), + )), + "capitalize" => Some(sig( + "capitalize", + vec![], + Some("str"), + Some("Returns a copy of the string with its first character capitalized."), + )), + "title" => Some(sig( + "title", + vec![], + Some("str"), + Some("Returns a version of the string where each word is titlecased."), + )), + "startswith" => Some(sig( + "startswith", + vec![param("prefix", Some("str"), false)], + Some("bool"), + Some("Returns True if the string starts with the specified prefix."), + )), + "endswith" => Some(sig( + "endswith", + vec![param("suffix", Some("str"), false)], + Some("bool"), + Some("Returns True if the string ends with the specified suffix."), + )), + "removeprefix" => Some(sig( + "removeprefix", + vec![param("prefix", Some("str"), false)], + Some("str"), + Some("Returns the string with the given prefix removed."), + )), + "removesuffix" => Some(sig( + "removesuffix", + vec![param("suffix", Some("str"), false)], + Some("str"), + Some("Returns the string with the given suffix removed."), + )), + "find" => Some(sig( + "find", + vec![param("sub", Some("str"), false)], + Some("int"), + Some("Returns the lowest index in the string where substring sub is found."), + )), + "index" => Some(sig( + "index", + vec![param("sub", Some("str"), false)], + Some("int"), + Some("Like find(), but raises ValueError when the substring is not found."), + )), + "rfind" => Some(sig( + "rfind", + vec![param("sub", Some("str"), false)], + Some("int"), + Some("Returns the highest index in the string where substring sub is found."), + )), + "rindex" => Some(sig( + "rindex", + vec![param("sub", Some("str"), false)], + Some("int"), + Some("Like rfind(), but raises ValueError when the substring is not found."), + )), + "count" => Some(sig( + "count", + vec![param("sub", Some("str"), false)], + Some("int"), + Some("Returns the number of non-overlapping occurrences of substring sub."), + )), + "replace" => Some(sig( + "replace", + vec![ + param("old", Some("str"), false), + param("new", Some("str"), false), + ], + Some("str"), + Some( + "Returns a copy of the string with all occurrences of substring old replaced by new.", + ), + )), + "join" => Some(sig( + "join", + vec![param("iterable", Some("iterable"), false)], + Some("str"), + Some("Returns a string which is the concatenation of the strings in iterable."), + )), + "partition" => Some(sig( + "partition", + vec![param("sep", Some("str"), false)], + Some("tuple"), + Some("Splits the string at the first occurrence of sep."), + )), + "rpartition" => Some(sig( + "rpartition", + vec![param("sep", Some("str"), false)], + Some("tuple"), + Some("Splits the string at the last occurrence of sep."), + )), + // "split" is handled above + // "splitlines"? + "codepoints" => Some(sig( + "codepoints", + vec![], + Some("list"), + Some("Returns a list of the integer codepoints in the string."), + )), + "elems" => Some(sig( + "elems", + vec![], + Some("list"), + Some("Returns a list of the characters in the string."), + )), + "isalnum" => Some(sig( + "isalnum", + vec![], + Some("bool"), + Some("Returns True if all characters in the string are alphanumeric."), + )), + "isalpha" => Some(sig( + "isalpha", + vec![], + Some("bool"), + Some("Returns True if all characters in the string are alphabetic."), + )), + "isdigit" => Some(sig( + "isdigit", + vec![], + Some("bool"), + Some("Returns True if all characters in the string are digits."), + )), + "islower" => Some(sig( + "islower", + vec![], + Some("bool"), + Some("Returns True if all cased characters in the string are lowercase."), + )), + "isupper" => Some(sig( + "isupper", + vec![], + Some("bool"), + Some("Returns True if all cased characters in the string are uppercase."), + )), + "isspace" => Some(sig( + "isspace", + vec![], + Some("bool"), + Some("Returns True if there are only whitespace characters in the string."), + )), + "istitle" => Some(sig( + "istitle", + vec![], + Some("bool"), + Some("Returns True if the string is a titlecased string."), + )), + _ => None, + } +} diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/mod.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/mod.rs index 43352ee26..0296d4bd2 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/interpreter/mod.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/mod.rs @@ -4,7 +4,7 @@ pub mod error; mod eval; mod exec; pub mod introspection; -mod methods; +pub mod methods; pub mod operations; pub mod printer; @@ -13,4 +13,7 @@ pub use self::core::Interpreter; pub use self::error::EldritchError; #[allow(unused_imports)] pub use self::error::EldritchErrorKind; +pub use self::methods::get_native_method_signature; pub use self::printer::{BufferPrinter, Printer, StdoutPrinter}; +pub mod signature; +pub use self::signature::{MethodSignature, ParameterSignature}; diff --git a/implants/lib/eldritchv2/eldritch-core/src/interpreter/signature.rs b/implants/lib/eldritchv2/eldritch-core/src/interpreter/signature.rs new file mode 100644 index 000000000..30af3809c --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-core/src/interpreter/signature.rs @@ -0,0 +1,20 @@ +use alloc::string::String; +use alloc::vec::Vec; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ParameterSignature { + pub name: String, + pub type_name: Option, + pub is_optional: bool, + pub is_variadic: bool, // for *args + pub is_kwargs: bool, // for **kwargs +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MethodSignature { + pub name: String, + pub params: Vec, + pub return_type: Option, + pub doc: Option, + pub deprecated: Option, +} diff --git a/implants/lib/eldritchv2/eldritch-core/src/lib.rs b/implants/lib/eldritchv2/eldritch-core/src/lib.rs index d21aeb325..383a30b62 100644 --- a/implants/lib/eldritchv2/eldritch-core/src/lib.rs +++ b/implants/lib/eldritchv2/eldritch-core/src/lib.rs @@ -13,7 +13,12 @@ mod token; // Re-export core types pub use ast::{Environment, ForeignValue, Value}; -pub use interpreter::{BufferPrinter, Interpreter, Printer, StdoutPrinter}; +// Export AST nodes for LSP/Tooling +pub use ast::{Argument, Expr, ExprKind, FStringSegment, Param, Stmt, StmtKind}; + +pub use interpreter::{ + BufferPrinter, Interpreter, Printer, StdoutPrinter, get_native_method_signature, +}; pub use lexer::Lexer; pub use token::{Span, TokenKind}; @@ -26,4 +31,5 @@ pub use interpreter::introspection; // Tests integration via `tests/` directory sees `eldritch_core` as an external crate. // So we must `pub use` it here for tests to see it. // The `Lexer` and `TokenKind` are already re-exported. +pub use interpreter::{MethodSignature, ParameterSignature}; pub use parser::Parser; diff --git a/implants/lib/eldritchv2/eldritch-core/tests/builtins_edge_cases.rs b/implants/lib/eldritchv2/eldritch-core/tests/builtins_edge_cases.rs index 15279b34e..e3976dac0 100644 --- a/implants/lib/eldritchv2/eldritch-core/tests/builtins_edge_cases.rs +++ b/implants/lib/eldritchv2/eldritch-core/tests/builtins_edge_cases.rs @@ -111,7 +111,7 @@ mod tests { // Let's check if it returns an error or works. // Assuming current implementation requires a callable. // If it errors, that's fine, we just want to ensure it doesn't panic. - let res = run_code("filter(None, [1, 0, 2])"); + let _res = run_code("filter(None, [1, 0, 2])"); // If it's not supported, it might error with "not callable" or similar. // We'll just assert it doesn't panic. diff --git a/implants/lib/eldritchv2/eldritch-core/tests/bytes_subscript.rs b/implants/lib/eldritchv2/eldritch-core/tests/bytes_subscript.rs new file mode 100644 index 000000000..7770b85e2 --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-core/tests/bytes_subscript.rs @@ -0,0 +1,36 @@ +use eldritch_core::{Interpreter, Value}; + +#[test] +fn test_bytes_subscript() { + let mut interp = Interpreter::new(); + let code = r#" +b = b"hello world" +a = b[0] +b_slice = b[0:5] +b_slice_step = b[::2] +"#; + + interp.interpret(code).unwrap(); + + // Check results by interpreting expressions that return the values + let a = interp.interpret("a").unwrap(); + if let Value::Int(i) = a { + assert_eq!(i, 104); // 'h' + } else { + panic!("b[0] should be Int, got {:?}", a); + } + + let b_slice = interp.interpret("b_slice").unwrap(); + if let Value::Bytes(b) = b_slice { + assert_eq!(b, b"hello".to_vec()); + } else { + panic!("b[0:5] should be Bytes, got {:?}", b_slice); + } + + let b_slice_step = interp.interpret("b_slice_step").unwrap(); + if let Value::Bytes(b) = b_slice_step { + assert_eq!(b, b"hlowrd".to_vec()); + } else { + panic!("b[::2] should be Bytes, got {:?}", b_slice_step); + } +} diff --git a/implants/lib/eldritchv2/eldritch-core/tests/slicing_coverage.rs b/implants/lib/eldritchv2/eldritch-core/tests/slicing_coverage.rs deleted file mode 100644 index 4ec650332..000000000 --- a/implants/lib/eldritchv2/eldritch-core/tests/slicing_coverage.rs +++ /dev/null @@ -1,134 +0,0 @@ -mod assert; - -#[test] -fn test_list_slicing_basic() { - assert::pass( - r#" - l = [0, 1, 2, 3, 4, 5] - assert_eq(l[0:6], l) - assert_eq(l[:], l) - assert_eq(l[0:3], [0, 1, 2]) - assert_eq(l[3:], [3, 4, 5]) - assert_eq(l[:3], [0, 1, 2]) - assert_eq(l[3:6], [3, 4, 5]) - "#, - ); -} - -#[test] -fn test_list_slicing_steps() { - assert::pass( - r#" - l = [0, 1, 2, 3, 4, 5] - assert_eq(l[::2], [0, 2, 4]) - assert_eq(l[1::2], [1, 3, 5]) - assert_eq(l[::3], [0, 3]) - assert_eq(l[::100], [0]) - "#, - ); -} - -#[test] -fn test_list_slicing_negative_indices() { - assert::pass( - r#" - l = [0, 1, 2, 3, 4, 5] - assert_eq(l[-1], 5) - assert_eq(l[-2], 4) - assert_eq(l[:-1], [0, 1, 2, 3, 4]) - assert_eq(l[-3:], [3, 4, 5]) - assert_eq(l[-3:-1], [3, 4]) - "#, - ); -} - -#[test] -fn test_list_slicing_negative_steps() { - assert::pass( - r#" - l = [0, 1, 2, 3, 4, 5] - assert_eq(l[::-1], [5, 4, 3, 2, 1, 0]) - assert_eq(l[::-2], [5, 3, 1]) - assert_eq(l[4:2:-1], [4, 3]) - assert_eq(l[2:4:-1], []) - "#, - ); -} - -#[test] -fn test_list_slicing_empty_result_edge_cases() { - assert::pass( - r#" - l = [0, 1, 2, 3, 4, 5] - # Start > Stop with positive step - assert_eq(l[4:2], []) - # Start < Stop with negative step - assert_eq(l[2:4:-1], []) - # Out of bounds start (positive) - assert_eq(l[100:], []) - # Out of bounds stop (negative) - assert_eq(l[:-100], []) - "#, - ); -} - -#[test] -fn test_list_slicing_out_of_bounds() { - assert::pass( - r#" - l = [0, 1, 2] - assert_eq(l[0:100], [0, 1, 2]) - assert_eq(l[-100:], [0, 1, 2]) - assert_eq(l[-100:-50], []) - "#, - ); -} - -#[test] -fn test_string_slicing_extended() { - assert::pass( - r#" - s = "012345" - assert_eq(s[::2], "024") - assert_eq(s[::-1], "543210") - assert_eq(s[-3:], "345") - assert_eq(s[100:], "") - assert_eq(s[-100:], "012345") - - # Empty string - assert_eq(""[:], "") - assert_eq(""[::-1], "") - "#, - ); -} - -#[test] -fn test_tuple_slicing_extended() { - assert::pass( - r#" - t = (0, 1, 2, 3, 4, 5) - assert_eq(t[::2], (0, 2, 4)) - assert_eq(t[::-1], (5, 4, 3, 2, 1, 0)) - assert_eq(t[100:], ()) - "#, - ); -} - -#[test] -fn test_bytes_slicing_not_supported() { - assert::fail( - r#" - b = b"012345" - b[::2] - "#, - "'bytes' object is not subscriptable", - ); -} - -#[test] -fn test_slicing_zero_step_error() { - assert::fail("l = [1]; l[::0]", "slice step cannot be zero"); - assert::fail("s = 'a'; s[::0]", "slice step cannot be zero"); - assert::fail("t = (1,); t[::0]", "slice step cannot be zero"); - assert::fail("b = b'a'; b[::0]", "slice step cannot be zero"); -} diff --git a/implants/lib/eldritchv2/eldritch-lsp/Cargo.toml b/implants/lib/eldritchv2/eldritch-lsp/Cargo.toml new file mode 100644 index 000000000..5341e0a25 --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-lsp/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "eldritch-lsp" +version = "0.1.0" +edition = "2021" +authors = ["Realm"] +description = "Language Server for Eldritch v2" +license = "MIT" + +[dependencies] +eldritch-core = { path = "../eldritch-core", features = ["std"] } +eldritchv2 = { path = "../eldritchv2", features = ["stdlib", "fake_bindings"] } +lsp-server = "0.7" +lsp-types = "0.94" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +log = "0.4" +simplelog = "0.12" +anyhow = "1.0" +crossbeam-channel = "0.5" +spin = { workspace = true, features = ["mutex", "spin_mutex", "rwlock"] } + +[features] +default = [] diff --git a/implants/lib/eldritchv2/eldritch-lsp/src/linter/deprecation.rs b/implants/lib/eldritchv2/eldritch-lsp/src/linter/deprecation.rs new file mode 100644 index 000000000..bb5bd94ea --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-lsp/src/linter/deprecation.rs @@ -0,0 +1,49 @@ +use eldritch_core::{ExprKind, Stmt, StmtKind, Value}; +use eldritchv2::Interpreter; +use lsp_types::{Diagnostic, DiagnosticSeverity}; + +use super::{utils::span_to_range, visitors::visit_stmts, LintRule}; + +pub struct DeprecationRule; + +impl LintRule for DeprecationRule { + fn name(&self) -> &str { + "deprecation" + } + + fn check(&self, stmts: &[Stmt], source: &str, interp: &Interpreter) -> Vec { + let mut diags = Vec::new(); + visit_stmts(stmts, &mut |stmt| { + if let StmtKind::Expression(expr) = &stmt.kind { + // Check for deprecated method calls + if let ExprKind::Call(callee, _) = &expr.kind { + if let ExprKind::GetAttr(obj, name) = &callee.kind { + // Attempt to resolve the object + if let ExprKind::Identifier(var_name) = &obj.kind { + // We can only check global variables or imported modules if they are present in the interpreter + // This is a best-effort check since we don't have full type inference. + if let Some(val) = interp.lookup_variable(var_name) { + if let Value::Foreign(foreign_obj) = val { + if let Some(sig) = foreign_obj.get_method_signature(name) { + if let Some(reason) = sig.deprecated { + diags.push(Diagnostic { + range: span_to_range(expr.span, source), + severity: Some(DiagnosticSeverity::WARNING), + message: format!( + "Method '{}.{}' is deprecated: {}", + var_name, name, reason + ), + ..Default::default() + }); + } + } + } + } + } + } + } + } + }); + diags + } +} diff --git a/implants/lib/eldritchv2/eldritch-lsp/src/linter/mod.rs b/implants/lib/eldritchv2/eldritch-lsp/src/linter/mod.rs new file mode 100644 index 000000000..09dbb816a --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-lsp/src/linter/mod.rs @@ -0,0 +1,257 @@ +use eldritch_core::{Stmt, Value}; +use eldritchv2::Interpreter; +use lsp_types::Diagnostic; +use spin::RwLock; +use std::collections::BTreeMap; +use std::sync::Arc; + +mod deprecation; +mod type_check; +mod undefined_symbol; +pub mod utils; +mod visitors; + +use self::deprecation::DeprecationRule; +use self::type_check::TypeCheckRule; +use self::undefined_symbol::UndefinedSymbolRule; + +pub trait LintRule { + #[allow(dead_code)] + fn name(&self) -> &str; + fn check(&self, stmts: &[Stmt], source: &str, interp: &Interpreter) -> Vec; +} + +pub struct Linter { + rules: Vec>, +} + +impl Linter { + pub fn new() -> Self { + Self { + rules: vec![ + Box::new(DeprecationRule), + Box::new(TypeCheckRule), + Box::new(UndefinedSymbolRule), + ], + } + } + + pub fn check(&self, stmts: &[Stmt], source: &str) -> Vec { + let mut interp = Interpreter::new().with_default_libs().with_fake_agent(); + + // Inject input_params + #[allow(clippy::mutable_key_type)] + let params = BTreeMap::new(); + let params_val = Value::Dictionary(Arc::new(RwLock::new(params))); + interp.define_variable("input_params", params_val); + + let mut diagnostics = Vec::new(); + for rule in &self.rules { + diagnostics.extend(rule.check(stmts, source, &interp)); + } + diagnostics + } +} + +#[cfg(test)] +mod tests { + use super::*; + use eldritch_core::{Lexer, Parser}; + use lsp_types::DiagnosticSeverity; + + #[test] + fn test_deprecation_rule() { + let code = "os.system('ls')"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + // We expect at least one diagnostic warning about deprecation + let warnings: Vec<_> = diagnostics + .iter() + .filter(|d| d.message.contains("os.system is deprecated")) + .collect(); + assert_eq!(warnings.len(), 1); + assert_eq!(warnings[0].severity, Some(DiagnosticSeverity::WARNING)); + } + + #[test] + fn test_type_check_missing_method() { + let code = "agent.not_a_method()"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + assert!(!diagnostics.is_empty()); + assert!(diagnostics[0] + .message + .contains("has no attribute 'not_a_method'")); + } + + #[test] + fn test_type_check_binary_op() { + let code = "x = [] + \"a\""; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + assert!(!diagnostics.is_empty()); + assert!(diagnostics[0].message.contains("Unsupported operand types")); + } + + #[test] + fn test_type_check_wrong_arg_type() { + let code = "sys.exec({'what': 'adict'})"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + if diagnostics.is_empty() { + panic!("No diagnostics found"); + } + println!("Diagnostics: {:?}", diagnostics); + + assert!(!diagnostics.is_empty()); + let found = diagnostics.iter().any(|d| { + d.message + .contains("TypeError: Argument 'path' expected type 'str', got 'Dictionary'") + }); + assert!(found, "Expected error not found"); + } + + #[test] + fn test_type_check_missing_args() { + let code = "sys.exec()"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + assert!(!diagnostics.is_empty()); + // sys.exec takes path, args. + let found = diagnostics.iter().any(|d| { + d.message + .contains("TypeError: Missing required arguments: path, args") + }); + assert!( + found, + "Expected missing args error not found. Diagnostics: {:?}", + diagnostics + ); + } + + #[test] + fn test_undefined_symbol_basic() { + let code = "print(not_defined)"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + assert!(!diagnostics.is_empty()); + assert!(diagnostics.iter().any(|d| d + .message + .contains("NameError: name 'not_defined' is not defined"))); + } + + #[test] + fn test_undefined_symbol_defined() { + let code = "x = 1; print(x)"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + // Filter out irrelevant diagnostics (like type checks if any) + let name_errors: Vec<_> = diagnostics + .iter() + .filter(|d| d.message.contains("NameError")) + .collect(); + assert!(name_errors.is_empty(), "Found NameError: {:?}", name_errors); + } + + #[test] + fn test_undefined_symbol_function_scope() { + let code = "def foo(a):\n print(a)\n print(b) # Error"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + let name_errors: Vec<_> = diagnostics + .iter() + .filter(|d| d.message.contains("NameError")) + .collect(); + assert_eq!(name_errors.len(), 1); + assert!(name_errors[0].message.contains("'b' is not defined")); + } + + #[test] + fn test_undefined_symbol_for_loop() { + let code = "for i in [1, 2]:\n print(i)\nprint(i) # Error: Loop var local to loop"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + let name_errors: Vec<_> = diagnostics + .iter() + .filter(|d| d.message.contains("NameError")) + .collect(); + assert_eq!(name_errors.len(), 1); + assert!(name_errors[0].message.contains("'i' is not defined")); + } + + #[test] + fn test_input_params_defined() { + let code = "print(input_params)"; + let mut lexer = Lexer::new(code.to_string()); + let tokens = lexer.scan_tokens().unwrap(); + let mut parser = Parser::new(tokens); + let stmts = parser.parse().unwrap(); + + let linter = Linter::new(); + let diagnostics = linter.check(&stmts, code); + + let name_errors: Vec<_> = diagnostics + .iter() + .filter(|d| d.message.contains("NameError")) + .collect(); + assert!( + name_errors.is_empty(), + "Found NameError for input_params: {:?}", + name_errors + ); + } +} diff --git a/implants/lib/eldritchv2/eldritch-lsp/src/linter/type_check.rs b/implants/lib/eldritchv2/eldritch-lsp/src/linter/type_check.rs new file mode 100644 index 000000000..a083f4986 --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-lsp/src/linter/type_check.rs @@ -0,0 +1,232 @@ +use eldritch_core::{Argument, ExprKind, Stmt, Value}; +use eldritchv2::Interpreter; +use lsp_types::{Diagnostic, DiagnosticSeverity}; +use std::collections::BTreeMap; + +use super::{utils::span_to_range, visitors::visit_stmts_exprs, LintRule}; + +pub struct TypeCheckRule; + +impl LintRule for TypeCheckRule { + fn name(&self) -> &str { + "type_check" + } + + fn check(&self, stmts: &[Stmt], source: &str, interp: &Interpreter) -> Vec { + let mut diags = Vec::new(); + visit_stmts_exprs(stmts, &mut |expr| { + // Check invalid binary ops + if let ExprKind::BinaryOp(lhs, op, rhs) = &expr.kind { + if let (Some(l_type), Some(r_type)) = (infer_type(lhs), infer_type(rhs)) { + match op { + eldritch_core::TokenKind::Plus => { + if l_type == "List" && r_type == "String" { + diags.push(Diagnostic { + range: span_to_range(expr.span, source), + severity: Some(DiagnosticSeverity::ERROR), + message: format!( + "TypeError: Unsupported operand types for +: '{}' and '{}'", + l_type, r_type + ), + ..Default::default() + }); + } + } + _ => {} + } + } + } + + // Check calls + if let ExprKind::Call(callee, args) = &expr.kind { + if let ExprKind::GetAttr(obj, method_name) = &callee.kind { + if let ExprKind::Identifier(var_name) = &obj.kind { + // Lookup variable in interpreter (libraries are globals) + if let Some(val) = interp.lookup_variable(var_name) { + if let Value::Foreign(foreign_obj) = val { + // 1. Check method existence + let methods = foreign_obj.method_names(); + if !methods.contains(method_name) { + diags.push(Diagnostic { + range: span_to_range(callee.span, source), + severity: Some(DiagnosticSeverity::ERROR), + message: format!( + "AttributeError: '{}' object has no attribute '{}'", + foreign_obj.type_name(), + method_name + ), + ..Default::default() + }); + } else { + // 2. Check arguments if signature is available + if let Some(sig) = foreign_obj.get_method_signature(method_name) + { + check_arguments(&sig, args, expr.span, source, &mut diags); + } + } + } + } + } + } + } + }); + diags + } +} + +fn check_arguments( + sig: &eldritch_core::MethodSignature, + args: &[Argument], + span: eldritch_core::Span, + source: &str, + diags: &mut Vec, +) { + let mut positional_count = 0; + let mut kw_args_present = BTreeMap::new(); + + for arg in args { + match arg { + Argument::Positional(_) => positional_count += 1, + Argument::Keyword(k, _) => { + kw_args_present.insert(k.clone(), ()); + } + _ => return, // Give up on *args / **kwargs for now + } + } + + // Check argument count + // Count required positionals + let mut required_params = 0; + let mut param_names = Vec::new(); + for p in &sig.params { + if !p.is_optional && !p.is_kwargs && !p.is_variadic { + required_params += 1; + } + param_names.push(p.name.clone()); + } + + // This is a naive check, doesn't handle mix of positional + keyword for same param perfectly + // But good enough for basic cases + if positional_count < required_params { + // Check if missing are covered by kwargs + let mut missing = Vec::new(); + for i in positional_count..sig.params.len() { + let p = &sig.params[i]; + if !p.is_optional && !kw_args_present.contains_key(&p.name) { + missing.push(p.name.clone()); + } + } + + if !missing.is_empty() { + diags.push(Diagnostic { + range: span_to_range(span, source), + severity: Some(DiagnosticSeverity::ERROR), + message: format!( + "TypeError: Missing required arguments: {}", + missing.join(", ") + ), + ..Default::default() + }); + } + } + + // Type checking for arguments + let mut param_idx = 0; + for arg in args { + match arg { + Argument::Positional(expr) => { + if param_idx < sig.params.len() { + let param = &sig.params[param_idx]; + check_arg_type(param, expr, source, diags); + } + param_idx += 1; + } + Argument::Keyword(name, expr) => { + // Find param by name + if let Some(param) = sig.params.iter().find(|p| &p.name == name) { + check_arg_type(param, expr, source, diags); + } + } + _ => {} + } + } +} + +fn check_arg_type( + param: &eldritch_core::ParameterSignature, + expr: &eldritch_core::Expr, + source: &str, + diags: &mut Vec, +) { + if let Some(expected_type_raw) = ¶m.type_name { + // Clean up expected type (e.g. "Option < String >" -> "String", "Vec < String >" -> "List") + let expected_type = clean_type_name(expected_type_raw); + if let Some(actual_type) = infer_type(expr) { + if !is_type_compatible(&expected_type, actual_type) { + diags.push(Diagnostic { + range: span_to_range(expr.span, source), + severity: Some(DiagnosticSeverity::ERROR), + message: format!( + "TypeError: Argument '{}' expected type '{}', got '{}'", + param.name, expected_type, actual_type + ), + ..Default::default() + }); + } + } + } +} + +fn clean_type_name(raw: &str) -> String { + let raw = raw + .replace("alloc :: string :: ", "") + .replace("alloc :: vec :: ", ""); + if raw.contains("Option <") { + return raw + .replace("Option <", "") + .replace(">", "") + .trim() + .to_string(); + } + if raw.contains("Vec <") { + return "List".to_string(); // Approximate Vec as List + } + if raw.contains("BTreeMap <") { + return "Dictionary".to_string(); + } + raw.replace("String", "str") + .replace("i64", "int") + .replace("f64", "float") + .replace("bool", "bool") + .replace("Vec < u8 >", "bytes") +} + +fn is_type_compatible(expected: &str, actual: &str) -> bool { + match expected { + "str" | "String" => actual == "String", + "int" | "i64" => actual == "Int", + "float" | "f64" => actual == "Float" || actual == "Int", // Allow Int for Float + "bool" => actual == "Bool", + "List" => actual == "List", + "Dictionary" => actual == "Dictionary", + _ => true, // Unknown expected type, pass + } +} + +// Helper to infer simple types +fn infer_type(expr: &eldritch_core::Expr) -> Option<&'static str> { + match &expr.kind { + ExprKind::Literal(val) => match val { + Value::String(_) => Some("String"), + Value::List(_) => Some("List"), + Value::Dictionary(_) => Some("Dictionary"), + Value::Int(_) => Some("Int"), + _ => None, + }, + ExprKind::List(_) => Some("List"), + ExprKind::Dictionary(_) => Some("Dictionary"), + ExprKind::Tuple(_) => Some("Tuple"), + ExprKind::Set(_) => Some("Set"), + _ => None, + } +} diff --git a/implants/lib/eldritchv2/eldritch-lsp/src/linter/undefined_symbol.rs b/implants/lib/eldritchv2/eldritch-lsp/src/linter/undefined_symbol.rs new file mode 100644 index 000000000..34274eff1 --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-lsp/src/linter/undefined_symbol.rs @@ -0,0 +1,363 @@ +use eldritch_core::{Argument, ExprKind, Param, Stmt, StmtKind}; +use eldritchv2::Interpreter; +use lsp_types::{Diagnostic, DiagnosticSeverity}; +use std::collections::HashSet; + +use super::{utils::span_to_range, LintRule}; + +pub struct UndefinedSymbolRule; + +impl LintRule for UndefinedSymbolRule { + fn name(&self) -> &str { + "undefined_symbol" + } + + fn check(&self, stmts: &[Stmt], source: &str, interp: &Interpreter) -> Vec { + let mut visitor = SymbolVisitor::new(source, interp); + visitor.visit_stmts(stmts); + visitor.diagnostics + } +} + +struct Scope { + vars: HashSet, +} + +struct SymbolVisitor<'a> { + scopes: Vec, + diagnostics: Vec, + source: &'a str, + interp: &'a Interpreter, +} + +impl<'a> SymbolVisitor<'a> { + fn new(source: &'a str, interp: &'a Interpreter) -> Self { + Self { + scopes: vec![Scope { + vars: HashSet::new(), + }], // Module scope + diagnostics: Vec::new(), + source, + interp, + } + } + + fn push_scope(&mut self) { + self.scopes.push(Scope { + vars: HashSet::new(), + }); + } + + fn pop_scope(&mut self) { + self.scopes.pop(); + } + + fn define(&mut self, name: &str) { + if let Some(scope) = self.scopes.last_mut() { + scope.vars.insert(name.to_string()); + } + } + + fn is_defined(&self, name: &str) -> bool { + // Check local scopes + for scope in self.scopes.iter().rev() { + if scope.vars.contains(name) { + return true; + } + } + // Check interpreter globals/builtins + self.interp.lookup_variable(name).is_some() + } + + fn visit_stmts(&mut self, stmts: &[Stmt]) { + for stmt in stmts { + self.visit_stmt(stmt); + } + } + + fn visit_stmt(&mut self, stmt: &Stmt) { + match &stmt.kind { + StmtKind::Expression(expr) => self.visit_expr(expr), + StmtKind::Assignment(lhs, type_hint, rhs) => { + // Visit rhs first (usage) + self.visit_expr(rhs); + // Visit type hint (usage) + if let Some(th) = type_hint { + self.visit_expr(th); + } + // Define vars in lhs + self.visit_assignment_target(lhs); + } + StmtKind::AugmentedAssignment(lhs, _, rhs) => { + self.visit_expr(rhs); + // lhs is also read in augmented assignment + self.visit_expr(lhs); + } + StmtKind::If(cond, then_branch, else_branch) => { + self.visit_expr(cond); + self.visit_stmts(then_branch); + if let Some(else_stmts) = else_branch { + self.visit_stmts(else_stmts); + } + } + StmtKind::Return(expr_opt) => { + if let Some(expr) = expr_opt { + self.visit_expr(expr); + } + } + StmtKind::Def(name, params, ret_hint, body) => { + // Function name defined in current scope + self.define(name); + + // Default values and type hints evaluated in current scope + for param in params { + match param { + Param::Normal(_, hint) => { + if let Some(h) = hint { + self.visit_expr(h); + } + } + Param::WithDefault(_, hint, default) => { + if let Some(h) = hint { + self.visit_expr(h); + } + self.visit_expr(default); + } + Param::Star(_, hint) => { + if let Some(h) = hint { + self.visit_expr(h); + } + } + Param::StarStar(_, hint) => { + if let Some(h) = hint { + self.visit_expr(h); + } + } + } + } + if let Some(h) = ret_hint { + self.visit_expr(h); + } + + self.push_scope(); + for param in params { + match param { + Param::Normal(n, _) => self.define(n), + Param::WithDefault(n, _, _) => self.define(n), + Param::Star(n, _) => self.define(n), + Param::StarStar(n, _) => self.define(n), + } + } + self.visit_stmts(body); + self.pop_scope(); + } + StmtKind::For(vars, iterable, body) => { + self.visit_expr(iterable); + self.push_scope(); + for v in vars { + self.define(v); + } + self.visit_stmts(body); + self.pop_scope(); + } + StmtKind::Break => {} + StmtKind::Continue => {} + StmtKind::Pass => {} + } + } + + fn visit_assignment_target(&mut self, expr: &eldritch_core::Expr) { + match &expr.kind { + ExprKind::Identifier(name) => self.define(name), + ExprKind::Tuple(exprs) | ExprKind::List(exprs) => { + for e in exprs { + self.visit_assignment_target(e); + } + } + ExprKind::Index(obj, idx) => { + // Not defining obj or idx, but using them + self.visit_expr(obj); + self.visit_expr(idx); + } + ExprKind::GetAttr(obj, _) => { + // Attribute assignment: obj.attr = val. 'obj' is used. + self.visit_expr(obj); + } + _ => { + // Other expressions on LHS are likely invalid but if valid, treat as usages? + // Actually if they appear on LHS they might be fancy unpacking or just invalid. + // We'll visit them as usages just in case. + self.visit_expr(expr); + } + } + } + + fn visit_expr(&mut self, expr: &eldritch_core::Expr) { + match &expr.kind { + ExprKind::Identifier(name) => { + if !self.is_defined(name) { + self.diagnostics.push(Diagnostic { + range: span_to_range(expr.span, self.source), + severity: Some(DiagnosticSeverity::ERROR), + message: format!("NameError: name '{}' is not defined", name), + ..Default::default() + }); + } + } + ExprKind::Literal(_) => {} + ExprKind::BinaryOp(lhs, _, rhs) => { + self.visit_expr(lhs); + self.visit_expr(rhs); + } + ExprKind::UnaryOp(_, op) => self.visit_expr(op), + ExprKind::LogicalOp(lhs, _, rhs) => { + self.visit_expr(lhs); + self.visit_expr(rhs); + } + ExprKind::Call(callee, args) => { + self.visit_expr(callee); + for arg in args { + match arg { + Argument::Positional(e) => self.visit_expr(e), + Argument::Keyword(_, e) => self.visit_expr(e), + Argument::StarArgs(e) => self.visit_expr(e), + Argument::KwArgs(e) => self.visit_expr(e), + } + } + } + ExprKind::List(exprs) => { + for e in exprs { + self.visit_expr(e); + } + } + ExprKind::Tuple(exprs) => { + for e in exprs { + self.visit_expr(e); + } + } + ExprKind::Dictionary(kv_pairs) => { + for (k, v) in kv_pairs { + self.visit_expr(k); + self.visit_expr(v); + } + } + ExprKind::Set(exprs) => { + for e in exprs { + self.visit_expr(e); + } + } + ExprKind::Index(obj, idx) => { + self.visit_expr(obj); + self.visit_expr(idx); + } + ExprKind::GetAttr(obj, _) => { + self.visit_expr(obj); + } + ExprKind::Slice(obj, start, end, step) => { + self.visit_expr(obj); + if let Some(e) = start { + self.visit_expr(e); + } + if let Some(e) = end { + self.visit_expr(e); + } + if let Some(e) = step { + self.visit_expr(e); + } + } + ExprKind::FString(segments) => { + for seg in segments { + if let eldritch_core::FStringSegment::Expression(e) = seg { + self.visit_expr(e); + } + } + } + ExprKind::ListComp { + body, + var, + iterable, + cond, + } => { + self.visit_expr(iterable); + self.push_scope(); + self.define(var); + if let Some(c) = cond { + self.visit_expr(c); + } + self.visit_expr(body); + self.pop_scope(); + } + ExprKind::DictComp { + key, + value, + var, + iterable, + cond, + } => { + self.visit_expr(iterable); + self.push_scope(); + self.define(var); + if let Some(c) = cond { + self.visit_expr(c); + } + self.visit_expr(key); + self.visit_expr(value); + self.pop_scope(); + } + ExprKind::SetComp { + body, + var, + iterable, + cond, + } => { + self.visit_expr(iterable); + self.push_scope(); + self.define(var); + if let Some(c) = cond { + self.visit_expr(c); + } + self.visit_expr(body); + self.pop_scope(); + } + ExprKind::Lambda { params, body } => { + for param in params { + match param { + Param::WithDefault(_, hint, default) => { + if let Some(h) = hint { + self.visit_expr(h); + } + self.visit_expr(default); + } + Param::Normal(_, hint) + | Param::Star(_, hint) + | Param::StarStar(_, hint) => { + if let Some(h) = hint { + self.visit_expr(h); + } + } + } + } + self.push_scope(); + for param in params { + match param { + Param::Normal(n, _) => self.define(n), + Param::WithDefault(n, _, _) => self.define(n), + Param::Star(n, _) => self.define(n), + Param::StarStar(n, _) => self.define(n), + } + } + self.visit_expr(body); + self.pop_scope(); + } + ExprKind::If { + cond, + then_branch, + else_branch, + } => { + self.visit_expr(cond); + self.visit_expr(then_branch); + self.visit_expr(else_branch); + } + } + } +} diff --git a/implants/lib/eldritchv2/eldritch-lsp/src/linter/utils.rs b/implants/lib/eldritchv2/eldritch-lsp/src/linter/utils.rs new file mode 100644 index 000000000..a9d7fc5a2 --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-lsp/src/linter/utils.rs @@ -0,0 +1,46 @@ +use eldritch_core::Span; +use lsp_types::{Position, Range}; + +pub fn span_to_range(span: Span, source: &str) -> Range { + let start_line_idx = span.line.saturating_sub(1); + let mut current_line = 0; + let mut offset = 0; + let mut line_start_offset = 0; + + for (i, b) in source.bytes().enumerate() { + if current_line == start_line_idx { + line_start_offset = offset; + break; + } + if b == b'\n' { + current_line += 1; + offset = i + 1; + } + } + if current_line < start_line_idx { + line_start_offset = offset; + } + + let start_col = span.start.saturating_sub(line_start_offset); + let (end_line, end_col) = byte_offset_to_pos(span.end, source); + + Range::new( + Position::new(start_line_idx as u32, start_col as u32), + Position::new(end_line as u32, end_col as u32), + ) +} + +fn byte_offset_to_pos(offset: usize, source: &str) -> (usize, usize) { + let mut line = 0; + let mut last_line_start = 0; + for (i, b) in source.bytes().enumerate() { + if i == offset { + return (line, i - last_line_start); + } + if b == b'\n' { + line += 1; + last_line_start = i + 1; + } + } + (line, offset.saturating_sub(last_line_start)) +} diff --git a/implants/lib/eldritchv2/eldritch-lsp/src/linter/visitors.rs b/implants/lib/eldritchv2/eldritch-lsp/src/linter/visitors.rs new file mode 100644 index 000000000..d8808048e --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-lsp/src/linter/visitors.rs @@ -0,0 +1,82 @@ +use eldritch_core::{Argument, ExprKind, Stmt, StmtKind}; + +pub fn visit_stmts(stmts: &[Stmt], callback: &mut F) +where + F: FnMut(&Stmt), +{ + for stmt in stmts { + callback(stmt); + match &stmt.kind { + StmtKind::If(_, then_branch, else_branch) => { + visit_stmts(then_branch, callback); + if let Some(else_b) = else_branch { + visit_stmts(else_b, callback); + } + } + StmtKind::For(_, _, body) => { + visit_stmts(body, callback); + } + StmtKind::Def(_, _, _, body) => { + visit_stmts(body, callback); + } + // Block variant does not exist in StmtKind, blocks are Vec + _ => {} + } + } +} + +pub fn visit_stmts_exprs(stmts: &[Stmt], callback: &mut F) +where + F: FnMut(&eldritch_core::Expr), +{ + for stmt in stmts { + match &stmt.kind { + StmtKind::Expression(expr) => { + visit_expr(expr, callback); + } + StmtKind::Assignment(lhs, _, rhs) => { + visit_expr(lhs, callback); + visit_expr(rhs, callback); + } + StmtKind::If(cond, then_b, else_b) => { + visit_expr(cond, callback); + visit_stmts_exprs(then_b, callback); + if let Some(b) = else_b { + visit_stmts_exprs(b, callback); + } + } + StmtKind::For(_, iter, body) => { + visit_expr(iter, callback); + visit_stmts_exprs(body, callback); + } + StmtKind::Def(_, _, _, body) => { + visit_stmts_exprs(body, callback); + } + _ => {} + } + } +} + +pub fn visit_expr(expr: &eldritch_core::Expr, callback: &mut F) +where + F: FnMut(&eldritch_core::Expr), +{ + callback(expr); + match &expr.kind { + ExprKind::BinaryOp(lhs, _, rhs) => { + visit_expr(lhs, callback); + visit_expr(rhs, callback); + } + ExprKind::Call(callee, args) => { + visit_expr(callee, callback); + for arg in args { + match arg { + Argument::Positional(e) => visit_expr(e, callback), + Argument::Keyword(_, e) => visit_expr(e, callback), + _ => {} + } + } + } + _ => {} + } +} diff --git a/implants/lib/eldritchv2/eldritch-lsp/src/main.rs b/implants/lib/eldritchv2/eldritch-lsp/src/main.rs new file mode 100644 index 000000000..b9c43c84b --- /dev/null +++ b/implants/lib/eldritchv2/eldritch-lsp/src/main.rs @@ -0,0 +1,644 @@ +use anyhow::Result; +use crossbeam_channel::Sender; +use eldritch_core::{Lexer, Parser}; +use eldritchv2::{Interpreter, Value}; +use lsp_server::{Connection, Message, Notification, Response}; +use lsp_types::{ + notification::{ + DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, + Notification as LspNotification, PublishDiagnostics, + }, + request::{Completion, HoverRequest, Request as LspRequest}, + CompletionItem, CompletionItemKind, CompletionParams, CompletionResponse, Diagnostic, + DiagnosticSeverity, DidChangeTextDocumentParams, DidCloseTextDocumentParams, + DidOpenTextDocumentParams, Hover, HoverContents, HoverParams, InitializeParams, MarkupContent, + MarkupKind, Position, PublishDiagnosticsParams, ServerCapabilities, TextDocumentSyncCapability, + TextDocumentSyncKind, Url, +}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; + +mod linter; +use linter::utils::span_to_range; +use linter::Linter; + +struct ServerState { + // Map of document URI to (version, content) + documents: HashMap, + linter: Linter, +} + +impl ServerState { + fn new() -> Self { + Self { + documents: HashMap::new(), + linter: Linter::new(), + } + } +} + +fn main() -> Result<()> { + // Initialize logging + let _ = simplelog::WriteLogger::init( + simplelog::LevelFilter::Info, + simplelog::Config::default(), + std::fs::File::create("/tmp/eldritch-lsp.log") + .unwrap_or_else(|_| std::fs::File::create("eldritch-lsp.log").unwrap()), + ); + + log::info!("Starting Eldritch LSP..."); + + let (connection, io_threads) = Connection::stdio(); + + // Run initialization handshake + let (id, params) = connection.initialize_start()?; + let _init_params: InitializeParams = serde_json::from_value(params)?; + + let capabilities = ServerCapabilities { + text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::FULL)), + completion_provider: Some(lsp_types::CompletionOptions { + resolve_provider: Some(false), + trigger_characters: Some(vec![".".to_string()]), + ..Default::default() + }), + hover_provider: Some(lsp_types::HoverProviderCapability::Simple(true)), + ..ServerCapabilities::default() + }; + + // Manually constructing JSON result to ensure correct structure + let result = serde_json::json!({ + "capabilities": capabilities, + "serverInfo": { + "name": "eldritch-lsp", + "version": "0.1.0" + } + }); + + connection.initialize_finish(id, result)?; + + let state = Arc::new(Mutex::new(ServerState::new())); + + log::info!("Eldritch LSP initialized."); + + for msg in &connection.receiver { + match msg { + Message::Request(req) => { + if connection.handle_shutdown(&req)? { + return Ok(()); + } + + let state = state.clone(); + let sender = connection.sender.clone(); + + match req.method.as_str() { + Completion::METHOD => { + let params: CompletionParams = serde_json::from_value(req.params).unwrap(); + let resp = handle_completion(state, params); + let result = serde_json::to_value(&resp).unwrap(); + sender + .send(Message::Response(Response { + id: req.id, + result: Some(result), + error: None, + })) + .unwrap(); + } + HoverRequest::METHOD => { + let params: HoverParams = serde_json::from_value(req.params).unwrap(); + let resp = handle_hover(state, params); + let result = serde_json::to_value(&resp).unwrap(); + sender + .send(Message::Response(Response { + id: req.id, + result: Some(result), + error: None, + })) + .unwrap(); + } + _ => { + // forward unknown requests or ignore + } + } + } + Message::Response(_) => {} + Message::Notification(not) => { + let state = state.clone(); + let sender = connection.sender.clone(); + + match not.method.as_str() { + DidOpenTextDocument::METHOD => { + let params: DidOpenTextDocumentParams = + serde_json::from_value(not.params).unwrap(); + handle_did_open(state, sender, params); + } + DidChangeTextDocument::METHOD => { + let params: DidChangeTextDocumentParams = + serde_json::from_value(not.params).unwrap(); + handle_did_change(state, sender, params); + } + DidCloseTextDocument::METHOD => { + let params: DidCloseTextDocumentParams = + serde_json::from_value(not.params).unwrap(); + let mut state = state.lock().unwrap(); + state.documents.remove(¶ms.text_document.uri); + } + _ => {} + } + } + } + } + + io_threads.join()?; + Ok(()) +} + +fn handle_did_open( + state: Arc>, + sender: Sender, + params: DidOpenTextDocumentParams, +) { + let mut s = state.lock().unwrap(); + s.documents.insert( + params.text_document.uri.clone(), + ( + params.text_document.version, + params.text_document.text.clone(), + ), + ); + + // Run diagnostics + let diagnostics = run_diagnostics(&s, ¶ms.text_document.text); + publish_diagnostics( + sender, + params.text_document.uri, + diagnostics, + Some(params.text_document.version), + ); +} + +fn handle_did_change( + state: Arc>, + sender: Sender, + params: DidChangeTextDocumentParams, +) { + let mut s = state.lock().unwrap(); + // We requested Full sync, so content_changes[0].text is the full text + if let Some(change) = params.content_changes.first() { + s.documents.insert( + params.text_document.uri.clone(), + (params.text_document.version, change.text.clone()), + ); + + let diagnostics = run_diagnostics(&s, &change.text); + publish_diagnostics( + sender, + params.text_document.uri, + diagnostics, + Some(params.text_document.version), + ); + } +} + +fn publish_diagnostics( + sender: Sender, + uri: Url, + diagnostics: Vec, + version: Option, +) { + let params = PublishDiagnosticsParams { + uri, + diagnostics, + version, + }; + let not = Notification { + method: PublishDiagnostics::METHOD.to_string(), + params: serde_json::to_value(¶ms).unwrap(), + }; + sender.send(Message::Notification(not)).unwrap(); +} + +fn run_diagnostics(state: &ServerState, text: &str) -> Vec { + let mut diagnostics = Vec::new(); + + // 1. Syntax Check via Parser + let mut lexer = Lexer::new(text.to_string()); + match lexer.scan_tokens() { + Ok(tokens) => { + let mut parser = Parser::new(tokens); + match parser.parse() { + Ok(stmts) => { + // 2. Run Linter on AST + diagnostics.extend(state.linter.check(&stmts, text)); + } + Err(e) => { + diagnostics.push(Diagnostic { + range: span_to_range(e.span, text), + severity: Some(DiagnosticSeverity::ERROR), + message: e.message, + ..Default::default() + }); + } + } + } + Err(e) => { + diagnostics.push(Diagnostic { + range: span_to_range(e.span, text), + severity: Some(DiagnosticSeverity::ERROR), + message: e.message, + ..Default::default() + }); + } + } + + diagnostics +} + +fn handle_completion( + state: Arc>, + params: CompletionParams, +) -> CompletionResponse { + let s = state.lock().unwrap(); + let uri = params.text_document_position.text_document.uri; + + if let Some((_, text)) = s.documents.get(&uri) { + let offset = position_to_offset(text, params.text_document_position.position); + + // Use facade interpreter with all libraries loaded (including fake agent for completion) + let interp = Interpreter::new().with_default_libs().with_fake_agent(); + + let (_start_idx, candidates) = interp.complete(text, offset); + + let items: Vec = candidates + .into_iter() + .map(|c| { + // Determine Kind + let kind = if let Some(val) = interp.lookup_variable(&c) { + match val { + Value::Foreign(_) => Some(CompletionItemKind::MODULE), // Libraries + Value::NativeFunction(_, _) + | Value::NativeFunctionWithKwargs(_, _) + | Value::Function(_) => Some(CompletionItemKind::FUNCTION), // Builtins + _ => Some(CompletionItemKind::VARIABLE), + } + } else { + // If resolving fails, it's likely a method or property from dot-completion + Some(CompletionItemKind::METHOD) + }; + + CompletionItem { + label: c, + kind, + ..Default::default() + } + }) + .collect(); + + return CompletionResponse::Array(items); + } + + CompletionResponse::Array(vec![]) +} + +fn handle_hover(state: Arc>, params: HoverParams) -> Option { + let s = state.lock().unwrap(); + let uri = params.text_document_position_params.text_document.uri; + + if let Some((_, text)) = s.documents.get(&uri) { + let offset = position_to_offset(text, params.text_document_position_params.position); + + // We need to resolve the symbol under cursor. + // We can reuse the `complete` logic logic partially, but we want the full token chain. + // e.g. "file.append" -> we want to resolve "file", then check "append" on it. + // Or simple variable lookup. + + let interp = Interpreter::new().with_default_libs().with_fake_agent(); + + let (start, end) = find_word_bounds(text, offset); + let _word = &text[start..end]; + + // For now, let's look backwards for dotted chain. + let full_expr = expand_dotted_chain(text, start, end); + + // Try to evaluate the parent object if there is a dot + if let Some((obj_name, method_name)) = full_expr.rsplit_once('.') { + // Resolve obj_name + if let Some(val) = interp.lookup_variable(obj_name) { + // Check if it's a library or built-in + if let Some(sig) = get_method_signature(&val, method_name) { + return Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: format_signature(&sig), + }), + range: None, + }); + } + } + } else { + // It's a global variable or function + if let Some(val) = interp.lookup_variable(&full_expr) { + // Should show type or doc if available + // Currently variables don't carry docs, but libraries (ForeignValue) do? + // Actually ForeignValue is an object. + // If it's a ForeignValue (library module), we can describe it. + let desc = match val { + Value::Foreign(f) => format!("Module: {}", f.type_name()), + Value::NativeFunction(name, _) | Value::NativeFunctionWithKwargs(name, _) => { + format!("Built-in function: {}", name) + } + Value::Function(f) => format!("Function: {}", f.name), + _ => format!("Variable: {}", full_expr), + }; + return Some(Hover { + contents: HoverContents::Markup(MarkupContent { + kind: MarkupKind::Markdown, + value: desc, + }), + range: None, + }); + } + } + } + None +} + +// Helpers + +fn find_word_bounds(text: &str, offset: usize) -> (usize, usize) { + if offset > text.len() { + return (0, 0); + } + + // Convert byte offset to char index for safe iteration + // Actually, iterating chars is easier. + + // Find "start" by scanning backwards from offset + // This requires char indices. + let char_indices: Vec<(usize, char)> = text.char_indices().collect(); + + // Find index in char_indices that corresponds to offset + // or the one immediately before it if offset is between chars (not likely if valid utf8 split, but cursor can be anywhere) + // Actually, LSP offset is typically UTF-16 code units. But here we have UTF-8 text. + // Assuming `offset` passed in `handle_hover` is byte offset calculated by `position_to_offset`. + + let mut current_idx = 0; + for (i, (byte_idx, _)) in char_indices.iter().enumerate() { + if *byte_idx >= offset { + current_idx = i; + break; + } + current_idx = i + 1; // if at end + } + if current_idx >= char_indices.len() { + current_idx = char_indices.len(); + } + + // Scan backwards + let mut start_char_idx = current_idx; + while start_char_idx > 0 { + let (_, c) = char_indices[start_char_idx - 1]; + if !c.is_alphanumeric() && c != '_' { + break; + } + start_char_idx -= 1; + } + + // Scan forwards + let mut end_char_idx = current_idx; + while end_char_idx < char_indices.len() { + let (_, c) = char_indices[end_char_idx]; + if !c.is_alphanumeric() && c != '_' { + break; + } + end_char_idx += 1; + } + + let start_byte = if start_char_idx < char_indices.len() { + char_indices[start_char_idx].0 + } else { + text.len() + }; + + let end_byte = if end_char_idx < char_indices.len() { + char_indices[end_char_idx].0 + } else { + text.len() + }; + + (start_byte, end_byte) +} + +fn expand_dotted_chain(text: &str, start_byte: usize, end_byte: usize) -> String { + let char_indices: Vec<(usize, char)> = text.char_indices().collect(); + + // Find char index for start_byte + let mut current_char_idx = 0; + for (i, (b, _)) in char_indices.iter().enumerate() { + if *b == start_byte { + current_char_idx = i; + break; + } + } + + let mut s = current_char_idx; + + // Look backwards + while s > 0 { + let (_, c) = char_indices[s - 1]; + if c == '.' { + s -= 1; + // Now verify identifier + let id_end = s; + while s > 0 { + let (_, c2) = char_indices[s - 1]; + if !c2.is_alphanumeric() && c2 != '_' { + break; + } + s -= 1; + } + if s == id_end { + // Dot without identifier + s += 1; + break; + } + } else { + break; + } + } + + let effective_start_byte = char_indices[s].0; + text[effective_start_byte..end_byte].to_string() +} + +fn get_method_signature(val: &Value, method: &str) -> Option { + match val { + Value::Foreign(f) => f.get_method_signature(method), + _ => eldritch_core::get_native_method_signature(val, method), + } +} + +fn format_signature(sig: &eldritch_core::MethodSignature) -> String { + let mut s = format!("`{}(", sig.name); + for (i, p) in sig.params.iter().enumerate() { + if i > 0 { + s.push_str(", "); + } + s.push_str(&p.name); + if let Some(t) = &p.type_name { + s.push_str(": "); + s.push_str(t); + } + } + s.push_str(")`"); + + if let Some(ret) = &sig.return_type { + s.push_str(" -> `"); + s.push_str(ret); + s.push('`'); + } + + if let Some(doc) = &sig.doc { + s.push_str("\n\n"); + s.push_str(doc); + } + + s +} + +fn position_to_offset(text: &str, position: Position) -> usize { + let mut offset = 0; + let target_line = position.line as usize; + let target_char = position.character as usize; + + for (i, line) in text.lines().enumerate() { + if i == target_line { + // Count characters to target_char + // LSP uses UTF-16 code units for character offset + // We need to walk the string and count UTF-16 units + let mut utf16_count = 0; + let mut byte_count = 0; + for c in line.chars() { + if utf16_count >= target_char { + break; + } + utf16_count += c.len_utf16(); + byte_count += c.len_utf8(); + } + offset += byte_count; + return offset; + } + // Add line length + newline chars + offset += line.len(); + + // Handle newline characters that `lines()` stripped + // We need to look at original text to see if it was \n or \r\n + // But iterating lines() loses that info. + // Better way: manual iteration over text bytes/chars looking for newlines. + } + + // Re-implementation using direct char iteration to be safe and accurate with newlines + let mut line = 0; + let mut utf16_col = 0; + let mut byte_offset = 0; + + let mut chars = text.chars().peekable(); + while let Some(c) = chars.next() { + if line == target_line && utf16_col == target_char { + return byte_offset; + } + + byte_offset += c.len_utf8(); + + if c == '\n' { + line += 1; + utf16_col = 0; + } else if c == '\r' { + // Check for \r\n + if let Some(&'\n') = chars.peek() { + // Next is \n, consume it in next iteration + // line increment happens on \n + } else { + // Just \r (classic mac or weirdness) + line += 1; + utf16_col = 0; + } + } else if line == target_line { + utf16_col += c.len_utf16(); + } + } + + byte_offset +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_completion_string_method() { + let code = r#" +def get_env(): + envs = sys.get_env() + for key, value in envs.items(): + print(f"{key}={value}") + "".s"#; + + // Offset of the end of the string + let offset = code.len(); + + // Use facade + let interp = Interpreter::new().with_default_libs(); + let (_start, candidates) = interp.complete(code, offset); + + // We expect string methods starting with 's' + assert!(candidates.contains(&"split".to_string())); + assert!(candidates.contains(&"strip".to_string())); + + // We expect NO globals like "set" or "sorted" + assert!(!candidates.contains(&"set".to_string())); + assert!(!candidates.contains(&"sorted".to_string())); + } + + #[test] + fn test_completion_libraries_and_literals() { + // Test library "agent" presence + let code1 = "ag"; + let interp = Interpreter::new().with_default_libs().with_fake_agent(); + let (_, candidates1) = interp.complete(code1, code1.len()); + assert!(candidates1.contains(&"agent".to_string())); + + // Test library method "agent.get_config" + let code2 = "agent."; + let (_, candidates2) = interp.complete(code2, code2.len()); + assert!(!candidates2.is_empty()); + + // Test literal list completion "[]." + let code3 = "[]."; + let (_, candidates3) = interp.complete(code3, code3.len()); + assert!(candidates3.contains(&"append".to_string())); + assert!(candidates3.contains(&"sort".to_string())); + + // Test literal dict completion "{}." + let code4 = "{}."; + let (_, candidates4) = interp.complete(code4, code4.len()); + assert!(candidates4.contains(&"keys".to_string())); + assert!(candidates4.contains(&"get".to_string())); + } + + #[test] + fn test_hover_list_append() { + let _code = "file.list"; + let _offset = 7; // end + + // We can manually call helper logic + let interp = Interpreter::new().with_default_libs(); + let val = interp + .lookup_variable("file") + .expect("file lib should be present"); + let sig = get_method_signature(&val, "list"); + + assert!(sig.is_some()); + let s = sig.unwrap(); + assert_eq!(s.name, "list"); + } +} diff --git a/implants/lib/eldritchv2/eldritch-macros/src/impls.rs b/implants/lib/eldritchv2/eldritch-macros/src/impls.rs index d21cac271..dfcbe8cf4 100644 --- a/implants/lib/eldritchv2/eldritch-macros/src/impls.rs +++ b/implants/lib/eldritchv2/eldritch-macros/src/impls.rs @@ -39,20 +39,44 @@ pub fn expand_eldritch_library( let mut method_dispatches = Vec::new(); let mut method_registrations = Vec::new(); + let mut method_signatures = Vec::new(); for item in &mut trait_def.items { if let TraitItem::Method(method) = item { // Check for eldritch_method attribute let mut is_eldritch = false; let mut rename = None; + let mut deprecated = None; let mut cfg_attrs = Vec::new(); for attr in &method.attrs { if attr.path.is_ident("eldritch_method") { is_eldritch = true; if let Ok(Meta::List(meta)) = attr.parse_meta() { - if let Some(NestedMeta::Lit(Lit::Str(lit))) = meta.nested.first() { - rename = Some(lit.value()); + for nested in meta.nested { + match nested { + NestedMeta::Lit(Lit::Str(lit)) => { + // Positional string is the rename + rename = Some(lit.value()); + } + NestedMeta::Meta(Meta::NameValue(nv)) => { + if nv.path.is_ident("deprecated") { + if let Lit::Bool(b) = nv.lit { + if b.value { + deprecated = Some("Deprecated".to_string()); + } + } else if let Lit::Str(s) = nv.lit { + deprecated = Some(s.value()); + } + } else if nv.path.is_ident("name") { + // Support named argument "name" for rename + if let Lit::Str(s) = nv.lit { + rename = Some(s.value()); + } + } + } + _ => {} + } } } } else if attr.path.is_ident("cfg") { @@ -64,6 +88,7 @@ pub fn expand_eldritch_library( let method_name = &method.sig.ident; let bind_name = rename.unwrap_or_else(|| method_name.to_string()); let (args_parsing, arg_names) = generate_args_parsing(&method.sig)?; + let signature_gen = generate_signature(&method.sig, &bind_name, deprecated)?; method_dispatches.push(quote! { #(#cfg_attrs)* @@ -78,6 +103,11 @@ pub fn expand_eldritch_library( #(#cfg_attrs)* names.push(alloc::string::String::from(#bind_name)); }); + + method_signatures.push(quote! { + #(#cfg_attrs)* + #bind_name => Some(#signature_gen), + }); } } } @@ -97,6 +127,15 @@ pub fn expand_eldritch_library( } }); + trait_def.items.push(parse_quote! { + fn _eldritch_get_method_signature(&self, name: &str) -> Option { + match name { + #(#method_signatures)* + _ => None, + } + } + }); + trait_def.items.push(parse_quote! { fn _eldritch_call_method( &self, @@ -149,6 +188,10 @@ pub fn expand_eldritch_library_impl( ::_eldritch_method_names(self) } + fn get_method_signature(&self, name: &str) -> Option { + ::_eldritch_get_method_signature(self, name) + } + fn call_method( &self, interp: &mut eldritch_core::Interpreter, @@ -175,11 +218,12 @@ fn is_option_type(ty: &Type) -> bool { fn is_interpreter_type(ty: &Type) -> bool { // Check if type is `Interpreter`, `&Interpreter`, or `&mut Interpreter` // Or fully qualified `eldritch_core::Interpreter` - if let Type::Reference(type_ref) = ty - && let Type::Path(type_path) = &*type_ref.elem - && let Some(segment) = type_path.path.segments.last() - { - return segment.ident == "Interpreter"; + if let Type::Reference(type_ref) = ty { + if let Type::Path(type_path) = &*type_ref.elem { + if let Some(segment) = type_path.path.segments.last() { + return segment.ident == "Interpreter"; + } + } } false } @@ -299,3 +343,50 @@ fn generate_args_parsing(sig: &Signature) -> Result<(TokenStream, TokenStream), Ok((quote! { #(#final_parsing)* }, quote! { #(#call_args),* })) } + +fn generate_signature(sig: &Signature, bind_name: &str, deprecated: Option) -> Result { + let mut params = Vec::new(); + + for input in &sig.inputs { + match input { + FnArg::Receiver(_) => continue, + FnArg::Typed(pat_type) => { + let ty = &pat_type.ty; + if is_interpreter_type(ty) { + continue; + } + + let pat = &pat_type.pat; + let arg_name_str = quote!(#pat).to_string(); + let is_optional = is_option_type(ty); + let type_name_str = quote!(#ty).to_string(); // Simple string representation for now + + params.push(quote! { + eldritch_core::ParameterSignature { + name: alloc::string::String::from(#arg_name_str), + type_name: Some(alloc::string::String::from(#type_name_str)), + is_optional: #is_optional, + is_variadic: false, + is_kwargs: false, + } + }); + } + } + } + + let deprecated_field = if let Some(reason) = deprecated { + quote! { Some(alloc::string::String::from(#reason)) } + } else { + quote! { None } + }; + + Ok(quote! { + eldritch_core::MethodSignature { + name: alloc::string::String::from(#bind_name), + params: alloc::vec![#(#params),*], + return_type: None, // TODO: Inspect return type if needed + doc: None, + deprecated: #deprecated_field, + } + }) +} diff --git a/implants/lib/eldritchv2/eldritchv2/src/lib.rs b/implants/lib/eldritchv2/eldritchv2/src/lib.rs index cbc2cf464..76578aada 100644 --- a/implants/lib/eldritchv2/eldritchv2/src/lib.rs +++ b/implants/lib/eldritchv2/eldritchv2/src/lib.rs @@ -210,6 +210,11 @@ impl Interpreter { pub fn complete(&self, code: &str, cursor: usize) -> (usize, Vec) { self.inner.complete(code, cursor) } + + pub fn lookup_variable(&self, name: &str) -> Option { + // Use lookup_variable but discard error (return None) + self.inner.lookup_variable(name, Span::new(0, 0, 0)).ok() + } } #[cfg(all(test, feature = "fake_bindings"))] diff --git a/tavern/internal/auth/context_edge_cases_test.go b/tavern/internal/auth/context_edge_cases_test.go deleted file mode 100644 index cb1b4c1ee..000000000 --- a/tavern/internal/auth/context_edge_cases_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package auth_test - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "realm.pub/tavern/internal/auth" - "realm.pub/tavern/internal/ent/enttest" -) - -func TestContextFromTokens_Invalid(t *testing.T) { - // Setup Dependencies - var ( - driverName = "sqlite3" - dataSourceName = "file:ent?mode=memory&cache=shared&_fk=1" - ) - graph := enttest.Open(t, driverName, dataSourceName, enttest.WithOptions()) - defer graph.Close() - - // Test ContextFromSessionToken with invalid token - t.Run("ContextFromSessionToken_NotFound", func(t *testing.T) { - // Pass a nil context first to check if it panics? No, Background is fine. - ctx, err := auth.ContextFromSessionToken(context.Background(), graph, "invalid-token") - require.Error(t, err) - // Usually ent returns "ent: user not found" - assert.Contains(t, err.Error(), "user not found") - // The returned context should be nil if error? - // Looking at context.go: if err != nil { return nil, err } - assert.Nil(t, ctx) - }) - - // Test ContextFromAccessToken with invalid token - t.Run("ContextFromAccessToken_NotFound", func(t *testing.T) { - ctx, err := auth.ContextFromAccessToken(context.Background(), graph, "invalid-token") - require.Error(t, err) - assert.Contains(t, err.Error(), "user not found") - assert.Nil(t, ctx) - }) -} - -func TestContextHelpers_EdgeCases(t *testing.T) { - ctx := context.Background() - - t.Run("EmptyContext", func(t *testing.T) { - assert.Nil(t, auth.IdentityFromContext(ctx)) - assert.Nil(t, auth.UserFromContext(ctx)) - assert.False(t, auth.IsAuthenticatedContext(ctx)) - assert.False(t, auth.IsActivatedContext(ctx)) - assert.False(t, auth.IsAdminContext(ctx)) - }) -} diff --git a/tavern/internal/redirectors/grpc/grpc_test.go b/tavern/internal/redirectors/grpc/grpc_test.go index 1838f586f..5cc768189 100644 --- a/tavern/internal/redirectors/grpc/grpc_test.go +++ b/tavern/internal/redirectors/grpc/grpc_test.go @@ -82,30 +82,6 @@ func setupRawUpstreamServer(t *testing.T) (string, func()) { } } -// dialWithRetry attempts to dial the given address, retrying on failure. -// This is necessary because the redirector is started in a goroutine and might not be ready immediately. -func dialWithRetry(t *testing.T, target string, opts ...grpc.DialOption) *grpc.ClientConn { - t.Helper() - - var conn *grpc.ClientConn - var err error - - deadline := time.Now().Add(5 * time.Second) - for time.Now().Before(deadline) { - ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) - // Let's use WithBlock to ensure it's connected, with a short timeout context. - optsWithBlock := append([]grpc.DialOption{grpc.WithBlock()}, opts...) - conn, err = grpc.DialContext(ctx, target, optsWithBlock...) - cancel() // Call cancel explicitly to avoid defer pile-up in loop - if err == nil { - return conn - } - time.Sleep(100 * time.Millisecond) - } - require.NoError(t, err, "failed to dial redirector") - return nil -} - func TestRedirector_FullDuplexCall(t *testing.T) { // 1. Setup the raw upstream test server. upstreamAddr, upstreamCleanup := setupRawUpstreamServer(t) @@ -127,7 +103,8 @@ func TestRedirector_FullDuplexCall(t *testing.T) { }() // 3. Connect a client to the redirector, also using the raw codec. - conn := dialWithRetry(t, addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions(grpc.CallContentSubtype("raw"))) + conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions(grpc.CallContentSubtype("raw"))) + require.NoError(t, err) defer conn.Close() // 4. Perform a bidirectional streaming call through the redirector. @@ -179,10 +156,7 @@ func TestRedirector_ContextCancellation(t *testing.T) { }() // Wait a moment for the server to start listening. - // Since we are testing cancellation, we can just poll until we can connect, or sleep. - // Sleeping is okay here as we aren't asserting on immediate start, but ensuring shutdown works. - // A robust way is to try to dial. - dialWithRetry(t, addr, grpc.WithTransportCredentials(insecure.NewCredentials())).Close() + time.Sleep(100 * time.Millisecond) // Cancel the context, which should trigger GracefulStop. cancel() @@ -217,7 +191,8 @@ func TestRedirector_UpstreamFailure(t *testing.T) { }() // 2. Connect a client to the redirector. - conn := dialWithRetry(t, addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions(grpc.CallContentSubtype("raw"))) + conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(insecure.NewCredentials()), grpc.WithDefaultCallOptions(grpc.CallContentSubtype("raw"))) + require.NoError(t, err) defer conn.Close() // 3. Attempt a streaming call. diff --git a/tavern/tomes/cat/main.eldritch b/tavern/tomes/cat/main.eldritch index 253724859..6ab755391 100644 --- a/tavern/tomes/cat/main.eldritch +++ b/tavern/tomes/cat/main.eldritch @@ -4,7 +4,6 @@ def cat(path): print(res) else: eprint(f"Error: Invalid Path '{path}'") - return time.sleep(5) diff --git a/vscode/.gitignore b/vscode/.gitignore index 6f11d3ddd..acd6edcf3 100644 --- a/vscode/.gitignore +++ b/vscode/.gitignore @@ -1,4 +1,5 @@ out node_modules .vscode-test -*.vsix \ No newline at end of file +*.vsix +bin/ diff --git a/vscode/README.md b/vscode/README.md deleted file mode 100644 index 196705c04..000000000 --- a/vscode/README.md +++ /dev/null @@ -1,10 +0,0 @@ -# README - -## Quick Start - -```sh -cd vscode/ -npm install -g @vscode/vsce -vsce package -code --install-extension eldritch-0.0.3.vsix -``` diff --git a/vscode/client/src/extension.ts b/vscode/client/src/extension.ts index bee79fdfb..a675a1812 100644 --- a/vscode/client/src/extension.ts +++ b/vscode/client/src/extension.ts @@ -1,32 +1,71 @@ -import { ExtensionContext } from 'vscode'; +import { ExtensionContext, window } from 'vscode'; import { LanguageClient, LanguageClientOptions, ServerOptions, } from 'vscode-languageclient'; +import * as path from 'path'; +import * as fs from 'fs'; let client: LanguageClient; export function activate(context: ExtensionContext) { - // Otherwise to spawn the server - let serverOptions: ServerOptions = { command: "eldritch-lang", args: ["--lsp"] }; + let serverPath = getServerPath(context); + + if (!serverPath) { + window.showErrorMessage("Eldritch LSP binary not found. Please install it or build it using `cargo build -p eldritch-lsp --release`."); + return; + } + + let serverOptions: ServerOptions = { + command: serverPath, + args: [] + }; // Options to control the language client let clientOptions: LanguageClientOptions = { - // Register the server for Starlark documents + // Register the server for Eldritch documents documentSelector: [{ scheme: 'file', language: 'eldritch' }], + outputChannelName: 'Eldritch LSP', + // Reveal output channel if there is an error + revealOutputChannelOn: 4 // RevealOutputChannelOn.Error }; // Create the language client and start the client. client = new LanguageClient( 'Eldritch', - 'Eldritch language server', + 'Eldritch Language Server', serverOptions, clientOptions ); - // Start the client. This will also launch the server client.start(); + + client.onReady().then(() => { + // Server started successfully + console.log("Eldritch LSP started."); + }, (error) => { + window.showErrorMessage(`Eldritch LSP failed to initialize: ${error}`); + console.error("Eldritch LSP initialization error:", error); + }); +} + +function getServerPath(context: ExtensionContext): string | undefined { + // 1. Check bundled binary in bin/ + let bundledPath = context.asAbsolutePath(path.join('bin', 'eldritch-lsp')); + if (process.platform === 'win32') { + bundledPath += '.exe'; + } + + if (fs.existsSync(bundledPath)) { + return bundledPath; + } + + // 2. Check PATH (naive check by returning command name) + // Actually, if we return "eldritch-lsp", vscode-languageclient will look in PATH. + // But we want to be descriptive if it fails. + // Let's assume if bundled is missing, we try PATH. + return "eldritch-lsp"; } export function deactivate(): Thenable | undefined { @@ -34,4 +73,4 @@ export function deactivate(): Thenable | undefined { return undefined; } return client.stop(); -} \ No newline at end of file +} diff --git a/vscode/eldritch-lang/.gitignore b/vscode/eldritch-lang/.gitignore deleted file mode 100644 index 1de565933..000000000 --- a/vscode/eldritch-lang/.gitignore +++ /dev/null @@ -1 +0,0 @@ -target \ No newline at end of file diff --git a/vscode/eldritch-lang/Cargo.lock b/vscode/eldritch-lang/Cargo.lock deleted file mode 100644 index c3288b95d..000000000 --- a/vscode/eldritch-lang/Cargo.lock +++ /dev/null @@ -1,1673 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 3 - -[[package]] -name = "Inflector" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" -dependencies = [ - "lazy_static", - "regex", -] - -[[package]] -name = "ahash" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" -dependencies = [ - "getrandom", - "once_cell", - "version_check", -] - -[[package]] -name = "aho-corasick" -version = "0.7.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" -dependencies = [ - "memchr", -] - -[[package]] -name = "annotate-snippets" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3b9d411ecbaf79885c6df4d75fff75858d5995ff25385657a28af47e82f9c36" -dependencies = [ - "unicode-width", - "yansi-term", -] - -[[package]] -name = "ansi_term" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" -dependencies = [ - "winapi", -] - -[[package]] -name = "anyhow" -version = "1.0.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "159bb86af3a200e19a068f4224eae4c8bb2d0fa054c7e5d1cacd5cef95e684cd" - -[[package]] -name = "argfile" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f3a889586e8b0753fd320a319e713b8ef6a2693259604db10eca22bc92e8810" -dependencies = [ - "os_str_bytes", -] - -[[package]] -name = "ascii-canvas" -version = "3.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8824ecca2e851cec16968d54a01dd372ef8f95b244fb84b84e70128be347c3c6" -dependencies = [ - "term", -] - -[[package]] -name = "atty" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" -dependencies = [ - "hermit-abi", - "libc", - "winapi", -] - -[[package]] -name = "autocfg" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" - -[[package]] -name = "beef" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "474a626a67200bd107d44179bb3d4fc61891172d11696609264589be6a0e6a43" - -[[package]] -name = "bit-set" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e11e16035ea35e4e5997b393eacbf6f63983188f7a2ad25bfb13465f5ad59de" -dependencies = [ - "bit-vec", -] - -[[package]] -name = "bit-vec" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" - -[[package]] -name = "bitflags" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" - -[[package]] -name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", - "serde", -] - -[[package]] -name = "bumpalo" -version = "3.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a45a46ab1f2412e53d3a0ade76ffad2025804294569aae387231a0cd6e0899" - -[[package]] -name = "cast" -version = "0.2.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c24dab4283a142afa2fdca129b80ad2c6284e073930f964c3a1293c225ee39a" -dependencies = [ - "rustc_version", -] - -[[package]] -name = "cc" -version = "1.0.73" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "clap" -version = "2.34.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" -dependencies = [ - "ansi_term", - "atty", - "bitflags", - "strsim 0.8.0", - "textwrap 0.11.0", - "unicode-width", - "vec_map", -] - -[[package]] -name = "convert_case" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" - -[[package]] -name = "criterion" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1604dafd25fba2fe2d5895a9da139f8dc9b319a5fe5354ca137cbbce4e178d10" -dependencies = [ - "atty", - "cast", - "clap", - "criterion-plot", - "csv", - "itertools 0.10.3", - "lazy_static", - "num-traits", - "oorandom", - "plotters", - "rayon", - "regex", - "serde", - "serde_cbor", - "serde_derive", - "serde_json", - "tinytemplate", - "walkdir", -] - -[[package]] -name = "criterion-plot" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d00996de9f2f7559f7f4dc286073197f83e92256a59ed395f9aac01fe717da57" -dependencies = [ - "cast", - "itertools 0.10.3", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" -dependencies = [ - "cfg-if", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-deque" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6455c0ca19f0d2fbf751b908d5c55c1f5cbc65e03c4225427254b46890bdde1e" -dependencies = [ - "cfg-if", - "crossbeam-epoch", - "crossbeam-utils", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.9.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c00d6d2ea26e8b151d99093005cb442fb9a37aeaca582a03ec70946f49ab5ed9" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "lazy_static", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" -dependencies = [ - "cfg-if", - "lazy_static", -] - -[[package]] -name = "crunchy" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" - -[[package]] -name = "csv" -version = "1.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" -dependencies = [ - "bstr", - "csv-core", - "itoa 0.4.8", - "ryu", - "serde", -] - -[[package]] -name = "csv-core" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" -dependencies = [ - "memchr", -] - -[[package]] -name = "debugserver-types" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bf6834a70ed14e8e4e41882df27190bea150f1f6ecf461f1033f8739cd8af4a" -dependencies = [ - "schemafy", - "serde", - "serde_json", -] - -[[package]] -name = "derivative" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "derive_more" -version = "0.99.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" -dependencies = [ - "convert_case", - "proc-macro2", - "quote", - "rustc_version", - "syn", -] - -[[package]] -name = "diff" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e25ea47919b1560c4e3b7fe0aaab9becf5b84a10325ddf7db0f0ba5e1026499" - -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - -[[package]] -name = "either" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" - -[[package]] -name = "eldritch" -version = "0.1.0" -dependencies = [ - "anyhow", - "derive_more", - "starlark", -] - -[[package]] -name = "eldritch-lang" -version = "0.0.2" -dependencies = [ - "annotate-snippets", - "anyhow", - "argfile", - "bumpalo", - "criterion", - "debugserver-types", - "derivative", - "derive_more", - "either", - "eldritch", - "fnv", - "gazebo 0.5.0", - "gazebo_lint", - "hashbrown", - "indenter", - "indexmap", - "indoc", - "itertools 0.9.0", - "lalrpop", - "lalrpop-util", - "logos", - "lsp-server", - "lsp-types", - "maplit", - "memoffset", - "once_cell", - "paste", - "rand", - "regex", - "rustyline", - "serde", - "serde_json", - "starlark", - "starlark_derive", - "static_assertions", - "strsim 0.10.0", - "structopt", - "textwrap 0.14.2", - "thiserror", - "walkdir", -] - -[[package]] -name = "ena" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7402b94a93c24e742487327a7cd839dc9d36fec9de9fb25b09f2dae459f36c3" -dependencies = [ - "log", -] - -[[package]] -name = "fixedbitset" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" -dependencies = [ - "matches", - "percent-encoding", -] - -[[package]] -name = "fs2" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "gazebo" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04aaea21e785604f0e979c9f8c1363c0da479a2ccfbc451febc990bbf5eb34f" -dependencies = [ - "gazebo_derive 0.1.1", -] - -[[package]] -name = "gazebo" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f84359d27ce1edf87a8316fb50f2c2bb436a4cba14e4116c0a1ae3c4b461382" -dependencies = [ - "gazebo_derive 0.4.1", -] - -[[package]] -name = "gazebo" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "300fdf63cb78f2927bbb598ab995fe3c5f4ee738cc7eec402ee2fca25056d06a" -dependencies = [ - "gazebo_derive 0.5.0", -] - -[[package]] -name = "gazebo_derive" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d67a8bf744290be7799e50aa9f9d50fa1b693fd7bc37daf7bca57d20988f2c63" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "gazebo_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5acbc546dfc8ade58e0acc5e6c7b918ed7956747a897fdca5b19f26c7fffbab1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "gazebo_derive" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2038279ae06a55781b37487d7657c4ebcf2358509c10dec4c953f8be6ba432" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "gazebo_lint" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "488536a9ca76c345d86d5d2168af815259cfb0681ac0c31c4c4cceab3723158d" -dependencies = [ - "smallvec", -] - -[[package]] -name = "getrandom" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d39cd93900197114fa1fcb7ae84ca742095eed9442088988ae74fa744e930e77" -dependencies = [ - "cfg-if", - "libc", - "wasi", -] - -[[package]] -name = "half" -version = "1.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eabb4a44450da02c90444cf74558da904edde8fb4e9035a9a6a4e15445af0bd7" - -[[package]] -name = "hashbrown" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" -dependencies = [ - "ahash", -] - -[[package]] -name = "heck" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" -dependencies = [ - "unicode-segmentation", -] - -[[package]] -name = "hermit-abi" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] - -[[package]] -name = "idna" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" -dependencies = [ - "matches", - "unicode-bidi", - "unicode-normalization", -] - -[[package]] -name = "indenter" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" - -[[package]] -name = "indexmap" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282a6247722caba404c065016bbfa522806e51714c34f5dfc3e4a3a46fcb4223" -dependencies = [ - "autocfg", - "hashbrown", - "serde", -] - -[[package]] -name = "indoc" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e7906a9fababaeacb774f72410e497a1d18de916322e33797bb2cd29baa23c9e" -dependencies = [ - "unindent", -] - -[[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "itertools" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" -dependencies = [ - "either", -] - -[[package]] -name = "itertools" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "0.4.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" - -[[package]] -name = "itoa" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" - -[[package]] -name = "js-sys" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a38fc24e30fd564ce974c02bf1d337caddff65be6cc4735a1f7eab22a7440f04" -dependencies = [ - "wasm-bindgen", -] - -[[package]] -name = "lalrpop" -version = "0.19.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852b75a095da6b69da8c5557731c3afd06525d4f655a4fc1c799e2ec8bc4dce4" -dependencies = [ - "ascii-canvas", - "atty", - "bit-set", - "diff", - "ena", - "itertools 0.10.3", - "lalrpop-util", - "petgraph", - "pico-args", - "regex", - "regex-syntax", - "string_cache", - "term", - "tiny-keccak", - "unicode-xid", -] - -[[package]] -name = "lalrpop-util" -version = "0.19.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6d265705249fe209280676d8f68887859fa42e1d34f342fc05bd47726a5e188" -dependencies = [ - "regex", -] - -[[package]] -name = "lazy_static" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" - -[[package]] -name = "libc" -version = "0.2.119" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bf2e165bb3457c8e098ea76f3e3bc9db55f87aa90d52d0e6be741470916aaa4" - -[[package]] -name = "lock_api" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" -dependencies = [ - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "logos" -version = "0.11.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b91c49573597a5d6c094f9031617bb1fed15c0db68c81e6546d313414ce107e4" -dependencies = [ - "logos-derive", -] - -[[package]] -name = "logos-derive" -version = "0.11.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "797b1f8a0571b331c1b47e7db245af3dc634838da7a92b3bef4e30376ae1c347" -dependencies = [ - "beef", - "fnv", - "proc-macro2", - "quote", - "regex-syntax", - "syn", - "utf8-ranges", -] - -[[package]] -name = "lsp-server" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c351c75989da23b355226dc188dc2b52538a7f4f218d70fd7393c6b62b110444" -dependencies = [ - "crossbeam-channel", - "log", - "serde", - "serde_json", -] - -[[package]] -name = "lsp-types" -version = "0.89.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "852e0dedfd52cc32325598b2631e0eba31b7b708959676a9f837042f276b09a2" -dependencies = [ - "bitflags", - "serde", - "serde_json", - "serde_repr", - "url", -] - -[[package]] -name = "maplit" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" - -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - -[[package]] -name = "memchr" -version = "2.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" - -[[package]] -name = "memoffset" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aa361d4faea93603064a027415f07bd8e1d5c88c9fbf68bf56a285428fd79ce" -dependencies = [ - "autocfg", -] - -[[package]] -name = "new_debug_unreachable" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" - -[[package]] -name = "nix" -version = "0.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2ccba0cfe4fdf15982d1674c69b1fd80bad427d293849982668dfe454bd61f2" -dependencies = [ - "bitflags", - "cc", - "cfg-if", - "libc", -] - -[[package]] -name = "num-traits" -version = "0.2.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" -dependencies = [ - "autocfg", -] - -[[package]] -name = "num_cpus" -version = "1.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" -dependencies = [ - "hermit-abi", - "libc", -] - -[[package]] -name = "once_cell" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" - -[[package]] -name = "oorandom" -version = "11.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" - -[[package]] -name = "os_str_bytes" -version = "6.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e22443d1643a904602595ba1cd8f7d896afe56d26712531c5ff73a15b2fbf64" -dependencies = [ - "memchr", -] - -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", -] - -[[package]] -name = "paste" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0744126afe1a6dd7f394cb50a716dbe086cb06e255e53d8d0185d82828358fb5" - -[[package]] -name = "percent-encoding" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" - -[[package]] -name = "petgraph" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "phf_shared" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pico-args" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db8bcd96cb740d03149cbad5518db9fd87126a10ab519c011893b1754134c468" - -[[package]] -name = "plotters" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a3fd9ec30b9749ce28cd91f255d569591cdf937fe280c312143e3c4bad6f2a" -dependencies = [ - "num-traits", - "plotters-backend", - "plotters-svg", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "plotters-backend" -version = "0.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d88417318da0eaf0fdcdb51a0ee6c3bed624333bff8f946733049380be67ac1c" - -[[package]] -name = "plotters-svg" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521fa9638fa597e1dc53e9412a4f9cefb01187ee1f7413076f9e6749e2885ba9" -dependencies = [ - "plotters-backend", -] - -[[package]] -name = "ppv-lite86" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" - -[[package]] -name = "precomputed-hash" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" - -[[package]] -name = "proc-macro-error" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" -dependencies = [ - "proc-macro-error-attr", - "proc-macro2", - "quote", - "syn", - "version_check", -] - -[[package]] -name = "proc-macro-error-attr" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" -dependencies = [ - "proc-macro2", - "quote", - "version_check", -] - -[[package]] -name = "proc-macro2" -version = "1.0.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029" -dependencies = [ - "unicode-xid", -] - -[[package]] -name = "quote" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha", - "rand_core", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core", -] - -[[package]] -name = "rand_core" -version = "0.6.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" -dependencies = [ - "getrandom", -] - -[[package]] -name = "rayon" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06aca804d41dbc8ba42dfd964f0d01334eceb64314b9ecf7c5fad5188a06d90" -dependencies = [ - "autocfg", - "crossbeam-deque", - "either", - "rayon-core", -] - -[[package]] -name = "rayon-core" -version = "1.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d78120e2c850279833f1dd3582f730c4ab53ed95aeaaaa862a2a5c71b1656d8e" -dependencies = [ - "crossbeam-channel", - "crossbeam-deque", - "crossbeam-utils", - "lazy_static", - "num_cpus", -] - -[[package]] -name = "redox_syscall" -version = "0.2.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" -dependencies = [ - "bitflags", -] - -[[package]] -name = "redox_users" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" -dependencies = [ - "getrandom", - "redox_syscall", -] - -[[package]] -name = "regex" -version = "1.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d07a8629359eb56f1e2fb1652bb04212c072a87ba68546a04065d525673ac461" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" - -[[package]] -name = "regex-syntax" -version = "0.6.25" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b" - -[[package]] -name = "rustc_version" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" -dependencies = [ - "semver", -] - -[[package]] -name = "rustversion" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" - -[[package]] -name = "rustyline" -version = "7.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8227301bfc717136f0ecbd3d064ba8199e44497a0bdd46bb01ede4387cfd2cec" -dependencies = [ - "bitflags", - "cfg-if", - "dirs-next", - "fs2", - "libc", - "log", - "memchr", - "nix", - "scopeguard", - "unicode-segmentation", - "unicode-width", - "utf8parse", - "winapi", -] - -[[package]] -name = "ryu" -version = "1.0.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schemafy" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8aea5ba40287dae331f2c48b64dbc8138541f5e97ee8793caa7948c1f31d86d5" -dependencies = [ - "Inflector", - "schemafy_core", - "schemafy_lib", - "serde", - "serde_derive", - "serde_json", - "serde_repr", - "syn", -] - -[[package]] -name = "schemafy_core" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41781ae092f4fd52c9287efb74456aea0d3b90032d2ecad272bd14dbbcb0511b" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "schemafy_lib" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e953db32579999ca98c451d80801b6f6a7ecba6127196c5387ec0774c528befa" -dependencies = [ - "Inflector", - "proc-macro2", - "quote", - "schemafy_core", - "serde", - "serde_derive", - "serde_json", - "syn", -] - -[[package]] -name = "scopeguard" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "semver" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a3381e03edd24287172047536f20cabde766e2cd3e65e6b00fb3af51c4f38d" - -[[package]] -name = "serde" -version = "1.0.136" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_cbor" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bef2ebfde456fb76bbcf9f59315333decc4fda0b2b44b420243c11e0f5ec1f5" -dependencies = [ - "half", - "serde", -] - -[[package]] -name = "serde_derive" -version = "1.0.136" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d9fa5c3b304765ce1fd9c4c8a3de2c8db365a5b91be52f186efc675681d95" -dependencies = [ - "itoa 1.0.1", - "ryu", - "serde", -] - -[[package]] -name = "serde_repr" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98d0516900518c29efa217c298fa1f4e6c6ffc85ae29fd7f4ee48f176e1a9ed5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "siphasher" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a86232ab60fa71287d7f2ddae4a7073f6b7aac33631c3015abb556f08c6d0a3e" - -[[package]] -name = "smallvec" -version = "1.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" - -[[package]] -name = "smawk" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" - -[[package]] -name = "starlark" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80312f7d70f03cde4505d7b2700da812056d8d3eebbf1689f76d16e16e8d1edb" -dependencies = [ - "annotate-snippets", - "anyhow", - "bumpalo", - "debugserver-types", - "derivative", - "derive_more", - "either", - "fnv", - "gazebo 0.4.4", - "hashbrown", - "indenter", - "indexmap", - "indoc", - "itertools 0.9.0", - "lalrpop", - "lalrpop-util", - "logos", - "lsp-server", - "lsp-types", - "maplit", - "memoffset", - "once_cell", - "paste", - "regex", - "rustyline", - "serde", - "serde_json", - "starlark_derive", - "static_assertions", - "strsim 0.10.0", - "structopt", - "textwrap 0.14.2", - "thiserror", - "walkdir", -] - -[[package]] -name = "starlark_derive" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc89ecc26b5695f64d0bffdf05d5020de7e06e355d9f51c939a1e4fdd5f5230" -dependencies = [ - "gazebo 0.2.2", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "string_cache" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33994d0838dc2d152d17a62adf608a869b5e846b65b389af7f3dbc1de45c5b26" -dependencies = [ - "lazy_static", - "new_debug_unreachable", - "parking_lot", - "phf_shared", - "precomputed-hash", -] - -[[package]] -name = "strsim" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" - -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - -[[package]] -name = "structopt" -version = "0.3.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" -dependencies = [ - "clap", - "lazy_static", - "structopt-derive", -] - -[[package]] -name = "structopt-derive" -version = "0.4.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" -dependencies = [ - "heck", - "proc-macro-error", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "syn" -version = "1.0.86" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - -[[package]] -name = "textwrap" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" -dependencies = [ - "unicode-width", -] - -[[package]] -name = "textwrap" -version = "0.14.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0066c8d12af8b5acd21e00547c3797fde4e8677254a7ee429176ccebbe93dd80" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - -[[package]] -name = "thiserror" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417" -dependencies = [ - "thiserror-impl", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.30" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tiny-keccak" -version = "2.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" -dependencies = [ - "crunchy", -] - -[[package]] -name = "tinytemplate" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" -dependencies = [ - "serde", - "serde_json", -] - -[[package]] -name = "tinyvec" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" - -[[package]] -name = "unicode-bidi" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" - -[[package]] -name = "unicode-linebreak" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" -dependencies = [ - "regex", -] - -[[package]] -name = "unicode-normalization" -version = "0.1.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "unicode-segmentation" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" - -[[package]] -name = "unicode-width" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" - -[[package]] -name = "unicode-xid" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ccb82d61f80a663efe1f787a51b16b5a51e3314d6ac365b08639f52387b33f3" - -[[package]] -name = "unindent" -version = "0.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "514672a55d7380da379785a4d70ca8386c8883ff7eaae877be4d2081cebe73d8" - -[[package]] -name = "url" -version = "2.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" -dependencies = [ - "form_urlencoded", - "idna", - "matches", - "percent-encoding", - "serde", -] - -[[package]] -name = "utf8-ranges" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ae116fef2b7fea257ed6440d3cfcff7f190865f170cdad00bb6465bf18ecba" - -[[package]] -name = "utf8parse" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" - -[[package]] -name = "vec_map" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" - -[[package]] -name = "version_check" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" - -[[package]] -name = "walkdir" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" -dependencies = [ - "same-file", - "winapi", - "winapi-util", -] - -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - -[[package]] -name = "wasm-bindgen" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25f1af7423d8588a3d840681122e72e6a24ddbcb3f0ec385cac0d12d24256c06" -dependencies = [ - "cfg-if", - "wasm-bindgen-macro", -] - -[[package]] -name = "wasm-bindgen-backend" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b21c0df030f5a177f3cba22e9bc4322695ec43e7257d865302900290bcdedca" -dependencies = [ - "bumpalo", - "lazy_static", - "log", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f4203d69e40a52ee523b2529a773d5ffc1dc0071801c87b3d270b471b80ed01" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa8a30d46208db204854cadbb5d4baf5fcf8071ba5bf48190c3e59937962ebc" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-backend", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.79" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d958d035c4438e28c70e4321a2911302f10135ce78a9c7834c0cab4123d06a2" - -[[package]] -name = "web-sys" -version = "0.3.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c060b319f29dd25724f09a2ba1418f142f539b2be99fbf4d2d5a8f7330afb8eb" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" -dependencies = [ - "winapi", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "yansi-term" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5c30ade05e61656247b2e334a031dfd0cc466fadef865bdcdea8d537951bf1" -dependencies = [ - "winapi", -] diff --git a/vscode/eldritch-lang/Cargo.toml b/vscode/eldritch-lang/Cargo.toml deleted file mode 100644 index f8ca8b976..000000000 --- a/vscode/eldritch-lang/Cargo.toml +++ /dev/null @@ -1,71 +0,0 @@ -[package] -name = "eldritch-lang" -edition = "2021" -version = "0.0.2" -license = "GNU GPL v3.0" -description = "Eldritch Language Server." -documentation = "https://docs.realm.pub/eldritch" -repository = "https://github.com/kcarretto/eldritch" -authors = [ - "Kyle Carretto ", - "Nick O'Brian " -] -keywords = ["realm", "red team", "redteam", "implant", "dsl"] -categories = ["parser-implementations", "development-tools"] - -[build-dependencies] -lalrpop = "0.19" - -[dependencies] -eldritch = { path = "../../cmd/implants/eldritch" } -annotate-snippets = { version = "0.9.0", features = ["color"] } -anyhow = "1.0.51" -derivative = "2.1.1" -derive_more = "0.99" -lalrpop-util = "0.19.1" -indexmap = { version = "1.6", features = ["serde-1"] } -indenter = { version = "0.3.3", features = ["std"] } -indoc = "1.0" -itertools = "0.9" -once_cell = "1.3" -bumpalo = "3.8" -paste = "1.0" -either = "1.6.1" -fnv = "1.0.7" -static_assertions = "1.1.0" -memoffset = "0.6.4" -thiserror = "1.0.9" -starlark = "0.6.0" -starlark_derive = "0.6.0" -# @oss-disable: gazebo = { path = "../../gazebo/gazebo", features = ["str_pattern_extensions"] } -gazebo = { version = "0.5.0", features = ["str_pattern_extensions"] } # @oss-enable -# @oss-disable: gazebo_lint = { path = "../../gazebo_lint/gazebo_lint", optional = true } -gazebo_lint = { version = "0.1", optional = true } # @oss-enable -structopt = "0.3.0" -walkdir = "2.3" -serde = { version = "1.0", features = ["derive"] } -logos = "0.11.4" -serde_json = "1.0" -rustyline = "7.0.0" -maplit = "1.0.2" -lsp-server = "0.5" -lsp-types = "0.89.0" -debugserver-types = "0.5.0" -hashbrown = { version = "0.11.2", features = ["raw"] } -textwrap = "0.14.2" -regex = "1.5.4" -strsim = "0.10.0" -argfile = "0.1.0" - -[dev-dependencies] -criterion = "0.3" -rand = { version = "0.8.4", features = ["small_rng"] } - -[features] -# @oss-disable: default = ["gazebo_linter"] -gazebo_linter = ["gazebo_lint"] - -[[bin]] -name = "eldritch-lang" -path = "src/main.rs" - diff --git a/vscode/eldritch-lang/rust-toolchain b/vscode/eldritch-lang/rust-toolchain deleted file mode 100644 index 4ca98912b..000000000 --- a/vscode/eldritch-lang/rust-toolchain +++ /dev/null @@ -1,2 +0,0 @@ -[toolchain] -channel = "nightly-2025-01-31" diff --git a/vscode/eldritch-lang/src/eval.rs b/vscode/eldritch-lang/src/eval.rs deleted file mode 100644 index 5240077b0..000000000 --- a/vscode/eldritch-lang/src/eval.rs +++ /dev/null @@ -1,182 +0,0 @@ -use std::{ - fs, iter, - path::{Path, PathBuf}, -}; - -use gazebo::prelude::*; -use itertools::Either; -use starlark::{ - starlark_module, - environment::{FrozenModule, Globals, GlobalsBuilder, Module}, - eval::Evaluator, - syntax::{AstModule, Dialect}, -}; -use eldritch::{ - file::FileLibrary, - process::ProcessLibrary, - sys::SysLibrary, -}; - -use crate::types::Message; - -#[derive(Debug)] -pub struct Context { - pub check: bool, - pub info: bool, - pub run: bool, - pub prelude: Vec, - pub module: Option, -} - -impl Context { - pub fn new( - check: bool, - info: bool, - run: bool, - prelude: &[PathBuf], - module: bool, - ) -> anyhow::Result { - let globals = globals(); - - let prelude = prelude.try_map(|x| { - let env = Module::new(); - - let mut eval = Evaluator::new(&env); - let module = AstModule::parse_file(x, &dialect())?; - eval.eval_module(module, &globals)?; - env.freeze() - })?; - - let module = if module { - Some(Self::new_module(&prelude)) - } else { - None - }; - - Ok(Self { - check, - info, - run, - prelude, - module, - }) - } - - fn new_module(prelude: &[FrozenModule]) -> Module { - let module = Module::new(); - for p in prelude { - module.import_public_symbols(p); - } - module - } - - fn go(&self, file: &str, ast: AstModule) -> impl Iterator { - let mut warnings = Either::Left(iter::empty()); - let mut errors = Either::Left(iter::empty()); - if self.info { - self.info(&ast); - } - if self.check { - warnings = Either::Right(self.check(&ast)); - } - if self.run { - errors = Either::Right(self.run(file, ast)); - } - warnings.chain(errors) - } - - // Convert an anyhow over iterator of Message, into an iterator of Message - fn err( - file: &str, - result: anyhow::Result>, - ) -> impl Iterator { - match result { - Err(e) => Either::Left(iter::once(Message::from_anyhow(file, e))), - Ok(res) => Either::Right(res), - } - } - - pub fn expression(&self, content: String) -> impl Iterator { - let file = "expression"; - Self::err( - file, - AstModule::parse(file, content, &dialect()).map(|module| self.go(file, module)), - ) - } - - pub fn file(&self, file: &Path) -> impl Iterator { - let filename = &file.to_string_lossy(); - Self::err( - filename, - fs::read_to_string(file) - .map(|content| self.file_with_contents(filename, content)) - .map_err(|e| e.into()), - ) - } - - pub fn file_with_contents( - &self, - filename: &str, - content: String, - ) -> impl Iterator { - Self::err( - filename, - AstModule::parse(filename, content, &dialect()).map(|module| self.go(filename, module)), - ) - } - - fn run(&self, file: &str, ast: AstModule) -> impl Iterator { - let new_module; - let module = match self.module.as_ref() { - Some(module) => module, - None => { - new_module = Self::new_module(&self.prelude); - &new_module - } - }; - let mut eval = Evaluator::new(module); - eval.enable_terminal_breakpoint_console(); - let globals = globals(); - Self::err(file, eval.eval_module(ast, &globals).map(|_| iter::empty())) - } - - fn info(&self, module: &AstModule) { - let exports = module.exported_symbols(); - println!("Exports {} symbol(s)", exports.len()); - for (loc, name) in exports { - println!("* {} {}", loc, name) - } - } - - fn check(&self, module: &AstModule) -> impl Iterator { - let mut globals = Vec::new(); - for x in &self.prelude { - globals.extend(x.names()); - } - let globals = if self.prelude.is_empty() { - None - } else { - Some(globals.as_slice()) - }; - - module.lint(globals).into_iter().map(Message::from_lint) - } -} - -pub fn globals() -> Globals { - #[starlark_module] - fn stdlib(builder: &mut GlobalsBuilder) { - const file: FileLibrary = FileLibrary(); - const process: ProcessLibrary = ProcessLibrary(); - const sys: SysLibrary = SysLibrary(); - } - - GlobalsBuilder::new().with(stdlib).build() -} - -pub fn dialect() -> Dialect { - Dialect { - enable_f_strings: true, - ..Dialect::Extended - } -} diff --git a/vscode/eldritch-lang/src/lsp.rs b/vscode/eldritch-lang/src/lsp.rs deleted file mode 100644 index dea95e731..000000000 --- a/vscode/eldritch-lang/src/lsp.rs +++ /dev/null @@ -1,193 +0,0 @@ -//! Based on the reference lsp-server example at . -use lsp_server::{Connection, Message, Notification}; -use lsp_types::{ - notification::{ - DidChangeTextDocument, DidCloseTextDocument, DidOpenTextDocument, LogMessage, - PublishDiagnostics, - }, - Diagnostic, DiagnosticSeverity, DidChangeTextDocumentParams, DidCloseTextDocumentParams, - DidOpenTextDocumentParams, InitializeParams, LogMessageParams, MessageType, NumberOrString, - Position, PublishDiagnosticsParams, Range, ServerCapabilities, TextDocumentSyncCapability, - TextDocumentSyncKind, Url, -}; -use serde::de::DeserializeOwned; - -use crate::{ - eval::Context, - types::{Message as StarlarkMessage, Severity}, -}; - -const VERSION: &str = env!("CARGO_PKG_VERSION"); - -struct Backend { - connection: Connection, - starlark: Context, -} - -fn to_severity(x: Severity) -> DiagnosticSeverity { - match x { - Severity::Error => DiagnosticSeverity::Error, - Severity::Warning => DiagnosticSeverity::Warning, - Severity::Advice => DiagnosticSeverity::Hint, - Severity::Disabled => DiagnosticSeverity::Information, - } -} - -fn to_diagnostic(x: StarlarkMessage) -> Diagnostic { - let range = match x.span { - Some(s) => Range::new( - Position::new(s.begin_line as u32, s.begin_column as u32), - Position::new(s.end_line as u32, s.end_column as u32), - ), - _ => Range::default(), - }; - Diagnostic::new( - range, - Some(to_severity(x.severity)), - Some(NumberOrString::String(x.name)), - None, - x.description, - None, - None, - ) -} - -/// The logic implementations of stuff -impl Backend { - fn server_capabilities() -> ServerCapabilities { - ServerCapabilities { - text_document_sync: Some(TextDocumentSyncCapability::Kind(TextDocumentSyncKind::Full)), - ..ServerCapabilities::default() - } - } - - fn validate(&self, uri: Url, version: Option, text: String) { - self.log_message(MessageType::Info, format!("Validating: {}", uri.path()).as_str()); - let diags = self - .starlark - .file_with_contents(&uri.to_string(), text) - .map(to_diagnostic) - .collect(); - self.publish_diagnostics(uri, diags, version) - } - - fn did_open(&self, params: DidOpenTextDocumentParams) { - self.validate( - params.text_document.uri, - Some(params.text_document.version as i64), - params.text_document.text, - ) - } - - fn did_change(&self, params: DidChangeTextDocumentParams) { - // We asked for Sync full, so can just grab all the text from params - let change = params.content_changes.into_iter().next().unwrap(); - self.validate( - params.text_document.uri, - Some(params.text_document.version as i64), - change.text, - ); - } - - fn did_close(&self, params: DidCloseTextDocumentParams) { - self.publish_diagnostics(params.text_document.uri, Vec::new(), None) - } -} - -/// The library style pieces -impl Backend { - fn send_notification(&self, x: Notification) { - self.connection - .sender - .send(Message::Notification(x)) - .unwrap() - } - - fn log_message(&self, typ: MessageType, message: &str) { - self.send_notification(new_notification::(LogMessageParams { - typ, - message: message.to_owned(), - })) - } - - fn publish_diagnostics(&self, uri: Url, diags: Vec, version: Option) { - self.send_notification(new_notification::( - PublishDiagnosticsParams::new(uri, diags, version.map(|i| i as i32)), - )); - } - - fn main_loop(&self, _params: InitializeParams) -> anyhow::Result<()> { - self.log_message(MessageType::Info, "Eldritch Language Server Initialised"); - for msg in &self.connection.receiver { - match msg { - Message::Request(req) => { - if self.connection.handle_shutdown(&req)? { - return Ok(()); - } - // Currently don't handle any other requests - } - Message::Notification(x) => { - if let Some(params) = as_notification::(&x) { - self.did_open(params) - } else if let Some(params) = as_notification::(&x) { - self.did_change(params) - } else if let Some(params) = as_notification::(&x) { - self.did_close(params) - } - } - Message::Response(_) => { - // Don't expect any of these - } - } - } - Ok(()) - } -} - -pub fn server(starlark: Context) -> anyhow::Result<()> { - // Note that we must have our logging only write out to stderr. - eprintln!("Starting Eldritch Language Server v{v}", v=VERSION); - - let (connection, io_threads) = Connection::stdio(); - // Run the server and wait for the two threads to end (typically by trigger LSP Exit event). - let server_capabilities = serde_json::to_value(&Backend::server_capabilities()).unwrap(); - let initialization_params = connection.initialize(server_capabilities)?; - let initialization_params = serde_json::from_value(initialization_params).unwrap(); - Backend { - connection, - starlark, - } - .main_loop(initialization_params)?; - io_threads.join()?; - - eprintln!("Stopping Eldritch Language Server v{v}", v=VERSION); - Ok(()) -} - -fn as_notification(x: &Notification) -> Option -where - T: lsp_types::notification::Notification, - T::Params: DeserializeOwned, -{ - if x.method == T::METHOD { - let params = serde_json::from_value(x.params.clone()).unwrap_or_else(|err| { - panic!( - "Invalid notification\nMethod: {}\n error: {}", - x.method, err - ) - }); - Some(params) - } else { - None - } -} - -fn new_notification(params: T::Params) -> Notification -where - T: lsp_types::notification::Notification, -{ - Notification { - method: T::METHOD.to_owned(), - params: serde_json::to_value(¶ms).unwrap(), - } -} \ No newline at end of file diff --git a/vscode/eldritch-lang/src/main.rs b/vscode/eldritch-lang/src/main.rs deleted file mode 100644 index f73999617..000000000 --- a/vscode/eldritch-lang/src/main.rs +++ /dev/null @@ -1,233 +0,0 @@ -// Features we use -#![feature(box_syntax)] -// -// Plugins -#![cfg_attr(feature = "custom_linter", feature(plugin))] -#![cfg_attr(feature = "custom_linter", allow(deprecated))] // :( -#![cfg_attr(feature = "custom_linter", plugin(gazebo_lint))] -// Disagree these are good hints -#![allow(clippy::type_complexity)] - -use std::{ffi::OsStr, fmt, fmt::Display, fs, path::PathBuf, sync::Arc}; - -use anyhow::anyhow; -use eval::Context; -use gazebo::prelude::*; -use itertools::Either; -use starlark::read_line::ReadLine; -use structopt::{clap::AppSettings, StructOpt}; -use walkdir::WalkDir; - -use crate::types::{LintMessage, Message, Severity}; - -mod eval; -mod lsp; -mod types; - -#[derive(Debug, StructOpt)] -#[structopt( - name = "eldritch-lang", - about = "Evaluate Eldritch tomes", - global_settings(&[AppSettings::ColoredHelp]), -)] -pub struct Args { - #[structopt( - long = "interactive", - long = "repl", - short = "i", - help = "Start an interactive REPL." - )] - interactive: bool, - - #[structopt(long = "lsp", help = "Start an LSP server.")] - lsp: bool, - - #[structopt(long = "check", help = "Run checks and lints.")] - check: bool, - - #[structopt(long = "info", help = "Show information about the code.")] - info: bool, - - #[structopt(long = "json", help = "Show output as JSON lines.")] - json: bool, - - #[structopt( - long = "repeat", - help = "Number of times to repeat the execution", - default_value = "1" - )] - repeat: usize, - - #[structopt( - long = "extension", - help = "File extension when searching directories." - )] - extension: Option, - - #[structopt(long = "prelude", help = "Files to load in advance.")] - prelude: Vec, - - #[structopt( - long = "expression", - short = "e", - name = "EXPRESSION", - help = "Expressions to evaluate." - )] - evaluate: Vec, - - #[structopt(name = "FILE", help = "Files to evaluate.")] - // String instead of PathBuf so we can expand @file things - files: Vec, -} - -// We'd really like clap to deal with args-files, but it doesn't yet -// Waiting on: https://github.com/clap-rs/clap/issues/1693. -// This is a minimal version to make basic @file options work. -fn expand_args(args: Vec) -> anyhow::Result> { - let mut res = Vec::with_capacity(args.len()); - for x in args { - match x.strip_prefix('@') { - None => res.push(PathBuf::from(x)), - Some(x) => { - let src = fs::read_to_string(x)?; - for x in src.lines() { - res.push(PathBuf::from(x)); - } - } - } - } - Ok(res) -} - -// Treat directories as things to recursively walk for . files, -// and everything else as normal files. -fn expand_dirs(extension: &str, xs: Vec) -> impl Iterator { - let extension = Arc::new(extension.to_owned()); - xs.into_iter().flat_map(move |x| { - // Have to keep cloning extension so we keep ownership - let extension = extension.dupe(); - if x.is_dir() { - Either::Left( - WalkDir::new(x) - .into_iter() - .filter_map(|e| e.ok()) - .filter(move |e| e.path().extension() == Some(OsStr::new(extension.as_str()))) - .map(|e| e.into_path()), - ) - } else { - Either::Right(box vec![x].into_iter()) - } - }) -} - -#[derive(Default)] -struct Stats { - file: usize, - error: usize, - warning: usize, - advice: usize, - disabled: usize, -} - -impl Display for Stats { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(&format!( - "{} files, {} errors, {} warnings, {} advices, {} disabled", - self.file, self.error, self.warning, self.advice, self.disabled - )) - } -} - -impl Stats { - fn increment_file(&mut self) { - self.file += 1; - } - - fn increment(&mut self, x: Severity) { - match x { - Severity::Error => self.error += 1, - Severity::Warning => self.warning += 1, - Severity::Advice => self.advice += 1, - Severity::Disabled => self.disabled += 1, - } - } -} - -fn drain(xs: impl Iterator, json: bool, stats: &mut Stats) { - for x in xs { - stats.increment(x.severity); - if json { - println!("{}", serde_json::to_string(&LintMessage::new(x)).unwrap()); - } else if let Some(error) = x.full_error_with_span { - let mut error = error.to_owned(); - if !error.is_empty() && !error.ends_with('\n') { - error.push('\n'); - } - print!("{}", error); - } else { - println!("{}", x); - } - } -} - -fn interactive(ctx: &Context) -> anyhow::Result<()> { - let mut rl = ReadLine::new(); - loop { - match rl.read_line("$> ")? { - Some(line) => { - let mut stats = Stats::default(); - drain(ctx.expression(line), false, &mut stats); - } - // User pressed EOF - disconnected terminal, or similar - None => return Ok(()), - } - } -} - -fn main() -> anyhow::Result<()> { - let args = Args::from_args(); - let ext = args - .extension - .as_ref() - .map_or("tome", |x| x.as_str()) - .trim_start_match('.'); - let mut ctx = Context::new( - args.check, - args.info, - !args.check && !args.info, - &expand_dirs(ext, args.prelude).collect::>(), - args.interactive, - )?; - - let mut stats = Stats::default(); - for _ in 0..args.repeat { - for e in args.evaluate.clone() { - stats.increment_file(); - drain(ctx.expression(e), args.json, &mut stats); - } - - for file in expand_dirs(ext, expand_args(args.files.clone())?) { - stats.increment_file(); - drain(ctx.file(&file), args.json, &mut stats); - } - } - - if args.interactive { - interactive(&ctx)?; - } - - if args.lsp { - ctx.check = true; - ctx.info = false; - ctx.run = false; - lsp::server(ctx)?; - } - - if !args.json { - println!("{}", stats); - if stats.error > 0 { - return Err(anyhow!("Failed with {} errors", stats.error)); - } - } - Ok(()) -} \ No newline at end of file diff --git a/vscode/eldritch-lang/src/types.rs b/vscode/eldritch-lang/src/types.rs deleted file mode 100644 index 67b49f3ea..000000000 --- a/vscode/eldritch-lang/src/types.rs +++ /dev/null @@ -1,135 +0,0 @@ - use std::fmt::{self, Display}; - - use gazebo::prelude::*; - use serde::Serialize; - use starlark::{ - codemap::ResolvedSpan, - errors::{Diagnostic, Lint}, - }; - - /// A standardised set of severities. - #[derive(Debug, Serialize, Dupe, Clone, Copy)] - #[serde(rename_all = "lowercase")] - pub enum Severity { - Error, - Warning, - // Not all severities are used right now - #[allow(dead_code)] - Advice, - Disabled, - } - - impl Display for Severity { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str(match self { - Severity::Error => "Error", - Severity::Warning => "Warning", - Severity::Advice => "Advice", - Severity::Disabled => "Disabled", - }) - } - } - - #[derive(Debug, Clone)] - pub struct Message { - pub path: String, - pub span: Option, - pub severity: Severity, - pub name: String, - pub description: String, - pub full_error_with_span: Option, - /// The text referred to by span - pub original: Option, - } - - impl Display for Message { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{}: {}:", self.severity, self.path)?; - if let Some(span) = self.span { - write!(f, "{}", span)?; - } - write!(f, " {}", self.description) - } - } - - impl Message { - pub fn from_anyhow(file: &str, x: anyhow::Error) -> Self { - match x.downcast_ref::() { - Some( - d @ Diagnostic { - message, - span: Some(span), - .. - }, - ) => { - let original = span.file.source_span(span.span).to_owned(); - let resolved_span = span.resolve_span(); - Self { - path: span.file.filename().to_owned(), - span: Some(resolved_span), - severity: Severity::Error, - name: "error".to_owned(), - description: format!("{:#}", message), - full_error_with_span: Some(d.to_string()), - original: Some(original), - } - } - _ => Self { - path: file.to_owned(), - span: None, - severity: Severity::Error, - name: "error".to_owned(), - description: format!("{:#}", x), - full_error_with_span: None, - original: None, - }, - } - } - - pub fn from_lint(x: Lint) -> Self { - Self { - path: x.location.file.filename().to_owned(), - span: Some(x.location.resolve_span()), - severity: if x.serious { - Severity::Warning - } else { - // Start with all non-serious errors disabled, and ramp up from there - Severity::Disabled - }, - name: x.short_name, - description: x.problem, - full_error_with_span: None, - original: Some(x.original), - } - } - } - - /// A JSON-deriving type that gives a stable interface to downstream types. - /// Do NOT change this type, change Message instead. - #[derive(Debug, Clone, Serialize)] - pub struct LintMessage { - path: String, - line: Option, - char: Option, - code: String, - severity: Severity, - name: String, - description: Option, - #[serde(skip_serializing_if = "Option::is_none")] - original: Option, - } - - impl LintMessage { - pub fn new(x: Message) -> Self { - Self { - path: x.path, - line: x.span.map(|x| x.begin_line + 1), - char: x.span.map(|x| x.begin_column + 1), - code: "STARLARK".to_owned(), - severity: x.severity, - name: x.name, - description: Some(x.description), - original: x.original, - } - } - } \ No newline at end of file diff --git a/vscode/package-lock.json b/vscode/package-lock.json index dd58e16dd..c2b8c5d9e 100644 --- a/vscode/package-lock.json +++ b/vscode/package-lock.json @@ -1,12 +1,12 @@ { "name": "eldritch", - "version": "0.0.1", + "version": "0.0.3", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "eldritch", - "version": "0.0.1", + "version": "0.0.3", "hasInstallScript": true, "license": "GNU GPL v3.0", "dependencies": { @@ -326,6 +326,7 @@ "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", "dev": true, + "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -1006,6 +1007,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, + "peer": true, "dependencies": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -3000,6 +3002,7 @@ "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", "dev": true, + "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -3534,7 +3537,8 @@ "version": "7.4.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true + "dev": true, + "peer": true }, "acorn-jsx": { "version": "5.3.2", @@ -4019,6 +4023,7 @@ "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", "dev": true, + "peer": true, "requires": { "@babel/code-frame": "7.12.11", "@eslint/eslintrc": "^0.4.3", @@ -5489,7 +5494,8 @@ "version": "4.5.5", "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz", "integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==", - "dev": true + "dev": true, + "peer": true }, "uc.micro": { "version": "1.0.6", diff --git a/vscode/package.json b/vscode/package.json index 3d6c2e9cf..4ddfa5fda 100644 --- a/vscode/package.json +++ b/vscode/package.json @@ -1,13 +1,13 @@ { "name": "eldritch", - "description": "An Eldritch language server", + "description": "Eldritch language support for Visual Studio Code", "author": "realm", "license": "GNU GPL v3.0", - "version": "0.0.3", + "version": "0.3.0", "icon": "images/eldritch.png", "repository": { "type": "git", - "url": "https://realm.pub" + "url": "https://github.com/spellshift/realm" }, "publisher": "realm", "categories": [], @@ -23,7 +23,8 @@ "activationEvents": [ "workspaceContains:*.tome", "workspaceContains:*/*.tome", - "workspaceContains:*/*/*.tome" + "workspaceContains:*/*/*.tome", + "onLanguage:eldritch" ], "main": "./client/out/extension", "contributes": { @@ -31,15 +32,16 @@ { "id": "eldritch", "icon": { - "light": "client/src/images/eldritch.png", - "dark": "client/src/images/eldritch.png" + "light": "./images/eldritch.png", + "dark": "./images/eldritch.png" }, "aliases": [ "Eldritch", "eldritch" ], "extensions": [ - ".tome" + ".tome", + ".eldritch" ], "filenames": [], "configuration": "./syntaxes/eldritch.configuration.json" @@ -62,7 +64,8 @@ "vscode:prepublish": "npm run compile", "compile": "tsc -b", "watch": "tsc -b -w", - "postinstall": "cd client && npm install && cd .." + "build-lsp": "cd ../implants && cargo build --release -p eldritch-lsp && mkdir -p ../vscode/bin && cp target/release/eldritch-lsp ../vscode/bin/ && cd ../vscode", + "postinstall": "cd client && npm install && cd .. && node install.js" }, "devDependencies": { "@types/mocha": "^9.0.0", diff --git a/vscode/syntaxes/eldritch.tmLanguage.json b/vscode/syntaxes/eldritch.tmLanguage.json index 0530c770d..aeb53f1e2 100644 --- a/vscode/syntaxes/eldritch.tmLanguage.json +++ b/vscode/syntaxes/eldritch.tmLanguage.json @@ -1,927 +1,29 @@ { "name": "Eldritch", "scopeName": "source.eldritch", - "fileTypes": ["eldritch"], "patterns": [ { - "include": "#statement" + "name": "invalid.illegal.keyword.eldritch", + "match": "\\b(?=|<=|<|>)(?# 4)", - "captures": { - "1": { - "name": "keyword.operator.logical.eldritch" - }, - "2": { - "name": "keyword.control.flow.eldritch" - }, - "3": { - "name": "keyword.operator.arithmetic.eldritch" - }, - "4": { - "name": "keyword.operator.comparison.eldritch" - } - } - }, - "literal": { - "patterns": [ - { - "name": "constant.language.eldritch", - "match": "\\b(True|False|None)\\b" - }, - { - "include": "#number" - } - ] - }, - "number": { - "patterns": [ - { - "include": "#number-decimal" - }, - { - "include": "#number-hexadecimal" - }, - { - "include": "#number-octal" - }, - { - "name": "invalid.illegal.name.eldritch", - "match": "\\b[0-9]+\\w+" - } - ] - }, - "number-decimal": { - "name": "constant.numeric.decimal.eldritch", - "match": "(?