├── Makefile ├── client ├── testFixture │ ├── completion.txt │ └── diagnostics.txt ├── .gitignore ├── tsconfig.json ├── src │ ├── test │ │ ├── runTest.ts │ │ ├── index.ts │ │ ├── completion.test.ts │ │ ├── helper.ts │ │ └── diagnostics.test.ts │ └── extension.ts └── package.json ├── .gitignore ├── docs ├── processes │ ├── backend-source_to_ast.md │ └── initialize.md ├── environment.md ├── php-parser.md ├── backend.md ├── index.md ├── phpls-rs.md ├── img │ ├── structurizr-backend-components.svg │ ├── structurizr-php-parser-components.svg │ ├── structurizr-environment-components.svg │ ├── structurizr-systemlandscape.svg │ ├── structurizr-backend-process-initialize.svg │ ├── structurizr-backend-process-source_to_ast.svg │ └── structurizr-containers.svg └── dsl │ └── index.dsl ├── src ├── formatter │ ├── classes │ │ └── mod.rs │ ├── loops.rs │ ├── expressions.rs │ └── v2.rs ├── parser │ └── ast │ │ ├── mod.rs │ │ ├── exception_handling.rs │ │ ├── loops.rs │ │ ├── arrays.rs │ │ ├── types.rs │ │ ├── keywords.rs │ │ ├── variables.rs │ │ ├── conditionals.rs │ │ ├── functions.rs │ │ ├── namespaces.rs │ │ ├── attributes.rs │ │ └── expressions.rs ├── backend │ ├── did_close.rs │ ├── did_change.rs │ ├── document_symbol.rs │ ├── goto_definition.rs │ ├── did_change_watched_files.rs │ ├── did_open.rs │ ├── symbol.rs │ ├── formatting.rs │ ├── hover.rs │ ├── document_highlight.rs │ ├── goto_implementation.rs │ └── completion.rs ├── environment │ ├── traverser.rs │ ├── scope.rs │ ├── visitor │ │ └── mod.rs │ ├── fs.rs │ ├── mod.rs │ └── import.rs └── main.rs ├── .github └── workflows │ └── ci.yml ├── sc.sh ├── Cargo.toml ├── .vscode ├── tasks.json └── launch.json ├── LICENSE ├── README.md └── package.json /Makefile: -------------------------------------------------------------------------------- 1 | coverage: 2 | ./sc.sh -------------------------------------------------------------------------------- /client/testFixture/completion.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /client/testFixture/diagnostics.txt: -------------------------------------------------------------------------------- 1 | ANY browsers, ANY OS. -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | out 2 | node_modules 3 | client/server 4 | .vscode-test -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /fixtures 3 | /tests 4 | *.vsix 5 | profile.sh 6 | rust-perf.svg 7 | perf.data* -------------------------------------------------------------------------------- /docs/processes/backend-source_to_ast.md: -------------------------------------------------------------------------------- 1 | # initialize 2 | 3 | The initialize function initializes the workspace. 4 | 5 | 6 | ![Workspace initialization](../img/structurizr-backend-process-initialize.svg) -------------------------------------------------------------------------------- /docs/processes/initialize.md: -------------------------------------------------------------------------------- 1 | # source_to_ast 2 | 3 | The source_to_ast function is takes care of turning a source string into an AST. 4 | 5 | 6 | ![Source to AST](../img/structurizr-backend-process-source_to_ast.svg) -------------------------------------------------------------------------------- /docs/environment.md: -------------------------------------------------------------------------------- 1 | # Environment 2 | 3 | The environment is aware of the available symbols and their references to each other. Actually, I am not so 4 | sure if "environment" is a good name. It might change in the future. 5 | 6 | ![Components of the Environment](img/structurizr-environment-components.svg) -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "out", 6 | "rootDir": "src", 7 | "sourceMap": true 8 | }, 9 | "include": [ 10 | "src" 11 | ], 12 | "exclude": [ 13 | "node_modules", 14 | ".vscode-test" 15 | ] 16 | } -------------------------------------------------------------------------------- /docs/php-parser.md: -------------------------------------------------------------------------------- 1 | # PHP Parser 2 | 3 | The PHP Parser basically consists of two components: A scanner, that turns a source string into 4 | individual tokens, and a parser, that takes that token stream and turns into an AST consisting of nodes. 5 | 6 | ![Components of the PHP Parser](img/structurizr-php-parser-components.svg) -------------------------------------------------------------------------------- /src/formatter/classes/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ 2 | node::{ClassStatement, Node}, 3 | token::Token, 4 | }; 5 | 6 | use super::v2::Span; 7 | 8 | pub(crate) fn class_stmt_to_spans( 9 | spans: &mut Vec, 10 | tokens: &[Token], 11 | stmt: &ClassStatement, 12 | lvl: u8, 13 | ) { 14 | } 15 | -------------------------------------------------------------------------------- /src/parser/ast/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod arrays; 2 | pub mod attributes; 3 | pub mod calls; 4 | pub mod classes; 5 | pub mod comments; 6 | pub mod conditionals; 7 | pub mod exception_handling; 8 | pub mod expressions; 9 | pub mod functions; 10 | pub mod keywords; 11 | pub mod loops; 12 | pub mod namespaces; 13 | pub mod types; 14 | pub mod variables; 15 | -------------------------------------------------------------------------------- /src/backend/did_close.rs: -------------------------------------------------------------------------------- 1 | use super::BackendState; 2 | use crate::environment::fs as EnvFs; 3 | use lsp_types::DidCloseTextDocumentParams; 4 | 5 | pub(crate) fn did_close(state: &mut BackendState, params: DidCloseTextDocumentParams) { 6 | let p = EnvFs::normalize_path(¶ms.text_document.uri.to_file_path().unwrap()); 7 | state.latest_version_of_file.remove(&p); 8 | state.opened_files.remove(&p); 9 | state.symbol_references.remove(&p); 10 | } 11 | -------------------------------------------------------------------------------- /docs/backend.md: -------------------------------------------------------------------------------- 1 | # Backend 2 | 3 | The backend is the interface between the language server client (running in the editor of the developer) 4 | and the actual meat of phpls-rs. It implements the Backend interface of tower-lsp. 5 | 6 | ![Components of the backend](img/structurizr-backend-components.svg) 7 | 8 | ## Processes 9 | 10 | Relevant processes: 11 | 12 | * [Initialize the workspace](processes/backend-initialize.md) 13 | * [Source to AST](processes/backend-source_to_ast.md) -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | name: Clippy check 3 | jobs: 4 | clippy_check: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - uses: actions-rs/toolchain@v1 9 | with: 10 | toolchain: nightly 11 | components: clippy 12 | override: true 13 | - uses: actions-rs/clippy-check@v1 14 | with: 15 | token: ${{ secrets.GITHUB_TOKEN }} 16 | args: --all-features 17 | -------------------------------------------------------------------------------- /sc.sh: -------------------------------------------------------------------------------- 1 | rm -rf ./target *.prof* 2 | 3 | # Export the flags needed to instrument the program to collect code coverage. 4 | export RUSTFLAGS="-Zinstrument-coverage" 5 | 6 | # Build the program 7 | cargo build 8 | 9 | # Run the program (you can replace this with `cargo test` if you want to collect code coverage for your tests). 10 | cargo test 11 | 12 | # Generate a HTML report in the coverage/ directory. 13 | grcov . --binary-path ./target/debug/ -s . -t html --branch --ignore-not-existing -o ./target/debug/coverage/ 14 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Architecture 2 | 3 | This document describes the architecture of phpls-rs. It serves as a quick first entry point into how 4 | this code base is structured. 5 | The documentation is organized in a way that lets you drill down from a very high-level view until you reach almost 6 | the code level. 7 | 8 | ## System landscape 9 | 10 | The following diagram shows where phpls-rs fits into the development workflow. 11 | 12 | ![System landscape](img/structurizr-systemlandscape.svg) 13 | 14 | To learn more about what phpls-rs is made of, have a look at its [components](phpls-rs.md). -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "phpls-rs" 3 | version = "0.1.0" 4 | authors = ["sawmurai "] 5 | edition = "2018" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | snafu = "0.6.6" 11 | spmc = "0.3.0" 12 | tower-lsp = { version = "0.15.1" } 13 | lsp-types= { version = "0.92" } 14 | tokio = { version = "1.6", features = ["full"] } 15 | indextree = "4.0" 16 | clap = "2.23.3" 17 | ignore = "0.4" 18 | crossbeam-channel = "0.5.0" 19 | walkdir = "2" 20 | 21 | [profile.release] 22 | panic = "abort" -------------------------------------------------------------------------------- /src/backend/did_change.rs: -------------------------------------------------------------------------------- 1 | use super::{Backend, BackendState}; 2 | use crate::environment::fs as EnvFs; 3 | use lsp_types::DidChangeTextDocumentParams; 4 | 5 | pub(crate) fn did_change(state: &mut BackendState, params: &DidChangeTextDocumentParams) { 6 | let uri = params.text_document.uri.clone(); 7 | let file_path = uri.to_file_path().unwrap(); 8 | let path = EnvFs::normalize_path(&file_path); 9 | 10 | if let Some(changes) = params.content_changes.first() { 11 | state 12 | .latest_version_of_file 13 | .insert(path, changes.text.clone()); 14 | 15 | Backend::refresh_file(state, uri, &changes.text); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/backend/document_symbol.rs: -------------------------------------------------------------------------------- 1 | use super::BackendState; 2 | use crate::environment::fs as EnvFs; 3 | use lsp_types::{DocumentSymbolParams, DocumentSymbolResponse}; 4 | use tower_lsp::jsonrpc::Result; 5 | 6 | pub(crate) fn document_symbol( 7 | state: &BackendState, 8 | params: DocumentSymbolParams, 9 | ) -> Result> { 10 | let file_path = EnvFs::normalize_path(¶ms.text_document.uri.to_file_path().unwrap()); 11 | 12 | let node_id = if let Some(node_id) = state.files.get(&file_path) { 13 | node_id 14 | } else { 15 | return Ok(None); 16 | }; 17 | 18 | Ok(Some(DocumentSymbolResponse::Nested( 19 | node_id 20 | .children(&state.arena) 21 | .filter_map(|s| state.arena[s].get().to_doc_sym(&state.arena, &s)) 22 | .collect(), 23 | ))) 24 | } 25 | -------------------------------------------------------------------------------- /docs/phpls-rs.md: -------------------------------------------------------------------------------- 1 | # System phpls-rs 2 | 3 | phpls-rs consists of a number of components that interact with each other. 4 | 5 | ![](img/structurizr-containers.svg) 6 | 7 | ## Backend 8 | 9 | The backend acts as the main interface between the language server client and phpls-rs. 10 | 11 | [More information](backend.md) 12 | 13 | ## PHP Parser 14 | 15 | The PHP Parser is used to turn a PHP source file into a token stream and an Abstract Syntax Tree. 16 | 17 | [More information](php-parser.md) 18 | 19 | ## Formatter 20 | 21 | The formatter turns the parsed AST and token stream back into a source string, while fixing the formatting. 22 | 23 | [More information](formatter.md) 24 | 25 | ## Environment 26 | 27 | The environment module is aware of the existing symbols (classes, traits, interfaces, global functions and consts, etc.) and knows how to resolve references to each of them. 28 | 29 | [More information](environment.md) -------------------------------------------------------------------------------- /src/backend/goto_definition.rs: -------------------------------------------------------------------------------- 1 | use super::BackendState; 2 | use crate::environment::{self, fs as EnvFs, get_range, in_range}; 3 | use lsp_types::{GotoDefinitionParams, GotoDefinitionResponse}; 4 | use tower_lsp::jsonrpc::Result; 5 | 6 | pub(crate) fn goto_definition( 7 | state: &BackendState, 8 | params: GotoDefinitionParams, 9 | ) -> Result> { 10 | let uri = params.text_document_position_params.text_document.uri; 11 | let file = EnvFs::normalize_path(&uri.to_file_path().unwrap()); 12 | 13 | let position = ¶ms.text_document_position_params.position; 14 | 15 | if let Some(references) = state.symbol_references.get(&file) { 16 | for (node, ranges) in references { 17 | if ranges.iter().any(|r| in_range(position, &get_range(*r))) { 18 | if let Some(location) = environment::symbol_location(&state.arena, node) { 19 | return Ok(Some(GotoDefinitionResponse::Scalar(location))); 20 | } 21 | } 22 | } 23 | } 24 | 25 | Ok(None) 26 | } 27 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "compile", 7 | "group": "build", 8 | "presentation": { 9 | "panel": "dedicated", 10 | "reveal": "never" 11 | }, 12 | "problemMatcher": [ 13 | "$tsc" 14 | ], 15 | "options": { 16 | "cwd": "${workspaceRoot}/client" 17 | } 18 | }, 19 | { 20 | "type": "npm", 21 | "script": "watch", 22 | "isBackground": true, 23 | "group": { 24 | "kind": "build", 25 | "isDefault": true 26 | }, 27 | "presentation": { 28 | "panel": "dedicated", 29 | "reveal": "never" 30 | }, 31 | "problemMatcher": [ 32 | "$tsc-watch" 33 | ], 34 | "options": { 35 | "cwd": "${workspaceRoot}/client" 36 | } 37 | }, 38 | { 39 | "label": "build", 40 | "type": "shell", 41 | "command": "cargo", 42 | "args": [ 43 | "build", 44 | // "--release", 45 | // "--", 46 | // "arg1" 47 | ], 48 | "group": { 49 | "kind": "build", 50 | "isDefault": true 51 | } 52 | } 53 | ] 54 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2021 Fabian Becker 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. -------------------------------------------------------------------------------- /client/src/test/runTest.ts: -------------------------------------------------------------------------------- 1 | /*--------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | *--------------------------------------------------------------------------------------------*/ 5 | import * as path from 'path'; 6 | 7 | import { runTests } from 'vscode-test'; 8 | 9 | async function main() { 10 | try { 11 | // The folder containing the Extension Manifest package.json 12 | // Passed to `--extensionDevelopmentPath` 13 | const extensionDevelopmentPath = path.resolve(__dirname, '../../../'); 14 | 15 | // The path to test runner 16 | // Passed to --extensionTestsPath 17 | const extensionTestsPath = path.resolve(__dirname, './index'); 18 | 19 | // Download VS Code, unzip it and run the integration test 20 | await runTests({ extensionDevelopmentPath, extensionTestsPath }); 21 | } catch (err) { 22 | console.error('Failed to run tests'); 23 | process.exit(1); 24 | } 25 | } 26 | 27 | main(); -------------------------------------------------------------------------------- /src/environment/traverser.rs: -------------------------------------------------------------------------------- 1 | use super::visitor::NextAction; 2 | use super::visitor::Visitor; 3 | use super::Symbol; 4 | use crate::parser::node::Node as AstNode; 5 | use indextree::{Arena, NodeId}; 6 | 7 | pub fn traverse(node: &AstNode, visitor: &mut T, arena: &mut Arena, parent: NodeId) 8 | where 9 | T: Visitor, 10 | { 11 | visitor.before(node, arena, parent); 12 | 13 | match visitor.visit(node, arena, parent) { 14 | NextAction::Abort => (), 15 | NextAction::ProcessChildren(next_parent) => { 16 | // If node is a namespace somehow the classes and interface etc. are not added to it but still to the file 17 | // Probably because they are not children in the ast node of namespace 18 | for child in node.children() { 19 | //eprintln!("[environment::traverser::traverse] calling traverse()"); 20 | traverse(child, visitor, arena, next_parent); 21 | //eprintln!("[environment::traverser::traverse] calling traverse() ended"); 22 | } 23 | } 24 | } 25 | 26 | visitor.after(node, arena, parent); 27 | } 28 | -------------------------------------------------------------------------------- /src/environment/scope.rs: -------------------------------------------------------------------------------- 1 | use super::get_range; 2 | use crate::parser::node::TypeRef; 3 | use indextree::NodeId; 4 | use tower_lsp::lsp_types::Range; 5 | 6 | #[derive(Clone, Debug)] 7 | pub struct Reference { 8 | /// The type_ref if applicable 9 | pub type_ref: TypeRef, 10 | 11 | /// Selection range of the usage 12 | pub range: Range, 13 | 14 | pub node: Option, 15 | } 16 | 17 | impl std::fmt::Display for Reference { 18 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 19 | f.write_str(&self.type_ref.to_fqdn())?; 20 | 21 | Ok(()) 22 | } 23 | } 24 | 25 | impl Reference { 26 | /// Reference to an identifier, for example a function or a member 27 | pub fn node(type_ref: TypeRef, node: NodeId) -> Self { 28 | let range = get_range(type_ref.range()); 29 | 30 | Self { 31 | type_ref, 32 | range, 33 | node: Some(node), 34 | } 35 | } 36 | 37 | /// Reference to a type 38 | pub fn type_ref(type_ref: TypeRef) -> Self { 39 | let range = get_range(type_ref.range()); 40 | 41 | Self { 42 | type_ref, 43 | range, 44 | node: None, 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/backend/did_change_watched_files.rs: -------------------------------------------------------------------------------- 1 | use lsp_types::{Diagnostic, DidChangeWatchedFilesParams}; 2 | 3 | use super::{Backend, BackendState}; 4 | use crate::environment::fs as EnvFs; 5 | 6 | pub(crate) fn did_change_watched_files( 7 | state: &mut BackendState, 8 | params: DidChangeWatchedFilesParams, 9 | ) { 10 | for change in params.changes { 11 | let file_path = change.uri.to_file_path().unwrap(); 12 | 13 | let path = EnvFs::normalize_path(&file_path); 14 | 15 | // If the file is currently opened we don't have to refresh 16 | if state.opened_files.contains_key(&path) { 17 | return; 18 | } 19 | 20 | let content = std::fs::read_to_string(file_path).unwrap(); 21 | 22 | if let Ok((ast, range, errors)) = Backend::source_to_ast(&content) { 23 | if let Err(e) = Backend::collect_symbols(&path, &ast, &range, state) { 24 | eprintln!("Error collecting symbols: {}", e); 25 | } 26 | 27 | let diagnostics = state 28 | .diagnostics 29 | .entry(path.to_string()) 30 | .or_insert_with(Vec::new); 31 | diagnostics.clear(); 32 | diagnostics.extend(errors.iter().map(Diagnostic::from)); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/backend/did_open.rs: -------------------------------------------------------------------------------- 1 | use super::{Backend, BackendState}; 2 | use crate::environment::fs as EnvFs; 3 | use lsp_types::{Diagnostic, DidOpenTextDocumentParams}; 4 | 5 | pub(crate) fn did_open(state: &mut BackendState, params: &DidOpenTextDocumentParams) { 6 | let file_path = params.text_document.uri.to_file_path().unwrap(); 7 | 8 | let path = EnvFs::normalize_path(&file_path); 9 | let source = std::fs::read_to_string(file_path).unwrap(); 10 | state 11 | .latest_version_of_file 12 | .insert(path.clone(), source.clone()); 13 | 14 | if !state.opened_files.contains_key(&path) { 15 | if let Ok((ast, range, errors)) = Backend::source_to_ast(&source) { 16 | let diags = errors.iter().map(Diagnostic::from).collect(); 17 | state.diagnostics.insert(path.to_owned(), diags); 18 | state.opened_files.insert(path.to_string(), (ast, range)); 19 | } else { 20 | return; 21 | } 22 | } 23 | 24 | let (ast, _) = if let Some((ast, range)) = state.opened_files.get(&path) { 25 | (ast.clone(), range) 26 | } else { 27 | return; 28 | }; 29 | 30 | if let Err(e) = Backend::collect_references(&path, &ast, state, None) { 31 | eprintln!("Error collecting references {}", e); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/test/index.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | import * as path from 'path'; 6 | import * as Mocha from 'mocha'; 7 | import * as glob from 'glob'; 8 | 9 | export function run(): Promise { 10 | // Create the mocha test 11 | const mocha = new Mocha({ 12 | ui: 'tdd', 13 | }); 14 | mocha.useColors(true); 15 | mocha.timeout(100000); 16 | 17 | const testsRoot = __dirname; 18 | 19 | return new Promise((resolve, reject) => { 20 | glob('**.test.js', { cwd: testsRoot }, (err, files) => { 21 | if (err) { 22 | return reject(err); 23 | } 24 | 25 | // Add files to the test suite 26 | files.forEach(f => mocha.addFile(path.resolve(testsRoot, f))); 27 | 28 | try { 29 | // Run the mocha test 30 | mocha.run(failures => { 31 | if (failures > 0) { 32 | reject(new Error(`${failures} tests failed.`)); 33 | } else { 34 | resolve(); 35 | } 36 | }); 37 | } catch (err) { 38 | console.error(err); 39 | reject(err); 40 | } 41 | }); 42 | }); 43 | } -------------------------------------------------------------------------------- /src/backend/symbol.rs: -------------------------------------------------------------------------------- 1 | use super::BackendState; 2 | use lsp_types::{Location, SymbolInformation, SymbolTag, Url, WorkspaceSymbolParams}; 3 | use tower_lsp::jsonrpc::Result; 4 | 5 | pub(crate) fn symbol( 6 | state: &BackendState, 7 | params: WorkspaceSymbolParams, 8 | ) -> Result>> { 9 | if params.query.is_empty() { 10 | return Ok(None); 11 | } 12 | 13 | let query = params.query.to_lowercase(); 14 | 15 | let mut symbols = Vec::new(); 16 | 17 | for (file_name, node) in state.files.iter() { 18 | for symbol in node.descendants(&state.arena) { 19 | let symbol = state.arena[symbol].get(); 20 | 21 | if symbol.normalized_name().starts_with(&query) { 22 | if let Some(kind) = symbol.kind.get_symbol_kind() { 23 | let tags = if symbol.deprecated.is_some() { 24 | Some(vec![SymbolTag::DEPRECATED]) 25 | } else { 26 | None 27 | }; 28 | symbols.push(SymbolInformation { 29 | name: symbol.name().to_owned(), 30 | tags, 31 | kind, 32 | location: Location { 33 | uri: Url::from_file_path(&file_name).unwrap(), 34 | range: symbol.range, 35 | }, 36 | container_name: None, 37 | deprecated: None, 38 | }) 39 | } 40 | } 41 | } 42 | } 43 | 44 | Ok(Some(symbols)) 45 | } 46 | -------------------------------------------------------------------------------- /src/backend/formatting.rs: -------------------------------------------------------------------------------- 1 | use super::BackendState; 2 | use crate::formatter::format_file; 3 | use crate::parser::scanner::Scanner; 4 | use crate::parser::Parser; 5 | use crate::{ 6 | environment::{fs as EnvFs, get_range}, 7 | formatter::FormatterOptions, 8 | }; 9 | use lsp_types::{DocumentFormattingParams, TextEdit}; 10 | use tower_lsp::jsonrpc::Result; 11 | 12 | pub(crate) fn formatting( 13 | state: &BackendState, 14 | params: DocumentFormattingParams, 15 | ) -> Result>> { 16 | let uri = params.text_document.uri; 17 | let file_path = uri.to_file_path().unwrap(); 18 | let path = EnvFs::normalize_path(&file_path); 19 | 20 | if let Some(source) = state.latest_version_of_file.get(&path) { 21 | let mut scanner = Scanner::new(source); 22 | scanner.scan().unwrap(); 23 | 24 | let range = scanner.document_range(); 25 | let (ast, errors) = Parser::ast(scanner.tokens).unwrap(); 26 | 27 | // Reformatting a half broken source is a very bad idea, only format if its 28 | // parsed without errors. 29 | if !errors.is_empty() { 30 | return Ok(None); 31 | } 32 | 33 | let formatted = format!( 34 | " { 11 | const docUri = getDocUri('completion.txt'); 12 | 13 | test('Completes JS/TS in txt file', async () => { 14 | await testCompletion(docUri, new vscode.Position(0, 0), { 15 | items: [ 16 | { label: 'JavaScript', kind: vscode.CompletionItemKind.Text }, 17 | { label: 'TypeScript', kind: vscode.CompletionItemKind.Text } 18 | ] 19 | }); 20 | }); 21 | }); 22 | 23 | async function testCompletion( 24 | docUri: vscode.Uri, 25 | position: vscode.Position, 26 | expectedCompletionList: vscode.CompletionList 27 | ) { 28 | await activate(docUri); 29 | 30 | // Executing the command `vscode.executeCompletionItemProvider` to simulate triggering completion 31 | const actualCompletionList = (await vscode.commands.executeCommand( 32 | 'vscode.executeCompletionItemProvider', 33 | docUri, 34 | position 35 | )) as vscode.CompletionList; 36 | 37 | assert.ok(actualCompletionList.items.length >= 2); 38 | expectedCompletionList.items.forEach((expectedItem, i) => { 39 | const actualItem = actualCompletionList.items[i]; 40 | assert.equal(actualItem.label, expectedItem.label); 41 | assert.equal(actualItem.kind, expectedItem.kind); 42 | }); 43 | } 44 | -------------------------------------------------------------------------------- /client/src/test/helper.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as path from 'path'; 8 | 9 | export let doc: vscode.TextDocument; 10 | export let editor: vscode.TextEditor; 11 | export let documentEol: string; 12 | export let platformEol: string; 13 | 14 | /** 15 | * Activates the vscode.lsp-sample extension 16 | */ 17 | export async function activate(docUri: vscode.Uri) { 18 | // The extensionId is `publisher.name` from package.json 19 | const ext = vscode.extensions.getExtension('vscode-samples.lsp-sample')!; 20 | await ext.activate(); 21 | try { 22 | doc = await vscode.workspace.openTextDocument(docUri); 23 | editor = await vscode.window.showTextDocument(doc); 24 | await sleep(2000); // Wait for server activation 25 | } catch (e) { 26 | console.error(e); 27 | } 28 | } 29 | 30 | async function sleep(ms: number) { 31 | return new Promise(resolve => setTimeout(resolve, ms)); 32 | } 33 | 34 | export const getDocPath = (p: string) => { 35 | return path.resolve(__dirname, '../../testFixture', p); 36 | }; 37 | export const getDocUri = (p: string) => { 38 | return vscode.Uri.file(getDocPath(p)); 39 | }; 40 | 41 | export async function setTestContent(content: string): Promise { 42 | const all = new vscode.Range( 43 | doc.positionAt(0), 44 | doc.positionAt(doc.getText().length) 45 | ); 46 | return editor.edit(eb => eb.replace(all, content)); 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # phpls-rs 2 | 3 | phpls-rs is a PHP language server written in Rust. 4 | 5 | ## Documentation 6 | 7 | [docs/index.md](docs/index.md) is the entrypoint of the (growing, incomplete) architecture documentation. 8 | 9 | ## Installation 10 | 11 | Currently it is only possible to install the development version manually. In the future there will of course be an installable VSCode extension in the 12 | VSCode marketplace. 13 | 14 | ```bash 15 | # Clone the repository 16 | git clone https://github.com/sawmurai/phpls-rs.git 17 | cd phpls-rs 18 | 19 | # Build the language server binary 20 | cargo build --release 21 | 22 | # The binary is now here, you will need the path to it later 23 | ls `pwd`/target/release/phpls-rs 24 | 25 | # Build the VSCode extension 26 | cd client 27 | yarn 28 | yarn build 29 | ``` 30 | 31 | Clone the phpstorm stubs 32 | ```bash 33 | git clone https://github.com/JetBrains/phpstorm-stubs.git 34 | ``` 35 | 36 | Install the `client/phpls-rs-client-0.0.1.vsix` file manually in VSCode: 37 | 38 | 1. Open VSCode 39 | 2. Open the extension menu 40 | 3. Click `...` and select "Install from VSIX" 41 | 4. Open the extension config (search for phpls) 42 | 5. Put the path to the binary (see instructions above) into the "binary" field 43 | 6. Put the path to the PHPStorm stubs into the "PHP-stubs" field 44 | 45 | ### Known issues 46 | 47 | This is an early development version! Do not use this in production yet (unless you also like to live dangerously ;) ). You might encounter high CPU usage which usually means that something is running in an infinite loop or, low CPU usage but no more response ... that means we are looking a deadlock. 48 | 49 | Should you be able to isolate the problem I would very much appreciate an [issue here](https://github.com/sawmurai/phpls-rs/issues) :) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpls-rs-client", 3 | "description": "VSCode part of the phpls-rs language server", 4 | "author": "Fabian Becker", 5 | "license": "MIT", 6 | "version": "0.0.1", 7 | "publisher": "sawmurai", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/sawmurai/phpls-rs" 11 | }, 12 | "engines": { 13 | "vscode": "^1.43.0" 14 | }, 15 | "dependencies": { 16 | "vscode-languageclient": "^6.1.3" 17 | }, 18 | "activationEvents": [ 19 | "onLanguage:plaintext" 20 | ], 21 | "main": "./client/out/extension", 22 | "contributes": { 23 | "configuration": { 24 | "type": "object", 25 | "title": "phplsrs", 26 | "properties": { 27 | "phplsrs.binary": { 28 | "scope": "resource", 29 | "type": "string", 30 | "default": ".", 31 | "description": "Path to the binary." 32 | }, 33 | "phplsrs.stubs": { 34 | "scope": "machine", 35 | "type": "string", 36 | "default": ".", 37 | "description": "Path to the PHPStorm language stubs." 38 | }, 39 | "phplsrs.trace.server": { 40 | "scope": "window", 41 | "type": "string", 42 | "enum": [ 43 | "off", 44 | "messages", 45 | "verbose" 46 | ], 47 | "default": "messages", 48 | "description": "Traces the communication between VS Code and the awesome language server." 49 | } 50 | } 51 | } 52 | }, 53 | "scripts": { 54 | "vscode:prepublish": "cd client && cd .. && npm run compile", 55 | "compile": "tsc -b", 56 | "watch": "tsc -b -w", 57 | "test": "sh ./scripts/e2e.sh" 58 | }, 59 | "devDependencies": { 60 | "@types/mocha": "^5.2.7", 61 | "mocha": "^6.2.2", 62 | "@types/node": "^12.12.0", 63 | "eslint": "^6.4.0", 64 | "@typescript-eslint/parser": "^2.3.0", 65 | "typescript": "^3.8.3", 66 | "@types/vscode": "1.43.0", 67 | "vscode-test": "^1.3.0" 68 | } 69 | } -------------------------------------------------------------------------------- /src/environment/visitor/mod.rs: -------------------------------------------------------------------------------- 1 | use super::Symbol; 2 | use crate::parser::node::Node as AstNode; 3 | use indextree::{Arena, NodeId}; 4 | 5 | macro_rules! ref_from_doc { 6 | ($source:ident, $dest:ident, $part:ident ) => { 7 | if let Some(doc_comment) = $source { 8 | if let AstNode::DocComment { $part, .. } = doc_comment.as_ref() { 9 | for rt in $part { 10 | $dest.extend( 11 | get_type_refs(rt) 12 | .iter() 13 | .map(|tr| Reference::type_ref(tr.clone())) 14 | .collect::>(), 15 | ); 16 | } 17 | } 18 | } 19 | }; 20 | } 21 | 22 | macro_rules! deprecated_from_doc { 23 | ($source:ident) => { 24 | if let Some(doc_comment) = $source.as_ref() { 25 | if let AstNode::DocComment { is_deprecated, .. } = doc_comment.as_ref() { 26 | if *is_deprecated { 27 | Some(*is_deprecated) 28 | } else { 29 | None 30 | } 31 | } else { 32 | None 33 | } 34 | } else { 35 | None 36 | }; 37 | }; 38 | } 39 | 40 | pub mod name_resolver; 41 | pub mod workspace_symbol; 42 | 43 | pub enum NextAction { 44 | /// Do not continue processing the children 45 | Abort, 46 | 47 | /// Do process the children and consider NodeId the next parent 48 | ProcessChildren(NodeId), 49 | } 50 | 51 | pub trait Visitor { 52 | fn before(&mut self, node: &AstNode, arena: &mut Arena, parent: NodeId); 53 | fn visit(&mut self, node: &AstNode, arena: &mut Arena, parent: NodeId) -> NextAction; 54 | fn after(&mut self, node: &AstNode, arena: &mut Arena, parent: NodeId); 55 | } 56 | -------------------------------------------------------------------------------- /src/backend/hover.rs: -------------------------------------------------------------------------------- 1 | use super::{Backend, BackendState}; 2 | use crate::environment::{fs as EnvFs, get_range, in_range}; 3 | use lsp_types::{Hover, HoverContents, HoverParams, MarkupContent, MarkupKind}; 4 | use tower_lsp::jsonrpc::Result; 5 | 6 | pub(crate) fn hover(state: &BackendState, params: HoverParams) -> Result> { 7 | let file = EnvFs::normalize_path( 8 | ¶ms 9 | .text_document_position_params 10 | .text_document 11 | .uri 12 | .to_file_path() 13 | .unwrap(), 14 | ); 15 | let position = ¶ms.text_document_position_params.position; 16 | 17 | let symbol = Backend::symbol_under_cursor(&state, position, &file); 18 | if let Some((node, _)) = symbol { 19 | let symbol = state.arena[node].get(); 20 | 21 | if in_range(position, &symbol.selection_range) { 22 | return Ok(Some(Hover { 23 | range: Some(symbol.range), 24 | contents: HoverContents::Markup(MarkupContent { 25 | kind: MarkupKind::Markdown, 26 | value: symbol.hover_text(node, &state.arena), 27 | }), 28 | })); 29 | } 30 | } 31 | 32 | if let Some(references) = state.symbol_references.get(&file) { 33 | for (node, ranges) in references { 34 | if let Some(range) = ranges.iter().find(|r| in_range(position, &get_range(**r))) { 35 | let symbol = state.arena[*node].get(); 36 | 37 | return Ok(Some(Hover { 38 | range: Some(get_range(*range)), 39 | contents: HoverContents::Markup(MarkupContent { 40 | kind: MarkupKind::Markdown, 41 | value: symbol.hover_text(*node, &state.arena), 42 | }), 43 | })); 44 | } 45 | } 46 | } 47 | 48 | Ok(None) 49 | } 50 | -------------------------------------------------------------------------------- /src/environment/fs.rs: -------------------------------------------------------------------------------- 1 | use std::{fs, path::Path, path::PathBuf}; 2 | use tokio::io; 3 | 4 | pub(crate) fn reindex_folder(dir: &Path, ignore: &[PathBuf]) -> io::Result> { 5 | let mut files = Vec::new(); 6 | 7 | if dir.is_dir() { 8 | let entries = match fs::read_dir(dir) { 9 | Ok(entries) => entries, 10 | Err(e) => { 11 | eprintln!("Error reading folder {:?}: {}", dir, e); 12 | 13 | return Ok(files); 14 | } 15 | }; 16 | 17 | for entry in entries { 18 | let entry = match entry { 19 | Ok(entry) => entry, 20 | Err(e) => { 21 | eprintln!("- Error reading folder {:?}: {}", dir, e); 22 | 23 | return Ok(files); 24 | } 25 | }; 26 | let path = entry.path(); 27 | if ignore.iter().any(|s| path.ends_with(s)) { 28 | continue; 29 | } 30 | 31 | if path.is_dir() { 32 | // && !path.ends_with("vendor") { 33 | files.extend(reindex_folder(&path, ignore)?); 34 | } else if let Some(ext) = path.extension() { 35 | if ext == "php" { 36 | files.push(path); 37 | } 38 | } 39 | } 40 | } 41 | Ok(files) 42 | } 43 | 44 | pub(crate) fn normalize_path(path: &Path) -> String { 45 | path.to_str().unwrap().to_owned() 46 | } 47 | 48 | pub(crate) fn file_read_range(path: &str, start: u32, end: u32) -> String { 49 | let content = match fs::read_to_string(path) { 50 | Ok(content) => content, 51 | _ => return String::from("Error reading source file"), 52 | }; 53 | 54 | content 55 | .lines() 56 | .skip(start as usize) 57 | .take((end - start + 1) as usize) 58 | .map(|s| format!("{}\n", s)) 59 | .collect() 60 | } 61 | -------------------------------------------------------------------------------- /client/src/test/diagnostics.test.ts: -------------------------------------------------------------------------------- 1 | /* -------------------------------------------------------------------------------------------- 2 | * Copyright (c) Microsoft Corporation. All rights reserved. 3 | * Licensed under the MIT License. See License.txt in the project root for license information. 4 | * ------------------------------------------------------------------------------------------ */ 5 | 6 | import * as vscode from 'vscode'; 7 | import * as assert from 'assert'; 8 | import { getDocUri, activate } from './helper'; 9 | 10 | suite('Should get diagnostics', () => { 11 | const docUri = getDocUri('diagnostics.txt'); 12 | 13 | test('Diagnoses uppercase texts', async () => { 14 | await testDiagnostics(docUri, [ 15 | { message: 'ANY is all uppercase.', range: toRange(0, 0, 0, 3), severity: vscode.DiagnosticSeverity.Warning, source: 'ex' }, 16 | { message: 'ANY is all uppercase.', range: toRange(0, 14, 0, 17), severity: vscode.DiagnosticSeverity.Warning, source: 'ex' }, 17 | { message: 'OS is all uppercase.', range: toRange(0, 18, 0, 20), severity: vscode.DiagnosticSeverity.Warning, source: 'ex' } 18 | ]); 19 | }); 20 | }); 21 | 22 | function toRange(sLine: number, sChar: number, eLine: number, eChar: number) { 23 | const start = new vscode.Position(sLine, sChar); 24 | const end = new vscode.Position(eLine, eChar); 25 | return new vscode.Range(start, end); 26 | } 27 | 28 | async function testDiagnostics(docUri: vscode.Uri, expectedDiagnostics: vscode.Diagnostic[]) { 29 | await activate(docUri); 30 | 31 | const actualDiagnostics = vscode.languages.getDiagnostics(docUri); 32 | 33 | assert.equal(actualDiagnostics.length, expectedDiagnostics.length); 34 | 35 | expectedDiagnostics.forEach((expectedDiagnostic, i) => { 36 | const actualDiagnostic = actualDiagnostics[i]; 37 | assert.equal(actualDiagnostic.message, expectedDiagnostic.message); 38 | assert.deepEqual(actualDiagnostic.range, expectedDiagnostic.range); 39 | assert.equal(actualDiagnostic.severity, expectedDiagnostic.severity); 40 | }); 41 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "type": "extensionHost", 6 | "request": "launch", 7 | "name": "Launch Client", 8 | "runtimeExecutable": "${execPath}", 9 | "args": [ 10 | "--extensionDevelopmentPath=${workspaceRoot}" 11 | ], 12 | "outFiles": [ 13 | "${workspaceRoot}/client/out/**/*.js" 14 | ], 15 | "env": { 16 | "RUST_BACKTRACE": "1" 17 | }, 18 | "preLaunchTask": "build", 19 | "trace": true, 20 | //"cwd": "${workspaceRoot}/client" 21 | }, 22 | { 23 | "type": "lldb", 24 | "request": "launch", 25 | "name": "Debug executable 'phpls-rs'", 26 | "cargo": { 27 | "args": [ 28 | "build", 29 | "--bin=phpls-rs", 30 | "--package=phpls-rs" 31 | ], 32 | "filter": { 33 | "name": "phpls-rs", 34 | "kind": "bin" 35 | } 36 | }, 37 | "args": [ 38 | "fixtures/class.php" 39 | ], 40 | "cwd": "${workspaceFolder}" 41 | }, 42 | { 43 | "type": "lldb-vscode", 44 | "request": "launch", 45 | "name": "Debug unit tests in executable 'phpls-rs'", 46 | "cargo": { 47 | "args": [ 48 | "test", 49 | "--no-run", 50 | "--bin=phpls-rs", 51 | "--package=phpls-rs", 52 | "--", 53 | "test_suggests_members_of_this" 54 | ], 55 | "filter": { 56 | "name": "phpls-rs", 57 | "kind": "bin" 58 | } 59 | }, 60 | "args": [], 61 | "cwd": "${workspaceFolder}" 62 | } 63 | ] 64 | } -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "phpls-rs-client", 3 | "description": "VSCode part of the phpls-rs language server", 4 | "author": "Fabian Becker", 5 | "license": "MIT", 6 | "version": "0.0.1", 7 | "publisher": "sawmurai", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/sawmurai/phpls-rs" 11 | }, 12 | "engines": { 13 | "vscode": "^1.52.0" 14 | }, 15 | "dependencies": { 16 | "vscode-languageclient": "^7.0.0" 17 | }, 18 | "activationEvents": [ 19 | "onLanguage:php" 20 | ], 21 | "main": "./out/extension", 22 | "contributes": { 23 | "configuration": { 24 | "type": "object", 25 | "title": "PHP LS RS", 26 | "properties": { 27 | "phplsrs.binary": { 28 | "scope": "resource", 29 | "type": "string", 30 | "default": ".", 31 | "description": "Path to the binary." 32 | }, 33 | "phplsrs.stubs": { 34 | "scope": "resource", 35 | "type": "string", 36 | "default": ".", 37 | "description": "Path to the PHPStorm language stubs." 38 | }, 39 | "phplsrs.ignorePatterns": { 40 | "scope": "resource", 41 | "type": "string", 42 | "default": "node_modules", 43 | "description": "Path endings to ignore while indexing." 44 | }, 45 | "phplsrs.trace.server": { 46 | "scope": "window", 47 | "type": "string", 48 | "enum": [ 49 | "off", 50 | "messages", 51 | "verbose" 52 | ], 53 | "default": "messages", 54 | "description": "Traces the communication between VS Code and the awesome language server." 55 | } 56 | } 57 | } 58 | }, 59 | "scripts": { 60 | "build": "vsce package", 61 | "vscode:prepublish": "npm run compile", 62 | "compile": "tsc -b", 63 | "watch": "tsc -b -w", 64 | "test": "sh ./scripts/e2e.sh" 65 | }, 66 | "devDependencies": { 67 | "@types/mocha": "^5.2.7", 68 | "@types/node": "^12.12.0", 69 | "@types/vscode": "1.52.0", 70 | "@typescript-eslint/parser": "^2.3.0", 71 | "eslint": "^6.4.0", 72 | "mocha": "^6.2.2", 73 | "typescript": "^3.8.3", 74 | "vsce": "^1.87.0", 75 | "vscode-test": "^1.3.0" 76 | } 77 | } -------------------------------------------------------------------------------- /docs/img/structurizr-backend-components.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | phpls-rs - Backend - Components 13 | 14 | cluster_12 15 | 16 | Backend 17 | [Container] 18 | 19 | 20 | 21 | 13 22 | 23 | source_to_ast 24 | [Component] 25 | Turns a source string into an 26 | AST 27 | 28 | 29 | 30 | 14 31 | 32 | initialize 33 | [Component] 34 | Initializes a workspace 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/parser/ast/exception_handling.rs: -------------------------------------------------------------------------------- 1 | use super::super::node::Node; 2 | use super::super::token::TokenType; 3 | use super::super::{ExpressionResult, Parser}; 4 | use super::{expressions, types}; 5 | 6 | /// Parses a try catch statement 7 | /// 8 | /// # Details 9 | /// ```php 10 | /// /** from here **/ 11 | /// try (true) { 12 | /// } catch (Exception $e) {} 13 | /// echo "stuff"; 14 | /// } 15 | /// /** to here **/ 16 | /// ``` 17 | pub(crate) fn try_catch_statement(parser: &mut Parser) -> ExpressionResult { 18 | let token = parser.consume(TokenType::Try)?; 19 | let try_block = Box::new(parser.block()?); 20 | 21 | let mut catch_blocks = Vec::new(); 22 | 23 | while parser.next_token_one_of(&[TokenType::Catch]) { 24 | catch_blocks.push(catch_block(parser)?); 25 | } 26 | 27 | let finally_block = if let Some(finally_token) = parser.consume_or_ignore(TokenType::Finally) { 28 | Some(Box::new(Node::FinallyBlock { 29 | token: finally_token, 30 | body: Box::new(parser.block()?), 31 | })) 32 | } else { 33 | None 34 | }; 35 | 36 | Ok(Node::TryCatch { 37 | token, 38 | try_block, 39 | catch_blocks, 40 | finally_block, 41 | }) 42 | } 43 | 44 | /// Parses a catch block (including the catch-keyword, yes, I need to make my mind up about including / excluding the keyword) 45 | /// 46 | /// # Details 47 | /// ```php 48 | /// /** from here **/catch (Exception $e) {} 49 | /// echo "stuff"; 50 | /// } 51 | /// /** to here **/ 52 | /// ``` 53 | pub(crate) fn catch_block(parser: &mut Parser) -> ExpressionResult { 54 | let token = parser.consume(TokenType::Catch)?; 55 | let op = parser.consume(TokenType::OpenParenthesis)?; 56 | let types = types::non_empty_type_ref_union(parser)?; 57 | let var = parser.consume(TokenType::Variable)?; 58 | let cp = parser.consume(TokenType::CloseParenthesis)?; 59 | 60 | let body = Box::new(parser.block()?); 61 | 62 | Ok(Node::CatchBlock { 63 | token, 64 | op, 65 | types, 66 | var, 67 | cp, 68 | body, 69 | }) 70 | } 71 | 72 | pub(crate) fn throw_statement(parser: &mut Parser) -> ExpressionResult { 73 | let token = parser.consume(TokenType::Throw)?; 74 | let expression = Box::new(expressions::expression(parser, 0)?); 75 | 76 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 77 | 78 | Ok(Node::ThrowStatement { token, expression }) 79 | } 80 | -------------------------------------------------------------------------------- /docs/img/structurizr-php-parser-components.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | phpls-rs - PHP Parser - Components 13 | 14 | cluster_4 15 | 16 | PHP Parser 17 | [Container] 18 | 19 | 20 | 21 | 5 22 | 23 | Token Scanner 24 | [Component] 25 | The token scanner turns a 26 | source file into a stream of 27 | Tokens 28 | 29 | 30 | 31 | 6 32 | 33 | Parser 34 | [Component] 35 | The actual parser that turns a 36 | stream of tokens into an AST 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /client/src/extension.ts: -------------------------------------------------------------------------------- 1 | import * as path from 'path'; 2 | import { workspace, ExtensionContext, IndentAction, languages, window, Uri, TextDocument } from 'vscode'; 3 | 4 | import { 5 | LanguageClient, 6 | LanguageClientOptions, 7 | ServerOptions, 8 | TransportKind, 9 | Executable 10 | } from 'vscode-languageclient/node'; 11 | 12 | let client: LanguageClient; 13 | 14 | export function activate(context: ExtensionContext) { 15 | // Used with gratidude from Ben Robert Mewburn 16 | // https://github.com/bmewburn/vscode-intelephense/blob/master/src/extension.ts 17 | languages.setLanguageConfiguration('php', { 18 | wordPattern: /(-?\d*\.\d\w*)|([^\-\`\~\!\@\#\%\^\&\*\(\)\=\+\[\{\]\}\|\;\:\'\"\,\.\<\>\/\?\s\\]+)/g, 19 | onEnterRules: [ 20 | { 21 | // e.g. /** | */ 22 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 23 | afterText: /^\s*\*\/$/, 24 | action: { indentAction: IndentAction.IndentOutdent, appendText: ' * ' } 25 | }, 26 | { 27 | // e.g. /** ...| 28 | beforeText: /^\s*\/\*\*(?!\/)([^\*]|\*(?!\/))*$/, 29 | action: { indentAction: IndentAction.None, appendText: ' * ' } 30 | }, 31 | { 32 | // e.g. * ...| 33 | beforeText: /^(\t|(\ \ ))*\ \*(\ ([^\*]|\*(?!\/))*)?$/, 34 | action: { indentAction: IndentAction.None, appendText: '* ' } 35 | }, 36 | { 37 | // e.g. */| 38 | beforeText: /^(\t|(\ \ ))*\ \*\/\s*$/, 39 | action: { indentAction: IndentAction.None, removeText: 1 } 40 | }, 41 | { 42 | // e.g. *-----*/| 43 | beforeText: /^(\t|(\ \ ))*\ \*[^/]*\*\/\s*$/, 44 | action: { indentAction: IndentAction.None, removeText: 1 } 45 | } 46 | ] 47 | }); 48 | 49 | const args: string[] = [ 50 | '--stubs', 51 | workspace.getConfiguration('phplsrs').get('stubs') 52 | ]; 53 | 54 | let installedServer = workspace.getConfiguration('phplsrs').get('binary'); 55 | 56 | const ignorePatterns = workspace.getConfiguration('phplsrs').get('ignorePatterns') as string | undefined; 57 | if (ignorePatterns) { 58 | ignorePatterns.split(';').forEach(element => { 59 | args.push('--ignore-patterns'); 60 | args.push(element); 61 | }); 62 | } 63 | 64 | let serverModule = (installedServer as string) || context.asAbsolutePath( 65 | path.join('target', 'debug', 'phpls-rs') 66 | ); 67 | 68 | const run: Executable = { 69 | command: serverModule, 70 | options: { cwd: "." }, 71 | args, 72 | }; 73 | 74 | let serverOptions: ServerOptions = { 75 | run, 76 | debug: run 77 | }; 78 | 79 | let clientOptions: LanguageClientOptions = { 80 | documentSelector: [{ scheme: 'file', language: 'php' }], 81 | synchronize: { 82 | fileEvents: workspace.createFileSystemWatcher('**/*.php') 83 | }, 84 | }; 85 | 86 | client = new LanguageClient( 87 | 'phplsrs', 88 | 'PHP Language Server', 89 | serverOptions, 90 | clientOptions 91 | ); 92 | 93 | client.start(); 94 | } 95 | 96 | export function deactivate(): Thenable | undefined { 97 | if (!client) { 98 | return undefined; 99 | } 100 | return client.stop(); 101 | } 102 | -------------------------------------------------------------------------------- /docs/dsl/index.dsl: -------------------------------------------------------------------------------- 1 | workspace "phpls-rs" "Documentation of the architecture of phpls-rs" { 2 | model { 3 | developer = person "The developer using the editor that uses phpls-rs" 4 | 5 | editor = softwaresystem "Editor" "Any text editor that implements a language server client" 6 | 7 | phplsRs = softwaresystem "phpls-rs" "The language server implementation for PHP" { 8 | 9 | parser = container "PHP Parser" "The parser component that turns a source file into an AST. It contains of a parser and a scanner." { 10 | scanner = component "Token Scanner" "The token scanner turns a source file into a stream of Tokens" 11 | parserParser = component "Parser" "The actual parser that turns a stream of tokens into an AST" 12 | } 13 | 14 | formatter = container "Formatter" "The formatter uses the AST and the token stream to reformat a given source file." 15 | 16 | environment = container "Environment" "The environment is aware of the available symbols in the code base." { 17 | NameResolver = component "NameResolver" "The NameResolver resolves an individual symbol to its definition" 18 | NameResolveVisitor = component "NameResolveVisitor" "The NameResolveVisitor resolves references to symbols by walking the AST recursively" 19 | WorkspaceSymbolVisitor = component "WorkspaceSymbolVisitor" "The WorkspaceSymbolVisitor walks the AST recursivly and collects symbols that are relevant from an outside-scope" 20 | } 21 | 22 | backend = container "Backend" "The backend contains handlers for the individual language server features" { 23 | source_to_ast = component "source_to_ast" "Turns a source string into an AST" 24 | initialize = component "initialize" "Initializes a workspace" 25 | } 26 | 27 | backend -> environment "Retrieve information about symbols and their references to each other" 28 | backend -> parser "Turn source files into ASTs" 29 | backend -> formatter "Turn ASTs (+ TokenStreams) back into source strings" 30 | } 31 | 32 | developer -> editor "Write and read code" 33 | editor -> phplsRs "Get semantic information about the code" 34 | } 35 | 36 | views { 37 | systemlandscape "systemlandscape" { 38 | include * 39 | autoLayout 40 | } 41 | 42 | container phplsRs "containers" { 43 | include * 44 | } 45 | 46 | component parser "php-parser-components" { 47 | include * 48 | } 49 | 50 | component backend "backend-components" { 51 | include * 52 | } 53 | 54 | component environment "environment-components" { 55 | include * 56 | } 57 | 58 | dynamic parser "backend-process-source_to_ast" "Shows how the parser returns an AST from a source file" { 59 | source_to_ast -> scanner "Calls to return a token stream from a source string" 60 | source_to_ast -> parserParser "Calls to return an AST from a token stream" 61 | } 62 | 63 | dynamic backend "backend-process-initialize" "Boots the language server" { 64 | initialize -> parser "Instructs to parse a file and return its AST" 65 | initialize -> environment "Instructs to find symbols" 66 | } 67 | } 68 | } -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::must_use_candidate)] 2 | 3 | extern crate clap; 4 | extern crate crossbeam_channel as channel; 5 | extern crate ignore; 6 | 7 | use crate::backend::Backend; 8 | use clap::{App, Arg}; 9 | use std::path::PathBuf; 10 | use tower_lsp::{LspService, Server}; 11 | 12 | pub mod backend; 13 | pub mod environment; 14 | pub mod formatter; 15 | pub mod parser; 16 | pub mod suggester; 17 | 18 | #[tokio::main] 19 | async fn main() { 20 | let matches = App::new("PHPLS-RS") 21 | .version("0.1") 22 | .author("Fabian Becker ") 23 | .about("PHP language server written in Rust") 24 | .arg( 25 | Arg::with_name("stubs") 26 | .long("stubs") 27 | .value_name("Stubs library") 28 | .help("Path to the phpstorm stubs") 29 | .required(true) 30 | .takes_value(true), 31 | ) 32 | .arg( 33 | Arg::with_name("file") 34 | .long("file") 35 | .short("f") 36 | .value_name("Parse single file") 37 | .help("Only parse a single file instead of launching a server") 38 | .takes_value(true), 39 | ) 40 | .arg( 41 | Arg::with_name("dir") 42 | .long("dir") 43 | .short("d") 44 | .value_name("Parse files in directory") 45 | .help("Only parse files in directory instead of launching a server") 46 | .takes_value(true), 47 | ) 48 | .arg( 49 | Arg::with_name("ignore-patterns") 50 | .long("ignore-patterns") 51 | .value_name("Path ending to ignore") 52 | .help("List of endings of file-paths to ignore during indexing") 53 | .multiple(true) 54 | .takes_value(true), 55 | ) 56 | .get_matches(); 57 | 58 | let ignore_patterns: Vec = matches 59 | .values_of("ignore-patterns") 60 | .unwrap_or_default() 61 | .map(|s| s.to_owned()) 62 | .collect(); 63 | 64 | if let Some(file) = matches.value_of("file") { 65 | match Backend::source_to_ast(file) { 66 | Ok((_, _, _)) => println!("Parsed ok"), 67 | Err(e) => eprintln!("Error: {}", e), 68 | } 69 | 70 | return; 71 | } 72 | 73 | if let Some(dir) = matches.value_of("dir") { 74 | let ip: Vec = ignore_patterns.iter().map(PathBuf::from).collect(); 75 | if let Ok(paths) = environment::fs::reindex_folder(&PathBuf::from(dir), &ip) { 76 | paths 77 | .iter() 78 | .map(|pb| environment::fs::normalize_path(pb)) 79 | .for_each(|file| { 80 | if let Err(e) = Backend::source_to_ast(&file) { 81 | eprintln!("Error: {}", e); 82 | } 83 | }); 84 | } 85 | 86 | return; 87 | } 88 | 89 | let stubs = matches.value_of("stubs").unwrap().to_owned(); 90 | 91 | let stdin = tokio::io::stdin(); 92 | let stdout = tokio::io::stdout(); 93 | 94 | let (service, messages) = 95 | LspService::new(|client| Backend::new(client, stubs, ignore_patterns)); 96 | Server::new(stdin, stdout) 97 | .interleave(messages) 98 | .serve(service) 99 | .await; 100 | } 101 | -------------------------------------------------------------------------------- /src/environment/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::environment::symbol::{PhpSymbolKind, Symbol}; 2 | use crate::parser::node::NodeRange; 3 | use indextree::{Arena, NodeId}; 4 | use tower_lsp::lsp_types::{DiagnosticSeverity, Location, Position, Range, Url}; 5 | 6 | pub mod fs; 7 | pub mod import; 8 | pub mod scope; 9 | pub mod symbol; 10 | pub mod traverser; 11 | pub mod visitor; 12 | 13 | /// Find the definition or reference under the cursor 14 | pub(crate) fn symbol_location(arena: &Arena, symbol_node: &NodeId) -> Option { 15 | let range = arena[*symbol_node].get().selection_range; 16 | 17 | let mut symbol_node = *symbol_node; 18 | 19 | while let Some(parent) = arena[symbol_node].parent() { 20 | let symbol = arena[parent].get(); 21 | 22 | if symbol.kind == PhpSymbolKind::File { 23 | return Some(Location { 24 | uri: Url::from_file_path(symbol.name()).unwrap(), 25 | range, 26 | }); 27 | } 28 | 29 | symbol_node = parent; 30 | } 31 | 32 | None 33 | } 34 | 35 | /// Convert a node range into a Range understood by tower lsp 36 | pub fn get_range(coords: NodeRange) -> Range { 37 | Range { 38 | start: Position { 39 | line: coords.start_line, 40 | character: coords.start_col, 41 | }, 42 | end: Position { 43 | line: coords.end_line, 44 | character: coords.end_col, 45 | }, 46 | } 47 | } 48 | 49 | /// Checks if a given `position` is within a given `range`. 50 | pub fn in_range(position: &Position, range: &Range) -> bool { 51 | // Before the start or behind the end 52 | if position.line > range.end.line || position.line < range.start.line { 53 | return false; 54 | } 55 | 56 | // Within the lines 57 | if position.line > range.start.line && position.line < range.end.line { 58 | return true; 59 | } 60 | 61 | // On the start line but before the start 62 | if position.line == range.start.line && position.character < range.start.character { 63 | return false; 64 | } 65 | 66 | // On the end line but behind the end 67 | if position.line == range.end.line && position.character > range.end.character { 68 | return false; 69 | } 70 | 71 | true 72 | } 73 | 74 | #[derive(Clone)] 75 | pub struct Notification { 76 | pub file: String, 77 | pub message: String, 78 | pub range: NodeRange, 79 | pub severity: DiagnosticSeverity, 80 | } 81 | 82 | impl Notification { 83 | pub fn error(file: String, message: String, range: NodeRange) -> Self { 84 | Notification { 85 | file, 86 | message, 87 | range, 88 | severity: DiagnosticSeverity::ERROR, 89 | } 90 | } 91 | 92 | pub fn warning(file: String, message: String, range: NodeRange) -> Self { 93 | Notification { 94 | file, 95 | message, 96 | range, 97 | severity: DiagnosticSeverity::WARNING, 98 | } 99 | } 100 | } 101 | 102 | #[cfg(test)] 103 | mod tests { 104 | use super::get_range; 105 | use tower_lsp::lsp_types::{Position, Range}; 106 | 107 | #[test] 108 | fn test_converts_ranges() { 109 | let expected = Range { 110 | start: Position { 111 | line: 1, 112 | character: 100, 113 | }, 114 | end: Position { 115 | line: 2, 116 | character: 200, 117 | }, 118 | }; 119 | let range = get_range(((100, 1), (200, 2)).into()); 120 | assert_eq!(expected, range); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /docs/img/structurizr-environment-components.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | phpls-rs - Environment - Components 13 | 14 | cluster_8 15 | 16 | Environment 17 | [Container] 18 | 19 | 20 | 21 | 10 22 | 23 | NameResolveVisitor 24 | [Component] 25 | The NameResolveVisitor 26 | resolves references to symbols 27 | by walking the AST recursively 28 | 29 | 30 | 31 | 11 32 | 33 | WorkspaceSymbolVisitor 34 | [Component] 35 | The WorkspaceSymbolVisitor 36 | walks the AST recursivly and 37 | collects symbols that are 38 | relevant from an outside-scope 39 | 40 | 41 | 42 | 9 43 | 44 | NameResolver 45 | [Component] 46 | The NameResolver resolves an 47 | individual symbol to its 48 | definition 49 | 50 | 51 | 52 | -------------------------------------------------------------------------------- /docs/img/structurizr-systemlandscape.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | System Landscape 13 | 14 | 15 | 1 16 | 17 | The developer 18 | using the editor 19 | that uses phpls-rs 20 | [Person] 21 | 22 | 23 | 24 | 2 25 | 26 | Editor 27 | [Software System] 28 | Any text editor that 29 | implements a language server 30 | client 31 | 32 | 33 | 34 | 1->2 35 | 36 | 37 | Write and read code 38 | 39 | 40 | 41 | 3 42 | 43 | phpls-rs 44 | [Software System] 45 | The language server 46 | implementation for PHP 47 | 48 | 49 | 50 | 2->3 51 | 52 | 53 | Get semantic information 54 | about the code 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /docs/img/structurizr-backend-process-initialize.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | Backend - Dynamic 13 | Boots the language server 14 | 15 | cluster_12 16 | 17 | Backend 18 | [Container] 19 | 20 | 21 | 22 | 4 23 | 24 | PHP Parser 25 | [Container] 26 | The parser component that 27 | turns a source file into an 28 | AST. It contains of a parser 29 | and a scanner. 30 | 31 | 32 | 33 | 8 34 | 35 | Environment 36 | [Container] 37 | The environment is aware of 38 | the available symbols in the 39 | code base. 40 | 41 | 42 | 43 | 14 44 | 45 | initialize 46 | [Component] 47 | Initializes a workspace 48 | 49 | 50 | 51 | 14->4 52 | 53 | 54 | 1. Instructs to parse a 55 | file and return its AST 56 | 57 | 58 | 59 | 14->8 60 | 61 | 62 | 2. Instructs to find 63 | symbols 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /docs/img/structurizr-backend-process-source_to_ast.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | PHP Parser - Dynamic 13 | Shows how the parser returns an AST from a source file 14 | 15 | cluster_4 16 | 17 | PHP Parser 18 | [Container] 19 | 20 | 21 | 22 | 13 23 | 24 | source_to_ast 25 | [Component] 26 | Turns a source string into an 27 | AST 28 | 29 | 30 | 31 | 5 32 | 33 | Token Scanner 34 | [Component] 35 | The token scanner turns a 36 | source file into a stream of 37 | Tokens 38 | 39 | 40 | 41 | 13->5 42 | 43 | 44 | 1. Calls to return a token 45 | stream from a source 46 | string 47 | 48 | 49 | 50 | 6 51 | 52 | Parser 53 | [Component] 54 | The actual parser that turns a 55 | stream of tokens into an AST 56 | 57 | 58 | 59 | 13->6 60 | 61 | 62 | 2. Calls to return an AST 63 | from a token stream 64 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/parser/ast/loops.rs: -------------------------------------------------------------------------------- 1 | use super::super::node::Node; 2 | use super::super::token::TokenType; 3 | use super::super::{ExpressionResult, Parser}; 4 | use super::{arrays, expressions}; 5 | 6 | /// Parses a for loop 7 | /// 8 | /// # Details 9 | /// ```php 10 | /// /** from here **/ 11 | /// for ($i = 0; $i < 100; $i++) { 12 | /// do_stuff(); 13 | /// } 14 | /// /** to here **/ 15 | /// ``` 16 | pub(crate) fn for_statement(parser: &mut Parser) -> ExpressionResult { 17 | let token = parser.consume(TokenType::For)?; 18 | parser.consume_or_ff_after(TokenType::OpenParenthesis, &[TokenType::Semicolon])?; 19 | 20 | let mut init = Vec::new(); 21 | while !parser.next_token_one_of(&[TokenType::Semicolon]) { 22 | init.push(expressions::expression(parser, 0)?); 23 | 24 | if parser.next_token_one_of(&[TokenType::Comma]) { 25 | parser.next(); 26 | } else { 27 | break; 28 | } 29 | } 30 | 31 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 32 | 33 | let mut condition = Vec::new(); 34 | while !parser.next_token_one_of(&[TokenType::Semicolon]) { 35 | condition.push(expressions::expression(parser, 0)?); 36 | 37 | if parser.next_token_one_of(&[TokenType::Comma]) { 38 | parser.next(); 39 | } else { 40 | break; 41 | } 42 | } 43 | 44 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 45 | 46 | let mut step = Vec::new(); 47 | while !parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 48 | step.push(expressions::expression(parser, 0)?); 49 | 50 | if parser.next_token_one_of(&[TokenType::Comma]) { 51 | parser.next(); 52 | } else { 53 | break; 54 | } 55 | } 56 | parser.consume_or_ff_after(TokenType::CloseParenthesis, &[TokenType::Semicolon])?; 57 | 58 | let body = Box::new(parser.alternative_block_or_statement(TokenType::EndFor)?); 59 | 60 | Ok(Node::ForStatement { 61 | token, 62 | init, 63 | condition, 64 | step, 65 | body, 66 | }) 67 | } 68 | 69 | /// Parses a while loop 70 | /// 71 | /// # Details 72 | /// ```php 73 | /// /** from here **/ 74 | /// while (true) { 75 | /// do_stuff(); 76 | /// } 77 | /// /** to here **/ 78 | /// ``` 79 | pub(crate) fn while_statement(parser: &mut Parser) -> ExpressionResult { 80 | let token = parser.consume(TokenType::While)?; 81 | let op = parser.consume(TokenType::OpenParenthesis)?; 82 | let condition = Box::new(expressions::expression(parser, 0)?); 83 | let cp = parser.consume(TokenType::CloseParenthesis)?; 84 | let body = Box::new(parser.alternative_block_or_statement(TokenType::EndWhile)?); 85 | 86 | Ok(Node::WhileStatement { 87 | token, 88 | op, 89 | condition, 90 | cp, 91 | body, 92 | }) 93 | } 94 | 95 | /// Parses a foreach loop 96 | /// 97 | /// # Details 98 | /// ```php 99 | /// foreach /** from here **/($array as $k => $v) { 100 | /// do_stuff(); 101 | /// } 102 | /// /** to here **/ 103 | /// ``` 104 | pub(crate) fn foreach_statement(parser: &mut Parser) -> ExpressionResult { 105 | let token = parser.consume(TokenType::Foreach)?; 106 | let op = parser.consume(TokenType::OpenParenthesis)?; 107 | let collection = Box::new(expressions::expression(parser, 0)?); 108 | 109 | let as_token = parser.consume(TokenType::As)?; 110 | let kv = Box::new(arrays::array_pair(parser)?); 111 | let cp = parser.consume(TokenType::CloseParenthesis)?; 112 | 113 | let body = Box::new(parser.alternative_block_or_statement(TokenType::EndForeach)?); 114 | 115 | Ok(Node::ForEachStatement { 116 | token, 117 | op, 118 | collection, 119 | as_token, 120 | kv, 121 | cp, 122 | body, 123 | }) 124 | } 125 | 126 | /// Parses a do-while loop 127 | /// 128 | /// # Details 129 | /// ```php 130 | /// /** from here **/ 131 | /// do { 132 | /// do_stuff(); 133 | /// } while (true); 134 | /// /** to here **/ 135 | /// ``` 136 | pub(crate) fn do_while_statement(parser: &mut Parser) -> ExpressionResult { 137 | let do_token = parser.consume(TokenType::Do)?; 138 | let body = Box::new(parser.statement()?); 139 | let while_token = parser.consume(TokenType::While)?; 140 | let op = parser.consume(TokenType::OpenParenthesis)?; 141 | let condition = Box::new(expressions::expression(parser, 0)?); 142 | let cp = parser.consume(TokenType::CloseParenthesis)?; 143 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 144 | 145 | Ok(Node::DoWhileStatement { 146 | do_token, 147 | while_token, 148 | op, 149 | cp, 150 | condition, 151 | body, 152 | }) 153 | } 154 | -------------------------------------------------------------------------------- /src/formatter/loops.rs: -------------------------------------------------------------------------------- 1 | use super::v2::{pre_statement_span, Chunk, Span}; 2 | use crate::{ 3 | formatter::v2::node_to_spans, 4 | parser::{ 5 | node::Node, 6 | token::{Token, TokenType}, 7 | }, 8 | }; 9 | 10 | // Split a while loop into spans 11 | #[inline] 12 | pub(crate) fn while_to_spans( 13 | spans: &mut Vec, 14 | tokens: &[Token], 15 | token: &Token, 16 | op: &Token, 17 | cp: &Token, 18 | condition: &Node, 19 | body: &Node, 20 | lvl: u8, 21 | ) { 22 | let token_offset = token.offset.unwrap(); 23 | let op_offset = op.offset.unwrap(); 24 | let cp_offset = cp.offset.unwrap(); 25 | 26 | // Start with all comments preceding this statement, then add the token chunk 27 | let pre_span = pre_statement_span(token_offset, tokens, lvl); 28 | 29 | // Add a chunk for the while token and the opening parenthesis 30 | let while_op = Chunk::new(&tokens[token_offset..=op_offset]); 31 | 32 | // Get all the stuff between the closing parenthesis and the start of the block, excluding the 33 | // start of the block. We are searching for something that is not a newline or a comment. Searching 34 | // for a { is not a good idea as the block might not have one 35 | let oc = tokens[cp_offset + 1..] 36 | .iter() 37 | .find(|t| !t.is_comment() && t.t != TokenType::Linebreak) 38 | .unwrap(); 39 | 40 | // If the block starts with a { we glue it to this chunk 41 | let cp_oc = if oc.t == TokenType::OpenCurly { 42 | Chunk::new(&tokens[cp_offset..=oc.offset.unwrap()]) 43 | } else { 44 | Chunk::unspaced(&tokens[cp_offset..oc.offset.unwrap()]) 45 | }; 46 | 47 | let token_span = Span::leaf(vec![while_op.clone()], lvl); 48 | 49 | let mut chunks = pre_span.chunks.clone(); 50 | chunks.push(while_op); 51 | 52 | let mut subspans = vec![pre_span, token_span]; 53 | let mut condition_spans = node_to_spans(condition, tokens, lvl + 1); 54 | let mut body_spans = node_to_spans(body, tokens, lvl + 1); 55 | 56 | if let Some(first) = condition_spans.first() { 57 | // Internalize span-spacing into chunks by attaching a space-right into each of the chunks, except for the last one. 58 | // TODO: Move this into a Span::chunks method that converts the internal chunks into externals, basically doing what the following block does. 59 | if first.spaced { 60 | let len = first.chunks.len(); 61 | chunks.extend(first.chunks.iter().cloned().enumerate().map(|(i, c)| { 62 | if i == len - 1 { 63 | c 64 | } else { 65 | c.with_space_after() 66 | } 67 | })); 68 | } else { 69 | chunks.extend(first.chunks.clone()); 70 | } 71 | } 72 | 73 | chunks.push(cp_oc.clone()); 74 | 75 | subspans.append(&mut condition_spans); 76 | subspans.push(Span::leaf(vec![cp_oc], lvl)); 77 | 78 | let head_span = Span::unspaced(chunks, subspans, lvl); 79 | 80 | spans.push(head_span); 81 | spans.extend(body_spans.drain(..)); 82 | } 83 | 84 | #[cfg(test)] 85 | mod test { 86 | use super::super::v2; 87 | 88 | #[test] 89 | fn test_formats_while() { 90 | let src = "\ 91 | // line comment before while 92 | /*d*/while ($rofl == true ) { 93 | // 1st line comment 94 | /* c1 */true == /* c2 */true/* c3 */ && /* c4 */false == false /* c5 */; // c6 95 | $a = 12 * /*lol*/ 1000 96 | // What happens now? 97 | / 3000* 1000 / 3000* 1000 / 3000* 1000; 98 | // 2nd line comment 99 | // 2nd 2nd line comment 100 | /* cb */$a = 2; /*cc*/$b = 3; /*ddddd*/ 101 | 102 | $a = 1; 103 | $a = 1; 104 | // 3rd new line comment 105 | } // me as well?? 106 | "; 107 | let expected = "\ 108 | // line comment before while 109 | /*d*/while ($rofl == true) { 110 | // 1st line comment 111 | /* c1 */true == /* c2 */true /* c3 */&& /* c4 */false == false/* c5 */; // c6 112 | $a = 12 * /*lol*/1000 // What happens now? 113 | / 3000 * 1000 / 3000 * 1000 / 3000 * 1000; 114 | // 2nd line comment 115 | // 2nd 2nd line comment 116 | /* cb */$a = 2; 117 | /*cc*/$b = 3; 118 | /*ddddd*/ 119 | 120 | $a = 1; 121 | $a = 1; 122 | // 3rd new line comment 123 | } // me as well?? 124 | "; 125 | let (tokens, ast) = v2::test::ast(src); 126 | 127 | let first_offset = tokens.first().unwrap().offset.unwrap(); 128 | let last_offset = tokens.last().unwrap().offset.unwrap(); 129 | let actual = v2::ast_to_spans(&ast, &tokens, 0, first_offset, last_offset) 130 | .iter() 131 | .map(|s| s.to_string()) 132 | .collect::>() 133 | .join("\n"); 134 | 135 | assert_eq!(expected, actual); 136 | 137 | //eprintln!( 138 | // "{:#?}", 139 | // ast_to_spans(&ast, &tokens, 0, first_offset, last_offset) 140 | //); 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/parser/ast/arrays.rs: -------------------------------------------------------------------------------- 1 | use super::super::node::Node; 2 | use super::super::token::TokenType; 3 | use super::super::{Error, ExpressionResult, Parser}; 4 | use super::expressions; 5 | use super::variables; 6 | 7 | /// Parses an array surounded by regular brackets. To parse an array according to the old syntax 8 | /// like `array(1, 2, 3)` use `arrays::old_array`. 9 | pub(crate) fn array(parser: &mut Parser) -> ExpressionResult { 10 | let start = parser.consume(TokenType::OpenBrackets)?; 11 | let mut elements = Vec::new(); 12 | 13 | while !parser.next_token_one_of(&[TokenType::CloseBrackets]) { 14 | // TODO: This is only allowed in a destructuring context. Probably need to split 15 | // this 16 | if parser.consume_or_ignore(TokenType::Comma).is_some() { 17 | continue; 18 | } 19 | 20 | elements.push(array_pair(parser)?); 21 | 22 | parser.consume_or_ignore(TokenType::Comma); 23 | } 24 | 25 | Ok(Node::Array { 26 | ob: start, 27 | elements, 28 | cb: parser.consume(TokenType::CloseBrackets)?, 29 | }) 30 | } 31 | 32 | pub(crate) fn old_array(parser: &mut Parser) -> ExpressionResult { 33 | let start = parser.consume(TokenType::TypeArray)?; 34 | let op = parser.consume(TokenType::OpenParenthesis)?; 35 | let mut elements = Vec::new(); 36 | 37 | while !parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 38 | elements.push(array_pair(parser)?); 39 | 40 | parser.consume_or_ignore(TokenType::Comma); 41 | } 42 | 43 | Ok(Node::OldArray { 44 | token: start, 45 | op, 46 | elements, 47 | cp: parser.consume(TokenType::CloseParenthesis)?, 48 | }) 49 | } 50 | 51 | pub(crate) fn array_pair(parser: &mut Parser) -> ExpressionResult { 52 | // At this point key might as well be the value 53 | let key = expressions::expression(parser, 0)?; 54 | 55 | if let Some(arrow) = parser.consume_or_ignore(TokenType::DoubleArrow) { 56 | // TODO: Raise warning if key is access by reference ... this no works 57 | 58 | // Todo: Rather check for scalarity 59 | if !key.is_offset() { 60 | return Err(Error::IllegalOffsetType { 61 | expr: Box::new(key), 62 | }); 63 | } 64 | 65 | if parser.next_token_one_of(&[TokenType::BinaryAnd]) { 66 | Ok(Node::ArrayElement { 67 | key: Some(Box::new(key)), 68 | arrow: Some(arrow), 69 | value: Box::new(variables::lexical_variable(parser)?), 70 | }) 71 | } else { 72 | Ok(Node::ArrayElement { 73 | key: Some(Box::new(key)), 74 | arrow: Some(arrow), 75 | value: Box::new(expressions::expression(parser, 0)?), 76 | }) 77 | } 78 | } else { 79 | Ok(Node::ArrayElement { 80 | key: None, 81 | arrow: None, 82 | value: Box::new(key), 83 | }) 84 | } 85 | } 86 | 87 | #[cfg(test)] 88 | mod tests { 89 | use super::*; 90 | use crate::parser::node::NodeRange; 91 | use crate::parser::token::{Token, TokenType}; 92 | use crate::parser::{Context, Parser}; 93 | 94 | #[test] 95 | fn test_parses_an_array() { 96 | let mut tokens = vec![ 97 | Token::new(TokenType::OpenBrackets, 1, 1, 0), 98 | Token::named(TokenType::LongNumber, 1, 1, 0, "10"), 99 | Token::new(TokenType::Comma, 1, 1, 0), 100 | Token::named(TokenType::LongNumber, 1, 1, 0, "12"), 101 | Token::new(TokenType::CloseBrackets, 10, 10, 0), 102 | ]; 103 | tokens.reverse(); 104 | 105 | let mut parser = Parser { 106 | doc_comments: Vec::new(), 107 | errors: Vec::new(), 108 | tokens, 109 | context: Context::Out, 110 | eof: (10, 10), 111 | end_of_prev_token: NodeRange::empty(), 112 | }; 113 | 114 | let expected = Node::Array { 115 | ob: Token::new(TokenType::OpenBrackets, 1, 1, 0), 116 | cb: Token::new(TokenType::CloseBrackets, 10, 10, 0), 117 | elements: vec![ 118 | Node::ArrayElement { 119 | key: None, 120 | arrow: None, 121 | value: Box::new(Node::Literal(Token::named( 122 | TokenType::LongNumber, 123 | 1, 124 | 1, 125 | 0, 126 | "10", 127 | ))), 128 | }, 129 | Node::ArrayElement { 130 | key: None, 131 | arrow: None, 132 | value: Box::new(Node::Literal(Token::named( 133 | TokenType::LongNumber, 134 | 1, 135 | 1, 136 | 0, 137 | "12", 138 | ))), 139 | }, 140 | ], 141 | }; 142 | 143 | assert_eq!(expected, array(&mut parser).unwrap()); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/parser/ast/types.rs: -------------------------------------------------------------------------------- 1 | use super::super::node::Node; 2 | use super::super::token::{Token, TokenType}; 3 | use super::super::{ExpressionListResult, ExpressionResult, Parser, Result}; 4 | 5 | pub(crate) fn type_ref_list(parser: &mut Parser) -> ExpressionListResult { 6 | let mut type_refs = vec![non_empty_type_ref(parser)?]; 7 | 8 | while parser.consume_or_ignore(TokenType::Comma).is_some() { 9 | type_refs.push(non_empty_type_ref(parser)?); 10 | } 11 | 12 | Ok(type_refs) 13 | } 14 | 15 | // path -> identifier ("\" identifier)* 16 | pub(crate) fn type_ref(parser: &mut Parser) -> Result>> { 17 | let mut path = Vec::new(); 18 | 19 | if let Some(ns) = parser.consume_or_ignore(TokenType::NamespaceSeparator) { 20 | path.push(ns); 21 | } 22 | 23 | while let Some(identifier) = parser.peek() { 24 | if identifier.is_identifier() { 25 | path.push(parser.next().unwrap()); 26 | 27 | if let Some(ns) = parser.consume_or_ignore(TokenType::NamespaceSeparator) { 28 | path.push(ns); 29 | } else { 30 | break; 31 | } 32 | } else { 33 | break; 34 | } 35 | } 36 | 37 | if path.is_empty() { 38 | Ok(None) 39 | } else { 40 | Ok(Some(Box::new(Node::TypeRef(path.into())))) 41 | } 42 | } 43 | 44 | // non_empty_type_ref -> identifier ("\" identifier)* 45 | pub(crate) fn non_empty_type_ref(parser: &mut Parser) -> ExpressionResult { 46 | let mut path = Vec::new(); 47 | 48 | if let Some(ns) = parser.consume_or_ignore(TokenType::NamespaceSeparator) { 49 | path.push(ns); 50 | } 51 | 52 | path.push(parser.consume_identifier()?); 53 | 54 | while let Some(ns) = parser.consume_or_ignore(TokenType::NamespaceSeparator) { 55 | path.push(ns); 56 | path.push(parser.consume_identifier()?); 57 | } 58 | 59 | Ok(Node::TypeRef(path.into())) 60 | } 61 | 62 | // non_empty_namespace_ref -> "\"? identifier ("\" identifier)* "\"? 63 | pub(crate) fn non_empty_namespace_ref(parser: &mut Parser) -> Result> { 64 | let mut path = Vec::new(); 65 | 66 | if let Some(ns) = parser.consume_or_ignore(TokenType::NamespaceSeparator) { 67 | path.push(ns); 68 | } 69 | 70 | path.push(parser.consume_identifier()?); 71 | 72 | while let Some(ns) = parser.consume_or_ignore(TokenType::NamespaceSeparator) { 73 | path.push(ns); 74 | 75 | if let Some(ident) = parser.peek() { 76 | if ident.is_identifier() { 77 | path.push(parser.next().unwrap()); 78 | 79 | continue; 80 | } 81 | } 82 | 83 | break; 84 | } 85 | Ok(path) 86 | } 87 | 88 | /// Parses a path list, pipe separated. This needs to become a type-list! 89 | /// 90 | /// # Details 91 | /// ```php 92 | /// catch (/** from here **/Exception1 | Exception2/** to here **/ $e) {} 93 | /// echo "stuff"; 94 | /// } 95 | /// 96 | /// ``` 97 | pub(crate) fn non_empty_type_ref_union(parser: &mut Parser) -> Result> { 98 | let mut paths = vec![non_empty_type_ref(parser)?]; 99 | 100 | while parser.consume_or_ignore(TokenType::BinaryOr).is_some() { 101 | paths.push(non_empty_type_ref(parser)?); 102 | } 103 | 104 | Ok(paths) 105 | } 106 | 107 | /// Parses an optional path list, pipe separated 108 | /// 109 | /// # Details 110 | /// ```php 111 | /// catch (/** from here **/Exception1 | Exception2/** to here **/ $e) {} 112 | /// echo "stuff"; 113 | /// } 114 | /// 115 | /// ``` 116 | pub(crate) fn type_ref_union(parser: &mut Parser) -> Result>> { 117 | let mut paths = Vec::new(); 118 | 119 | if let Some(tr) = type_ref(parser)? { 120 | paths.push(*tr); 121 | } else { 122 | return Ok(None); 123 | } 124 | 125 | while parser.consume_or_ignore(TokenType::BinaryOr).is_some() { 126 | paths.push(non_empty_type_ref(parser)?); 127 | } 128 | 129 | Ok(Some(paths)) 130 | } 131 | 132 | /// Parse a variable type 133 | /// 134 | /// # Details 135 | /// ```php 136 | /// function something(/** from here */?int | string/** to here */) {} 137 | /// ``` 138 | pub(crate) fn data_type(parser: &mut Parser) -> Result { 139 | Ok(Node::DataType { 140 | nullable: parser.consume_or_ignore(TokenType::QuestionMark), 141 | type_refs: non_empty_type_ref_union(parser)?, 142 | }) 143 | } 144 | 145 | #[cfg(test)] 146 | mod tests { 147 | use crate::{ 148 | formatter::{format_file, FormatterOptions}, 149 | parser::{scanner::Scanner, Parser}, 150 | }; 151 | 152 | #[test] 153 | fn test_parses_union_type() { 154 | let mut scanner = Scanner::new( 155 | " Result>> { 11 | let file = EnvFs::normalize_path( 12 | ¶ms 13 | .text_document_position_params 14 | .text_document 15 | .uri 16 | .to_file_path() 17 | .unwrap(), 18 | ); 19 | let position = ¶ms.text_document_position_params.position; 20 | 21 | exec(state, position, &file) 22 | } 23 | 24 | #[inline] 25 | fn exec( 26 | state: &BackendState, 27 | position: &Position, 28 | file: &str, 29 | ) -> Result>> { 30 | // Check all references in the current file 31 | if let Some(references) = state.symbol_references.get(file) { 32 | for (node, ranges) in references { 33 | // Does this symbol has a reference at the location we are looking? 34 | if ranges.iter().any(|r| in_range(position, &get_range(*r))) { 35 | // If it does, return with all of its references 36 | if let Some(ranges) = references.get(&node) { 37 | return Ok(Some( 38 | ranges 39 | .iter() 40 | .map(|pos| DocumentHighlight { 41 | kind: None, 42 | range: get_range(*pos), 43 | }) 44 | .collect(), 45 | )); 46 | } 47 | } 48 | } 49 | } 50 | 51 | Ok(None) 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use crate::backend::tests::populate_state; 57 | 58 | use super::*; 59 | use lsp_types::{DocumentHighlight, Position, Range}; 60 | 61 | #[test] 62 | fn returns_none_if_no_symbol_at_position() { 63 | let sources = [("index.php", " Result> { 16 | let position = ¶ms.text_document_position_params.position; 17 | let file = EnvFs::normalize_path( 18 | ¶ms 19 | .text_document_position_params 20 | .text_document 21 | .uri 22 | .to_file_path() 23 | .unwrap(), 24 | ); 25 | let mut results = Vec::new(); 26 | 27 | if let Some((nuc, sym_name)) = Backend::symbol_under_cursor(&state, position, &file) { 28 | let kind_of_suc = state.arena[nuc].get().kind; 29 | 30 | if kind_of_suc == PhpSymbolKind::Method { 31 | let parent_node = if let Some(parent_node) = state.arena[nuc].parent() { 32 | let parent_symbol = state.arena[parent_node].get(); 33 | if parent_symbol.kind == PhpSymbolKind::Interface { 34 | parent_node 35 | } else { 36 | return Ok(None); 37 | } 38 | } else { 39 | return Ok(None); 40 | }; 41 | 42 | let method_name = sym_name.to_lowercase(); 43 | 44 | // We are searching for the implementations of a particular method 45 | return Ok(Some(GotoDefinitionResponse::Array( 46 | implementing_interfaces(parent_node, state) 47 | .iter() 48 | .filter_map(|(file, node)| { 49 | node.children(&state.arena).find_map(|child_of_interface| { 50 | let meth = state.arena[child_of_interface].get(); 51 | 52 | if meth.normalized_name().eq(&method_name) { 53 | Some(Location { 54 | uri: Url::from_file_path(file).unwrap(), 55 | range: meth.selection_range, 56 | }) 57 | } else { 58 | None 59 | } 60 | }) 61 | }) 62 | .collect(), 63 | ))); 64 | } 65 | 66 | if kind_of_suc == PhpSymbolKind::Interface { 67 | results.extend( 68 | implementing_interfaces(nuc, state) 69 | .iter() 70 | .map(|(file, node)| Location { 71 | uri: Url::from_file_path(file).unwrap(), 72 | range: state.arena[*node].get().selection_range, 73 | }), 74 | ) 75 | } 76 | } 77 | 78 | Ok(Some(GotoDefinitionResponse::Array(results))) 79 | } 80 | 81 | fn implementing_interfaces(interface: NodeId, state: &mut BackendState) -> Vec<(String, NodeId)> { 82 | let mut results = Vec::new(); 83 | state.global_symbols.iter().find(|(_, node)| { 84 | let potential_symbol = state.arena[**node].get(); 85 | let symbol_name = potential_symbol.normalized_name(); 86 | 87 | if potential_symbol.kind != PhpSymbolKind::Class { 88 | return false; 89 | } 90 | 91 | potential_symbol 92 | .data_types 93 | .iter() 94 | .filter_map(|reference| { 95 | if let Some(tip) = reference.type_ref.tip() { 96 | // Skip self reference 97 | if !tip.to_lowercase().eq(&symbol_name) { 98 | return Some(reference.type_ref.clone()); 99 | } 100 | } 101 | 102 | None 103 | }) 104 | .find(|type_ref| { 105 | // There is always an enclosing file so we can safely unwrap 106 | let enclosing_file = node 107 | .ancestors(&state.arena) 108 | .find_map(|a| { 109 | let ancestor = state.arena[a].get(); 110 | 111 | if ancestor.kind == PhpSymbolKind::File { 112 | Some(a) 113 | } else { 114 | None 115 | } 116 | }) 117 | .unwrap(); 118 | 119 | let mut resolver = NameResolver::new(&state.global_symbols, enclosing_file); 120 | 121 | if let Some(resolved) = 122 | resolver.resolve_type_ref(&type_ref, &state.arena, &enclosing_file, false) 123 | { 124 | if resolved == interface { 125 | results.push((state.arena[enclosing_file].get().name.clone(), **node)); 126 | return true; 127 | } 128 | 129 | // Check if the type extends the interface we are searching for 130 | if state.arena[resolved].get().is_child_of( 131 | resolved, 132 | &mut resolver, 133 | &state.arena, 134 | interface, 135 | ) { 136 | results.push((state.arena[enclosing_file].get().name.clone(), **node)); 137 | 138 | return true; 139 | } 140 | } 141 | 142 | false 143 | }); 144 | 145 | false 146 | }); 147 | 148 | results 149 | } 150 | -------------------------------------------------------------------------------- /src/parser/ast/keywords.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::token::ScriptStartType; 2 | 3 | use super::super::node::Node; 4 | use super::super::token::TokenType; 5 | use super::super::{ExpressionResult, Parser}; 6 | use super::{arrays, expressions, functions}; 7 | 8 | /// Parses declare statements 9 | pub(crate) fn declare_statement(parser: &mut Parser) -> ExpressionResult { 10 | let token = parser.consume(TokenType::Declare)?; 11 | let op = parser.consume(TokenType::OpenParenthesis)?; 12 | let directive = parser.consume(TokenType::Identifier)?; 13 | let assignment = parser.consume(TokenType::Assignment)?; 14 | let value = parser.consume_one_of(&[ 15 | TokenType::False, 16 | TokenType::True, 17 | TokenType::Null, 18 | TokenType::LongNumber, 19 | TokenType::DecimalNumber, 20 | TokenType::ExponentialNumber, 21 | TokenType::HexNumber, 22 | TokenType::BinaryNumber, 23 | TokenType::ConstantEncapsedString, 24 | TokenType::EncapsedAndWhitespaceString, 25 | ])?; 26 | let cp = parser.consume(TokenType::CloseParenthesis)?; 27 | 28 | Ok(Node::DeclareStatement { 29 | token, 30 | op, 31 | directive, 32 | assignment, 33 | value, 34 | cp, 35 | }) 36 | } 37 | 38 | /// Parses unset statements 39 | pub(crate) fn unset_statement(parser: &mut Parser) -> ExpressionResult { 40 | let token = parser.consume(TokenType::Unset)?; 41 | let op = parser.consume(TokenType::OpenParenthesis)?; 42 | let vars = functions::non_empty_parameter_list(parser)?; 43 | let cp = parser.consume(TokenType::CloseParenthesis)?; 44 | 45 | Ok(Node::UnsetStatement { 46 | token, 47 | cp, 48 | op, 49 | vars, 50 | }) 51 | } 52 | 53 | /// Parses die statements 54 | pub(crate) fn die_statement(parser: &mut Parser) -> ExpressionResult { 55 | let token = parser.consume(TokenType::Die)?; 56 | let op = parser.consume(TokenType::OpenParenthesis)?; 57 | 58 | let (expr, cp) = if let Some(cp) = parser.consume_or_ignore(TokenType::CloseParenthesis) { 59 | (None, cp) 60 | } else { 61 | ( 62 | Some(Box::new(expressions::expression(parser, 0)?)), 63 | parser.consume(TokenType::CloseParenthesis)?, 64 | ) 65 | }; 66 | 67 | Ok(Node::DieStatement { 68 | token, 69 | cp, 70 | op, 71 | expr, 72 | }) 73 | } 74 | 75 | /// Parses define statements 76 | pub(crate) fn define_statement(parser: &mut Parser) -> ExpressionResult { 77 | let token = parser.consume(TokenType::Define)?; 78 | let op = parser.consume(TokenType::OpenParenthesis)?; 79 | let name = Box::new(expressions::expression(parser, 0)?); 80 | parser.consume(TokenType::Comma)?; 81 | let value = Box::new(expressions::expression(parser, 0)?); 82 | 83 | let is_caseinsensitive = if parser.consume_or_ignore(TokenType::Comma).is_some() { 84 | Some(parser.consume_one_of(&[TokenType::True, TokenType::False])?) 85 | } else { 86 | None 87 | }; 88 | 89 | let cp = parser.consume(TokenType::CloseParenthesis)?; 90 | 91 | Ok(Node::DefineStatement { 92 | token, 93 | op, 94 | name, 95 | value, 96 | cp, 97 | is_caseinsensitive, 98 | }) 99 | } 100 | 101 | pub(crate) fn echo_statement(parser: &mut Parser) -> ExpressionResult { 102 | let token = parser.consume(TokenType::Echo)?; 103 | let mut expressions = vec![expressions::expression(parser, 0)?]; 104 | 105 | while parser.consume_or_ignore(TokenType::Comma).is_some() { 106 | expressions.push(expressions::expression(parser, 0)?); 107 | } 108 | 109 | parser.consume_end_of_statement()?; 110 | 111 | Ok(Node::EchoStatement { token, expressions }) 112 | } 113 | 114 | pub(crate) fn short_tag_echo_statement(parser: &mut Parser) -> ExpressionResult { 115 | let token = parser.consume(TokenType::ScriptStart(ScriptStartType::Echo))?; 116 | let mut expressions = vec![expressions::expression(parser, 0)?]; 117 | 118 | while parser.consume_or_ignore(TokenType::Comma).is_some() { 119 | expressions.push(expressions::expression(parser, 0)?); 120 | } 121 | 122 | parser.consume_end_of_statement()?; 123 | 124 | parser.consume_or_ff_after(TokenType::ScriptEnd, &[TokenType::ScriptEnd])?; 125 | 126 | Ok(Node::EchoStatement { token, expressions }) 127 | } 128 | 129 | pub(crate) fn print_statement(parser: &mut Parser) -> ExpressionResult { 130 | let token = parser.consume(TokenType::Print)?; 131 | let expressions = vec![expressions::expression(parser, 0)?]; 132 | 133 | parser.consume_end_of_statement()?; 134 | 135 | Ok(Node::PrintStatement { token, expressions }) 136 | } 137 | 138 | /// Parses the list destructuring operation 139 | pub(crate) fn list(parser: &mut Parser) -> ExpressionResult { 140 | let start = parser.consume(TokenType::List)?; 141 | let op = parser.consume(TokenType::OpenParenthesis)?; 142 | let mut elements = Vec::new(); 143 | 144 | while !parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 145 | // Empty element ... list(,,,$a) 146 | if parser.consume_or_ignore(TokenType::Comma).is_some() { 147 | continue; 148 | } 149 | 150 | elements.push(arrays::array_pair(parser)?); 151 | 152 | parser.consume_or_ignore(TokenType::Comma); 153 | } 154 | Ok(Node::List { 155 | token: start, 156 | op, 157 | elements, 158 | cp: parser.consume(TokenType::CloseParenthesis)?, 159 | }) 160 | } 161 | 162 | pub(crate) fn goto_statement(parser: &mut Parser) -> ExpressionResult { 163 | let token = parser.consume(TokenType::Goto)?; 164 | 165 | let label = parser.consume_identifier()?; 166 | 167 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 168 | 169 | Ok(Node::GotoStatement { token, label }) 170 | } 171 | -------------------------------------------------------------------------------- /src/formatter/expressions.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::{ 2 | node::Node, 3 | token::{Token, TokenType}, 4 | }; 5 | 6 | use super::v2::{next_that, node_to_spans, prev_that, Chunk, Span}; 7 | 8 | // Convert an expression statement into a set of spans. 9 | // 10 | // First get the spans of the inner expression. Then, take the first inner expression and 11 | // prepend all multiline comments that are between the expressions left edge and the previous 12 | // non-comment token. 13 | // Then, get the last inner span and find the actual end of the statement by finding the 14 | // semicolon that terminates the statement. Check if the next token is a line comment and if so, 15 | // make sure to also attach that one. 16 | // Afterwards attach all inner spans to the span container 17 | #[inline] 18 | pub(crate) fn expression_stmt_to_spans( 19 | spans: &mut Vec, 20 | tokens: &[Token], 21 | expression: &Node, 22 | lvl: u8, 23 | ) { 24 | let mut inner_spans = node_to_spans(expression, tokens, lvl); 25 | 26 | if let Some(inner_span) = inner_spans.first_mut() { 27 | let left_edge = inner_span.left_offset(); 28 | 29 | let prev_non_comment = 30 | prev_that(left_edge, tokens, &|t| t.t != TokenType::MultilineComment); 31 | 32 | // Add all the comments from before this expression 33 | inner_span.left_extend(Chunk::new(&tokens[prev_non_comment + 1..left_edge])); 34 | } 35 | 36 | if let Some(inner_span) = inner_spans.last_mut() { 37 | // Add all the stuff to the end of the line 38 | let right_edge = inner_span.right_offset(); 39 | 40 | let end_of_statement = next_that(right_edge, tokens, &|t| t.t == TokenType::Semicolon); 41 | 42 | let end_of_span = if tokens[end_of_statement + 1].t == TokenType::LineComment { 43 | end_of_statement + 1 44 | } else { 45 | end_of_statement 46 | }; 47 | 48 | // Expand the span to either directly after the statement or after a line comment 49 | inner_span.right_extend(Chunk::new(&tokens[right_edge + 1..=end_of_span])); 50 | } 51 | 52 | spans.extend(inner_spans); 53 | } 54 | 55 | // Walk down the binary (tree) and create chunks from it. The function also needs 56 | // to take linecomments into account and create mulitple spans whenever one 57 | // of those comments is encountered. 58 | #[inline] 59 | pub(crate) fn binary_to_spans( 60 | spans: &mut Vec, 61 | tokens: &[Token], 62 | left: &Node, 63 | right: &Node, 64 | lvl: u8, 65 | ) { 66 | let mut chunks = Vec::new(); 67 | let mut subspans: Vec = Vec::new(); 68 | 69 | // Split the left side down into spans 70 | let left_spans = node_to_spans(left, tokens, lvl); 71 | if let Some(first) = left_spans.first() { 72 | chunks.extend(first.chunks.clone()); 73 | subspans.push(first.clone()); 74 | } 75 | 76 | // Basically the right edge of the left side 77 | let rels = chunks.last().unwrap().right_offset(); 78 | 79 | let mut right_spans = node_to_spans(right, tokens, lvl); 80 | 81 | // Take the first span on the right side to determine its left offset 82 | if let Some(right_first) = right_spans.first_mut() { 83 | // Now capture all in between. That includes comments and the operator 84 | let lmt = right_first.left_offset(); 85 | 86 | let mut start = rels + 1; 87 | while let Some(lc) = &tokens[start..lmt] 88 | .iter() 89 | .find(|t| t.t == TokenType::LineComment) 90 | { 91 | // Try to find a line comment in the part between the left and the right side 92 | // If a line comment is found, add it and everything before it as one separate span 93 | 94 | let lc_offset = lc.offset.unwrap() as usize; 95 | let chunk = Chunk::unspaced(&tokens[start..=lc_offset]); 96 | 97 | chunks.push(chunk.clone()); 98 | subspans.push(Span::leaf(vec![chunk], lvl)); 99 | 100 | spans.push(Span::new(chunks, subspans, lvl)); 101 | 102 | chunks = Vec::new(); 103 | subspans = Vec::new(); 104 | 105 | start = lc_offset + 1; 106 | } 107 | 108 | // Add the rest by glueing it the left side of the right operand 109 | let op_chunk = Chunk::unspaced(&tokens[start..lmt]); 110 | 111 | right_first.left_extend(op_chunk); 112 | 113 | chunks.extend(right_first.chunks.clone()); 114 | 115 | subspans.push(right_first.clone()); 116 | spans.push(Span::new(chunks, subspans, lvl)); 117 | } 118 | 119 | // Add the rest of the right spans as individual spans. There are only multiple 120 | // spans if the expression on the right could not be fit into one line (intermittent line comment) 121 | for right in right_spans.iter().skip(1) { 122 | spans.push(right.clone()); 123 | } 124 | } 125 | 126 | #[cfg(test)] 127 | pub(crate) mod test { 128 | use crate::formatter::v2::{ast_to_spans, test::ast}; 129 | 130 | #[test] 131 | fn test_formats_expression() { 132 | let src = "\ 133 | // 1### 134 | $a = 1 * 1 * 1 * 1 135 | // 2### 136 | * 137 | // oh oh 138 | 1000 139 | // 3### /** no single */ 140 | // 4### 141 | /** single */ 142 | / 3000; // Behind 143 | "; 144 | let expected = "\ 145 | // 1### 146 | $a = 1 * 1 * 1 * 1 // 2### 147 | * // oh oh 148 | 1000 // 3### /** no single */ 149 | // 4### 150 | /** single *// 3000; // Behind 151 | "; 152 | 153 | let (tokens, ast) = ast(src); 154 | 155 | let first_offset = tokens.first().unwrap().offset.unwrap(); 156 | let last_offset = tokens.last().unwrap().offset.unwrap(); 157 | 158 | let actual = ast_to_spans(&ast, &tokens, 0, first_offset, last_offset) 159 | .iter() 160 | .map(|s| s.to_string()) 161 | .collect::>() 162 | .join("\n"); 163 | 164 | assert_eq!(expected, actual); 165 | 166 | //eprintln!( 167 | // "{:#?}", 168 | // ast_to_spans(&ast, &tokens, 1, first_offset, last_offset) 169 | //); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /docs/img/structurizr-containers.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | %3 11 | 12 | phpls-rs - Containers 13 | 14 | cluster_3 15 | 16 | phpls-rs 17 | [Software System] 18 | 19 | 20 | 21 | 12 22 | 23 | Backend 24 | [Container] 25 | The backend contains handlers 26 | for the individual language 27 | server features 28 | 29 | 30 | 31 | 4 32 | 33 | PHP Parser 34 | [Container] 35 | The parser component that 36 | turns a source file into an 37 | AST. It contains of a parser 38 | and a scanner. 39 | 40 | 41 | 42 | 12->4 43 | 44 | 45 | Turn source files into 46 | ASTs 47 | 48 | 49 | 50 | 7 51 | 52 | Formatter 53 | [Container] 54 | The formatter uses the AST and 55 | the token stream to reformat a 56 | given source file. 57 | 58 | 59 | 60 | 12->7 61 | 62 | 63 | Turn ASTs (+ TokenStreams) 64 | back into source strings 65 | 66 | 67 | 68 | 8 69 | 70 | Environment 71 | [Container] 72 | The environment is aware of 73 | the available symbols in the 74 | code base. 75 | 76 | 77 | 78 | 12->8 79 | 80 | 81 | Retrieve information about 82 | symbols and their 83 | references to each other 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /src/backend/completion.rs: -------------------------------------------------------------------------------- 1 | use super::BackendState; 2 | use crate::environment::fs as EnvFs; 3 | use crate::environment::get_range; 4 | use crate::{environment::symbol::PhpSymbolKind, suggester}; 5 | 6 | use lsp_types::CompletionContext; 7 | use suggester::SuggestionContext; 8 | use tower_lsp::jsonrpc::Result; 9 | use tower_lsp::lsp_types::{CompletionItem, CompletionParams, CompletionResponse, TextEdit}; 10 | 11 | fn get_trigger(context: Option) -> Option { 12 | if let Some(context) = context { 13 | if let Some(tc) = context.trigger_character { 14 | tc.chars().next() 15 | } else { 16 | None 17 | } 18 | } else { 19 | None 20 | } 21 | } 22 | 23 | pub(crate) fn completion( 24 | state: &BackendState, 25 | params: CompletionParams, 26 | ) -> Result> { 27 | let pos = params.text_document_position.position; 28 | 29 | let opened_file = &EnvFs::normalize_path( 30 | ¶ms 31 | .text_document_position 32 | .text_document 33 | .uri 34 | .to_file_path() 35 | .unwrap(), 36 | ); 37 | 38 | let trigger = get_trigger(params.context); 39 | let file_ast = state.opened_files.get(opened_file); 40 | 41 | if let Some((file_ast, _range)) = file_ast { 42 | let ast = file_ast; 43 | 44 | let current_file_symbol = if let Some(current_file_symbol) = state.files.get(opened_file) { 45 | current_file_symbol 46 | } else { 47 | return Ok(None); 48 | }; 49 | let current_file = state.arena[*current_file_symbol].get(); 50 | 51 | let symbol_under_cursor = current_file.symbol_at(&pos, *current_file_symbol, &state.arena); 52 | 53 | if let Some(references) = state.symbol_references.get(opened_file) { 54 | let mut suggestions = suggester::get_suggestions_at( 55 | trigger, 56 | pos, 57 | symbol_under_cursor, 58 | ast, 59 | &state.arena, 60 | &state.global_symbols, 61 | references, 62 | ); 63 | 64 | return Ok(Some(CompletionResponse::Array( 65 | suggestions 66 | .drain(..) 67 | .map(|sug| { 68 | if sug.is_this { 69 | return CompletionItem { 70 | label: String::from("$this"), 71 | tags: None, 72 | ..CompletionItem::default() 73 | }; 74 | } 75 | 76 | if let Some(token) = sug.token { 77 | return token.into(); 78 | } 79 | 80 | let sn = sug.node.unwrap(); 81 | let symbol = state.arena[sn].get(); 82 | 83 | if symbol.kind == PhpSymbolKind::Class 84 | || symbol.kind == PhpSymbolKind::Trait 85 | { 86 | if sug.context == SuggestionContext::Import { 87 | return CompletionItem { 88 | additional_text_edits: Some(vec![TextEdit { 89 | range: get_range(sug.replace.unwrap()), 90 | new_text: String::from(""), 91 | }]), 92 | label: symbol.fqdn(), 93 | ..symbol.completion_item(sn, &state.arena) 94 | }; 95 | } 96 | 97 | // If the symbo is defined in the global namespace we just return it 98 | if symbol.namespace.is_none() { 99 | return symbol.completion_item(sn, &state.arena); 100 | }; 101 | 102 | // Same namespace, no need to add an import 103 | if current_file.namespace.eq(&symbol.namespace) { 104 | return symbol.completion_item(sn, &state.arena); 105 | } 106 | 107 | let fqdn = symbol.fqdn(); 108 | // Check if the current file already has that import. if yes we are good 109 | let line = if let Some(imports) = current_file.imports.as_ref() { 110 | if imports.all().any(|import| import.full_name() == fqdn) { 111 | return symbol.completion_item(sn, &state.arena); 112 | } else { 113 | // add use to the end of the imports 114 | if let Some(first_import) = imports.all().next() { 115 | first_import.path.range().start_line 116 | } else { 117 | 3 118 | } 119 | } 120 | } else { 121 | // add use right after the namespace or the opening >(), 144 | ))); 145 | } 146 | } 147 | 148 | Ok(None) 149 | } 150 | -------------------------------------------------------------------------------- /src/parser/ast/variables.rs: -------------------------------------------------------------------------------- 1 | use crate::parser::Error; 2 | 3 | use super::super::node::Node; 4 | use super::super::token::{Token, TokenType}; 5 | use super::super::{expressions, ExpressionListResult, ExpressionResult, Parser}; 6 | 7 | pub(crate) fn variable(parser: &mut Parser) -> ExpressionResult { 8 | let variable = parser.consume(TokenType::Variable)?; 9 | 10 | // Named, regular variable. No problem here. 11 | if variable.label.is_some() { 12 | return Ok(Node::Variable(variable)); 13 | } 14 | 15 | // Dynamic member ${expr} 16 | if let Some(oc) = parser.consume_or_ignore(TokenType::OpenCurly) { 17 | return Ok(Node::DynamicVariable { 18 | variable, 19 | oc, 20 | expr: Box::new(expressions::expression(parser, 0)?), 21 | cc: parser.consume(TokenType::CloseCurly)?, 22 | }); 23 | } 24 | 25 | // Collect the list of aliases and return the actual variable at the end 26 | let mut list = vec![variable]; 27 | let mut root = loop { 28 | // This can happen if the user is just typing the variable 29 | if !parser.next_token_one_of(&[TokenType::Variable]) { 30 | let last = list.last().unwrap(); 31 | let pos = last.end(); 32 | parser.errors.push(Error::MissingIdentifier { 33 | token: last.to_owned(), 34 | }); 35 | return Ok(Node::Variable(Token::missing(pos))); 36 | } 37 | 38 | let variable = parser.consume_or_ff_before( 39 | TokenType::Variable, 40 | &[TokenType::Semicolon, TokenType::ScriptEnd], 41 | )?; 42 | 43 | if variable.label.is_some() { 44 | break Node::Variable(variable); 45 | } 46 | 47 | list.push(variable); 48 | }; 49 | 50 | for link in list.drain(..).rev() { 51 | root = Node::AliasedVariable { 52 | variable: link, 53 | expr: Box::new(root), 54 | }; 55 | } 56 | 57 | // Aliased variable $$$$a 58 | Ok(root) 59 | } 60 | 61 | pub(crate) fn global_variables(parser: &mut Parser) -> ExpressionResult { 62 | let token = parser.consume(TokenType::Global)?; 63 | let mut vars = vec![variable(parser)?]; 64 | 65 | parser.consume_or_ignore(TokenType::Comma); 66 | 67 | while !parser.next_token_one_of(&[TokenType::Semicolon]) { 68 | vars.push(variable(parser)?); 69 | 70 | if parser.next_token_one_of(&[TokenType::Semicolon]) { 71 | break; 72 | } else { 73 | parser.consume_or_ff_after(TokenType::Comma, &[TokenType::Semicolon])?; 74 | } 75 | } 76 | 77 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 78 | 79 | Ok(Node::GlobalVariablesStatement { token, vars }) 80 | } 81 | 82 | /// Parses a static variables definition. The token is passed as a parameter as it needs to be fetched 83 | /// in the main loop to tell `static $a` from `static::$a`. 84 | pub(crate) fn static_variables(parser: &mut Parser, token: Token) -> ExpressionResult { 85 | let mut assignments = Vec::new(); 86 | 87 | loop { 88 | let variable = parser.consume(TokenType::Variable)?; 89 | 90 | if let Some(assignment) = parser.consume_or_ignore(TokenType::Assignment) { 91 | assignments.push(Node::StaticVariable { 92 | variable, 93 | assignment: Some(assignment), 94 | value: Some(Box::new(expressions::expression(parser, 0)?)), 95 | }); 96 | } 97 | 98 | if parser.consume_or_ignore(TokenType::Comma).is_none() { 99 | break; 100 | } 101 | } 102 | 103 | Ok(Node::StaticVariablesStatement { token, assignments }) 104 | } 105 | 106 | /// Parses a global variables definition 107 | pub(crate) fn const_statement(parser: &mut Parser) -> ExpressionResult { 108 | let token = parser.consume(TokenType::Const)?; 109 | 110 | let mut constants = vec![Node::Const { 111 | name: parser.consume_identifier()?, 112 | token: parser.consume(TokenType::Assignment)?, 113 | value: Box::new(expressions::expression(parser, 0)?), 114 | }]; 115 | 116 | while parser.consume_or_ignore(TokenType::Semicolon).is_none() { 117 | parser.consume_or_ff_after(TokenType::Comma, &[TokenType::Semicolon])?; 118 | constants.push(Node::Const { 119 | name: parser.consume_identifier()?, 120 | token: parser.consume(TokenType::Assignment)?, 121 | value: Box::new(expressions::expression(parser, 0)?), 122 | }); 123 | } 124 | 125 | Ok(Node::ConstStatement { token, constants }) 126 | } 127 | 128 | /// Parses all the arguments of a call 129 | pub(crate) fn non_empty_lexical_variables_list(parser: &mut Parser) -> ExpressionListResult { 130 | parser.consume_or_ff_after(TokenType::OpenParenthesis, &[TokenType::Semicolon])?; 131 | 132 | let mut arguments = vec![lexical_variable(parser)?]; 133 | 134 | parser.consume_or_ignore(TokenType::Comma); 135 | 136 | while !parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 137 | arguments.push(lexical_variable(parser)?); 138 | 139 | if parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 140 | break; 141 | } else { 142 | parser.consume_or_ff_after(TokenType::Comma, &[TokenType::Semicolon])?; 143 | } 144 | } 145 | 146 | parser.consume_or_ff_after(TokenType::CloseParenthesis, &[TokenType::Semicolon])?; 147 | 148 | Ok(arguments) 149 | } 150 | 151 | /// Parses a lexical variable, used in a "use ()" list for example 152 | pub(crate) fn lexical_variable(parser: &mut Parser) -> ExpressionResult { 153 | Ok(Node::LexicalVariable { 154 | reference: parser.consume_or_ignore(TokenType::BinaryAnd), 155 | variable: parser.consume(TokenType::Variable)?, 156 | }) 157 | } 158 | 159 | #[cfg(test)] 160 | mod test { 161 | use crate::parser::scanner::Scanner; 162 | use crate::parser::Parser; 163 | 164 | #[test] 165 | fn test_parses_aliased_variables() { 166 | let code_semicolon = " ExpressionResult { 20 | let token = parser.consume(TokenType::If)?; 21 | 22 | let op = parser.consume(TokenType::OpenParenthesis)?; 23 | let condition = Box::new(expressions::expression(parser, 0)?); 24 | let cp = parser.consume(TokenType::CloseParenthesis)?; 25 | 26 | // Alternative syntax 27 | if let Some(colon) = parser.consume_or_ignore(TokenType::Colon) { 28 | let mut statements = Vec::new(); 29 | 30 | while !parser.next_token_one_of(&[TokenType::EndIf, TokenType::Else, TokenType::ElseIf]) { 31 | statements.push(parser.statement()?); 32 | } 33 | 34 | let mut terminator = 35 | parser.consume_one_of(&[TokenType::EndIf, TokenType::Else, TokenType::ElseIf])?; 36 | 37 | let if_branch = Node::IfBranch { 38 | token, 39 | op, 40 | condition, 41 | cp, 42 | body: Box::new(Node::AlternativeBlock { 43 | colon, 44 | statements, 45 | terminator: terminator.clone(), 46 | }), 47 | }; 48 | 49 | // Collect all elseif branches 50 | let mut elseif_branches = Vec::new(); 51 | while terminator.t == TokenType::ElseIf { 52 | let token = terminator.clone(); 53 | 54 | let mut statements = Vec::new(); 55 | let op = parser.consume(TokenType::OpenParenthesis)?; 56 | let condition = Box::new(expressions::expression(parser, 0)?); 57 | let cp = parser.consume(TokenType::CloseParenthesis)?; 58 | let colon = parser.consume(TokenType::Colon)?; 59 | 60 | while !parser.next_token_one_of(&[TokenType::EndIf, TokenType::Else, TokenType::ElseIf]) 61 | { 62 | statements.push(parser.statement()?); 63 | } 64 | terminator = 65 | parser.consume_one_of(&[TokenType::EndIf, TokenType::Else, TokenType::ElseIf])?; 66 | 67 | elseif_branches.push(Node::IfBranch { 68 | token, 69 | op, 70 | condition, 71 | cp, 72 | body: Box::new(Node::AlternativeBlock { 73 | colon, 74 | statements, 75 | terminator: terminator.clone(), 76 | }), 77 | }); 78 | } 79 | 80 | let else_branch = if terminator.t == TokenType::Else { 81 | Some(Box::new(Node::ElseBranch { 82 | token: terminator, 83 | body: Box::new(parser.alternative_block(TokenType::EndIf)?), 84 | })) 85 | } else { 86 | None 87 | }; 88 | 89 | return Ok(Node::IfStatement { 90 | if_branch: Box::new(if_branch), 91 | elseif_branches, 92 | else_branch, 93 | }); 94 | } 95 | 96 | // Regular syntax 97 | let if_branch = Node::IfBranch { 98 | token, 99 | op, 100 | condition, 101 | cp, 102 | body: Box::new(parser.statement()?), 103 | }; 104 | 105 | let mut elseif_branches = Vec::new(); 106 | while let Some(token) = parser.consume_or_ignore(TokenType::ElseIf) { 107 | let op = parser.consume(TokenType::OpenParenthesis)?; 108 | let condition = Box::new(expressions::expression(parser, 0)?); 109 | let cp = parser.consume(TokenType::CloseParenthesis)?; 110 | 111 | elseif_branches.push(Node::IfBranch { 112 | token, 113 | op, 114 | condition, 115 | cp, 116 | body: Box::new(parser.statement()?), 117 | }); 118 | } 119 | 120 | let else_branch = if let Some(else_branch) = parser.consume_or_ignore(TokenType::Else) { 121 | Some(Box::new(Node::ElseBranch { 122 | token: else_branch, 123 | body: Box::new(parser.statement()?), 124 | })) 125 | } else { 126 | None 127 | }; 128 | 129 | Ok(Node::IfStatement { 130 | if_branch: Box::new(if_branch), 131 | elseif_branches, 132 | else_branch, 133 | }) 134 | } 135 | 136 | /// Parses a switch case 137 | /// 138 | /// # Details 139 | /// ```php 140 | /// switch /** from here **/(true) { 141 | /// case "bla": 142 | /// echo "stuff"; 143 | /// } 144 | /// /** to here **/ 145 | /// ``` 146 | pub(crate) fn switch_statement(parser: &mut Parser) -> ExpressionResult { 147 | let token = parser.consume(TokenType::Switch)?; 148 | let op = parser.consume(TokenType::OpenParenthesis)?; 149 | let expr = Box::new(expressions::expression(parser, 0)?); 150 | let cp = parser.consume(TokenType::CloseParenthesis)?; 151 | let body = Box::new(switch_body(parser)?); 152 | 153 | Ok(Node::SwitchCase { 154 | token, 155 | op, 156 | expr, 157 | cp, 158 | body, 159 | }) 160 | } 161 | 162 | /// Parses the body part of the switch case and can handle { -> } as well as : -> endswitch 163 | fn switch_body(parser: &mut Parser) -> ExpressionResult { 164 | let mut branches = Vec::new(); 165 | 166 | let (start, terminator_type) = if parser.next_token_one_of(&[TokenType::Colon]) { 167 | (parser.consume(TokenType::Colon)?, TokenType::EndSwitch) 168 | } else { 169 | (parser.consume(TokenType::OpenCurly)?, TokenType::CloseCurly) 170 | }; 171 | 172 | while !parser.next_token_one_of(&[terminator_type.clone()]) { 173 | let cases_current_branch = case_list(parser)?; 174 | 175 | let mut statements = Vec::new(); 176 | while !parser.next_token_one_of(&[ 177 | terminator_type.clone(), 178 | TokenType::Case, 179 | TokenType::Default, 180 | ]) { 181 | statements.push(parser.statement()?); 182 | } 183 | 184 | branches.push(Node::SwitchBranch { 185 | cases: cases_current_branch, 186 | body: statements, 187 | }); 188 | } 189 | 190 | let end = parser.consume(terminator_type)?; 191 | 192 | Ok(Node::SwitchBody { 193 | start, 194 | branches, 195 | end, 196 | }) 197 | } 198 | 199 | pub(crate) fn case_list(parser: &mut Parser) -> Result>> { 200 | let mut cases_current_branch = Vec::new(); 201 | 202 | loop { 203 | match parser.peek() { 204 | Some(Token { 205 | t: TokenType::Default, 206 | .. 207 | }) => { 208 | cases_current_branch.push(None); 209 | parser.next(); 210 | parser.consume_one_of(&[TokenType::Colon, TokenType::Semicolon])?; 211 | } 212 | Some(Token { 213 | t: TokenType::Case, .. 214 | }) => { 215 | parser.next(); 216 | cases_current_branch.push(Some(expressions::expression(parser, 0)?)); 217 | parser.consume_one_of(&[TokenType::Colon, TokenType::Semicolon])?; 218 | } 219 | _ => { 220 | break; 221 | } 222 | } 223 | } 224 | Ok(cases_current_branch) 225 | } 226 | 227 | pub(crate) fn match_body(parser: &mut Parser) -> Result> { 228 | let mut options = Vec::new(); 229 | 230 | while !parser.next_token_one_of(&[TokenType::CloseCurly]) { 231 | let mut patterns = Vec::new(); 232 | 233 | if parser.consume_or_ignore(TokenType::Default).is_some() { 234 | options.push(Node::MatchArm { 235 | patterns: None, 236 | arrow: parser.consume(TokenType::DoubleArrow)?, 237 | expression: Box::new(expressions::expression(parser, 0)?), 238 | }); 239 | } else { 240 | // Loop for all cases 241 | loop { 242 | patterns.push(expressions::expression(parser, 0)?); 243 | 244 | if parser.consume_or_ignore(TokenType::Comma).is_none() { 245 | options.push(Node::MatchArm { 246 | patterns: Some(patterns), 247 | arrow: parser.consume(TokenType::DoubleArrow)?, 248 | expression: Box::new(expressions::expression(parser, 0)?), 249 | }); 250 | break; 251 | } 252 | } 253 | } 254 | 255 | parser.consume_or_ignore(TokenType::Comma); 256 | 257 | if parser.peek().is_none() { 258 | break; 259 | } 260 | } 261 | 262 | Ok(options) 263 | } 264 | -------------------------------------------------------------------------------- /src/parser/ast/functions.rs: -------------------------------------------------------------------------------- 1 | use super::super::token::{Token, TokenType}; 2 | use super::super::{ArgumentListResult, ExpressionListResult, ExpressionResult, Parser, Result}; 3 | use super::comments; 4 | use super::expressions; 5 | use super::types; 6 | use super::variables; 7 | use super::{super::node::Node, attributes::attributes_block}; 8 | 9 | /// Parses the argument list of a function, excluding the parenthesis 10 | /// 11 | /// # Details 12 | /// ```php 13 | /// function my_funy (/** from here **/string $a, int $b/** to here **/): void { 14 | /// echo "Hello!"; 15 | /// } 16 | /// ``` 17 | pub(crate) fn argument_list( 18 | parser: &mut Parser, 19 | doc_comment: &Option>, 20 | ) -> ArgumentListResult { 21 | let mut arguments = Vec::new(); 22 | 23 | if parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 24 | return Ok(None); 25 | } 26 | 27 | loop { 28 | let attributes = attributes_block(parser)?; 29 | let argument_type = argument_type(parser)?; 30 | let reference = parser.consume_or_ignore(TokenType::BinaryAnd); 31 | let spread = parser.consume_or_ignore(TokenType::Elipsis); 32 | let name = parser.consume(TokenType::Variable)?; 33 | let has_default = parser.consume_or_ignore(TokenType::Assignment); 34 | 35 | let default_value = if has_default.is_some() { 36 | Some(Box::new(expressions::expression(parser, 0)?)) 37 | } else { 38 | None 39 | }; 40 | 41 | let doc_comment = comments::param_comment_for(doc_comment, &name).map(Box::new); 42 | 43 | arguments.push(Node::FunctionArgument { 44 | argument_type, 45 | name, 46 | spread, 47 | reference, 48 | has_default, 49 | default_value, 50 | doc_comment, 51 | attributes, 52 | }); 53 | 54 | if parser.next_token_one_of(&[TokenType::Comma]) { 55 | parser.next(); 56 | } else { 57 | break; 58 | } 59 | } 60 | 61 | Ok(Some(arguments)) 62 | } 63 | 64 | /// Parses the argument type of a function argument 65 | /// 66 | /// # Details 67 | /// ```php 68 | /// function my_funy (/** from here **/string/** to here **/ $a, int $b): ?int { 69 | /// echo "Hello!"; 70 | /// } 71 | /// ``` 72 | pub(crate) fn argument_type(parser: &mut Parser) -> Result>> { 73 | if let Some(qm) = parser.consume_or_ignore(TokenType::QuestionMark) { 74 | Ok(Some(Box::new(Node::DataType { 75 | nullable: Some(qm), 76 | type_refs: types::non_empty_type_ref_union(parser)?, 77 | }))) 78 | } else if let Some(type_refs) = types::type_ref_union(parser)? { 79 | Ok(Some(Box::new(Node::DataType { 80 | nullable: None, 81 | type_refs, 82 | }))) 83 | } else { 84 | Ok(None) 85 | } 86 | } 87 | 88 | /// Parses the return type of a function, excluding the colon 89 | /// 90 | /// # Details 91 | /// ```php 92 | /// function my_funy (string $a, int $b): /** from here **/?int/** to here **/ { 93 | /// echo "Hello!"; 94 | /// } 95 | /// ``` 96 | pub(crate) fn return_type(parser: &mut Parser) -> Result>> { 97 | if let Some(colon) = parser.consume_or_ignore(TokenType::Colon) { 98 | Ok(Some(Box::new(Node::ReturnType { 99 | token: colon, 100 | data_type: Box::new(types::data_type(parser)?), 101 | }))) 102 | } else { 103 | Ok(None) 104 | } 105 | } 106 | 107 | /// Parses a function definition by calling methods to parse the argument list, return type and body. 108 | /// Handles named function 109 | /// 110 | /// # Details 111 | /// ```php 112 | /// /** from here **/function my_funy (string $a, int $b): void { 113 | /// echo "Hello!"; 114 | /// } 115 | /// /** to here **/ 116 | /// ``` 117 | pub(crate) fn named_function( 118 | parser: &mut Parser, 119 | doc_comment: &Option>, 120 | attributes: Vec, 121 | ) -> ExpressionResult { 122 | Ok(Node::NamedFunctionDefinitionStatement { 123 | token: parser.consume(TokenType::Function)?, 124 | by_ref: parser.consume_or_ignore(TokenType::BinaryAnd), 125 | name: parser.consume_identifier()?, 126 | function: Box::new(anonymous_function_statement(parser, doc_comment)?), 127 | attributes, 128 | }) 129 | } 130 | 131 | /// Parses a function definition by calling methods to parse the argument list, return type and body. 132 | /// It only handles anonymous functions, since the name of a named function was parses previously ... and 133 | /// a named function stripped off of the name is ... anonymous :) 134 | /// 135 | /// # Details 136 | /// ```php 137 | /// function /** from here **/ (string $a, int $b): void { 138 | /// echo "Hello!"; 139 | /// } 140 | /// /** to here **/ 141 | /// ``` 142 | pub(crate) fn anonymous_function_statement( 143 | parser: &mut Parser, 144 | doc_comment: &Option>, 145 | ) -> ExpressionResult { 146 | let op = parser.consume(TokenType::OpenParenthesis)?; 147 | let arguments = argument_list(parser, doc_comment)?; 148 | let cp = parser.consume(TokenType::CloseParenthesis)?; 149 | 150 | let return_type = return_type(parser)?; 151 | 152 | if parser.consume_or_ignore(TokenType::Semicolon).is_some() { 153 | return Ok(Node::FunctionDefinitionStatement { 154 | op, 155 | arguments, 156 | cp, 157 | return_type, 158 | body: None, 159 | doc_comment: doc_comment.clone(), 160 | }); 161 | } 162 | 163 | let body = Some(Box::new(parser.block()?)); 164 | 165 | Ok(Node::FunctionDefinitionStatement { 166 | op, 167 | arguments, 168 | cp, 169 | return_type, 170 | body, 171 | doc_comment: doc_comment.clone(), 172 | }) 173 | } 174 | 175 | pub(crate) fn arrow_function( 176 | parser: &mut Parser, 177 | is_static: Option, 178 | attributes: Vec, 179 | ) -> ExpressionResult { 180 | let token = parser.consume(TokenType::Fn)?; 181 | let by_ref = parser.consume_or_ignore(TokenType::BinaryAnd); 182 | 183 | let op = parser.consume(TokenType::OpenParenthesis)?; 184 | let arguments = argument_list(parser, &None)?; 185 | let cp = parser.consume(TokenType::CloseParenthesis)?; 186 | 187 | let return_type = return_type(parser)?; 188 | 189 | let arrow = parser.consume(TokenType::DoubleArrow)?; 190 | let body = Box::new(expressions::expression(parser, 0)?); 191 | 192 | Ok(Node::ArrowFunction { 193 | is_static, 194 | by_ref, 195 | token, 196 | op, 197 | arguments, 198 | cp, 199 | return_type, 200 | arrow, 201 | body, 202 | attributes, 203 | }) 204 | } 205 | 206 | pub(crate) fn anonymous_function( 207 | parser: &mut Parser, 208 | is_static: Option, 209 | attributes: Vec, 210 | ) -> ExpressionResult { 211 | let token = parser.consume(TokenType::Function)?; 212 | let by_ref = parser.consume_or_ignore(TokenType::BinaryAnd); 213 | 214 | let op = parser.consume(TokenType::OpenParenthesis)?; 215 | let arguments = argument_list(parser, &None)?; 216 | let cp = parser.consume(TokenType::CloseParenthesis)?; 217 | 218 | let uses = if parser.next_token_one_of(&[TokenType::Use]) { 219 | parser.next(); 220 | Some(variables::non_empty_lexical_variables_list(parser)?) 221 | } else { 222 | None 223 | }; 224 | 225 | let return_type = return_type(parser)?; 226 | 227 | let body = Box::new(parser.block()?); 228 | 229 | Ok(Node::Function { 230 | is_static, 231 | by_ref, 232 | token, 233 | op, 234 | arguments, 235 | cp, 236 | return_type, 237 | uses, 238 | body, 239 | attributes, 240 | }) 241 | } 242 | 243 | /// Parses all the parameters of a call 244 | pub(crate) fn non_empty_parameter_list(parser: &mut Parser) -> ExpressionListResult { 245 | let mut arguments = vec![expressions::expression(parser, 0)?]; 246 | 247 | parser.consume_or_ignore(TokenType::Comma); 248 | 249 | while !parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 250 | arguments.push(expressions::expression(parser, 0)?); 251 | 252 | if parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 253 | break; 254 | } else { 255 | parser.consume_or_ff_after(TokenType::Comma, &[TokenType::CloseParenthesis])?; 256 | } 257 | } 258 | 259 | Ok(arguments) 260 | } 261 | 262 | /// Parses all the arguments of a call 263 | /// Does not parse the surounding parenthesis so the caller can fetch and store them 264 | pub(crate) fn parameter_list(parser: &mut Parser) -> ExpressionListResult { 265 | let mut arguments = Vec::new(); 266 | while !parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 267 | arguments.push(expressions::expression(parser, 0)?); 268 | 269 | if parser.next_token_one_of(&[TokenType::CloseParenthesis]) { 270 | break; 271 | } else { 272 | parser.consume_or_ff_after(TokenType::Comma, &[TokenType::CloseParenthesis])?; 273 | } 274 | } 275 | Ok(arguments) 276 | } 277 | 278 | pub(crate) fn return_statement(parser: &mut Parser) -> ExpressionResult { 279 | let token = parser.consume(TokenType::Return)?; 280 | 281 | if parser.next_token_one_of(&[TokenType::Semicolon]) { 282 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 283 | Ok(Node::ReturnStatement { 284 | token, 285 | expression: None, 286 | }) 287 | } else { 288 | let value = Box::new(expressions::expression(parser, 0)?); 289 | 290 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 291 | 292 | Ok(Node::ReturnStatement { 293 | token, 294 | expression: Some(value), 295 | }) 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /src/parser/ast/namespaces.rs: -------------------------------------------------------------------------------- 1 | use super::super::node::Node; 2 | use super::super::token::{Token, TokenType}; 3 | use super::super::{Error, ExpressionListResult, ExpressionResult, Parser}; 4 | use super::types; 5 | 6 | /// Parses a single namespace statement or namespace block 7 | /// 8 | /// # Details 9 | /// ```php 10 | /// namespace /** from here **/My\Super\Duper\Namespace;/** to here **/ 11 | /// ``` 12 | pub(crate) fn namespace_statement(parser: &mut Parser) -> ExpressionResult { 13 | let token = parser.consume(TokenType::Namespace)?; 14 | let type_ref = types::type_ref(parser)?; 15 | 16 | if parser.next_token_one_of(&[TokenType::OpenCurly]) { 17 | Ok(Node::NamespaceBlock { 18 | token, 19 | type_ref, 20 | block: Box::new(parser.block()?), 21 | }) 22 | } else if let Some(type_ref) = type_ref { 23 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 24 | Ok(Node::NamespaceStatement { token, type_ref }) 25 | } else if let Some(token) = parser.next() { 26 | Err(Error::WrongTokenError { 27 | expected: vec![TokenType::OpenBrackets, TokenType::Identifier], 28 | token, 29 | }) 30 | } else { 31 | Err(Error::Eof) 32 | } 33 | } 34 | 35 | /// Parse a single import 36 | /// 37 | /// # Details 38 | /// ```php 39 | /// use Some\Other\ { 40 | /// Namespace1, 41 | /// Namespace2 as Alias, 42 | /// } 43 | /// ``` 44 | fn symbol_import(parser: &mut Parser) -> ExpressionResult { 45 | if parser.consume_or_ignore(TokenType::Function).is_some() { 46 | let name = types::non_empty_type_ref(parser)?; 47 | 48 | if let Some(alias) = parser.consume_or_ignore(TokenType::As) { 49 | return Ok(Node::UseFunction { 50 | token: None, 51 | function: Box::new(name), 52 | aliased: Some(alias), 53 | alias: Some(parser.consume(TokenType::Identifier)?), 54 | }); 55 | } else { 56 | return Ok(Node::UseFunction { 57 | token: None, 58 | function: Box::new(name), 59 | aliased: None, 60 | alias: None, 61 | }); 62 | } 63 | } 64 | 65 | if parser.consume_or_ignore(TokenType::Const).is_some() { 66 | let name = types::non_empty_type_ref(parser)?; 67 | 68 | if let Some(alias) = parser.consume_or_ignore(TokenType::As) { 69 | return Ok(Node::UseConst { 70 | token: None, 71 | constant: Box::new(name), 72 | aliased: Some(alias), 73 | alias: Some(parser.consume(TokenType::Identifier)?), 74 | }); 75 | } else { 76 | return Ok(Node::UseConst { 77 | token: None, 78 | constant: Box::new(name), 79 | aliased: None, 80 | alias: None, 81 | }); 82 | } 83 | } 84 | 85 | let name = types::non_empty_type_ref(parser)?; 86 | 87 | if let Some(alias) = parser.consume_or_ignore(TokenType::As) { 88 | Ok(Node::UseDeclaration { 89 | token: None, 90 | declaration: Box::new(name), 91 | aliased: Some(alias), 92 | alias: Some(parser.consume(TokenType::Identifier)?), 93 | }) 94 | } else { 95 | Ok(Node::UseDeclaration { 96 | token: None, 97 | declaration: Box::new(name), 98 | aliased: None, 99 | alias: None, 100 | }) 101 | } 102 | } 103 | 104 | /// Parse comma separated list of imports 105 | /// 106 | /// # Details 107 | /// ```php 108 | /// use Some\Other, Some\Else, What\Ever 109 | /// ``` 110 | fn symbol_imports(parser: &mut Parser) -> ExpressionListResult { 111 | let mut imports = vec![symbol_import(parser)?]; 112 | 113 | while parser.consume_or_ignore(TokenType::Comma).is_some() { 114 | if !parser.next_token_one_of(&[ 115 | TokenType::Identifier, 116 | TokenType::Const, 117 | TokenType::Function, 118 | ]) { 119 | break; 120 | } 121 | 122 | imports.push(symbol_import(parser)?); 123 | } 124 | 125 | Ok(imports) 126 | } 127 | 128 | /// Parses a `use function ...` statement. The `use` is passed as it was already consumed 129 | /// before consuming the `function` token in order to know what type of import 130 | /// is done 131 | pub(crate) fn use_function_statement(parser: &mut Parser, token: Token) -> ExpressionResult { 132 | let mut imports = Vec::new(); 133 | 134 | loop { 135 | let name = types::non_empty_type_ref(parser)?; 136 | 137 | if let Some(alias) = parser.consume_or_ignore(TokenType::As) { 138 | imports.push(Node::UseFunction { 139 | token: Some(token.clone()), 140 | function: Box::new(name), 141 | aliased: Some(alias), 142 | alias: Some(parser.consume(TokenType::Identifier)?), 143 | }); 144 | } else { 145 | imports.push(Node::UseFunction { 146 | token: Some(token.clone()), 147 | function: Box::new(name), 148 | aliased: None, 149 | alias: None, 150 | }); 151 | } 152 | 153 | if parser.consume_or_ignore(TokenType::Comma).is_some() { 154 | continue; 155 | } 156 | 157 | break; 158 | } 159 | 160 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 161 | 162 | Ok(Node::UseFunctionStatement { token, imports }) 163 | } 164 | 165 | /// Parses a `use const ...` statement. The `use` is passed as it was already consumed 166 | /// before consuming the `function` token in order to know what type of import 167 | /// is done 168 | pub(crate) fn use_const_statement(parser: &mut Parser, token: Token) -> ExpressionResult { 169 | let mut imports = Vec::new(); 170 | 171 | loop { 172 | let name = types::non_empty_type_ref(parser)?; 173 | 174 | if let Some(alias) = parser.consume_or_ignore(TokenType::As) { 175 | imports.push(Node::UseConst { 176 | token: Some(token.clone()), 177 | constant: Box::new(name), 178 | aliased: Some(alias), 179 | alias: Some(parser.consume(TokenType::Identifier)?), 180 | }); 181 | } else { 182 | imports.push(Node::UseConst { 183 | token: Some(token.clone()), 184 | constant: Box::new(name), 185 | aliased: None, 186 | alias: None, 187 | }); 188 | } 189 | 190 | if parser.consume_or_ignore(TokenType::Comma).is_some() { 191 | continue; 192 | } 193 | 194 | break; 195 | } 196 | 197 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 198 | 199 | Ok(Node::UseConstStatement { token, imports }) 200 | } 201 | 202 | /// Parses a `use ...` statement. The `use` is passed as it was already consumed 203 | /// before in order to know what type of import 204 | /// is done 205 | pub(crate) fn use_statement(parser: &mut Parser, token: Token) -> ExpressionResult { 206 | let mut imports = Vec::new(); 207 | 208 | loop { 209 | let declaration = types::non_empty_namespace_ref(parser)?; 210 | 211 | // Ends with \, so it should be followed by a group wrapped in curly braces 212 | if declaration.last().unwrap().t == TokenType::NamespaceSeparator { 213 | imports.push(Node::GroupedUse { 214 | token: token.clone(), 215 | parent: Box::new(Node::TypeRef(declaration.into())), 216 | oc: parser.consume(TokenType::OpenCurly)?, 217 | uses: symbol_imports(parser)?, 218 | cc: parser.consume(TokenType::CloseCurly)?, 219 | }); 220 | // Is aliased 221 | } else if let Some(aliased) = parser.consume_or_ignore(TokenType::As) { 222 | imports.push(Node::UseDeclaration { 223 | token: Some(token.clone()), 224 | declaration: Box::new(Node::TypeRef(declaration.into())), 225 | aliased: Some(aliased), 226 | alias: Some(parser.consume(TokenType::Identifier)?), 227 | }); 228 | // Is a regular use 229 | } else { 230 | imports.push(Node::UseDeclaration { 231 | token: Some(token.clone()), 232 | declaration: Box::new(Node::TypeRef(declaration.into())), 233 | aliased: None, 234 | alias: None, 235 | }); 236 | } 237 | 238 | if parser.consume_or_ignore(TokenType::Comma).is_some() { 239 | continue; 240 | } 241 | 242 | parser.consume_or_ff_after(TokenType::Semicolon, &[TokenType::Semicolon])?; 243 | 244 | break; 245 | } 246 | 247 | Ok(Node::UseStatement { token, imports }) 248 | } 249 | 250 | #[cfg(test)] 251 | mod tests { 252 | use super::*; 253 | use crate::formatter::{format_file, FormatterOptions}; 254 | use crate::parser::scanner::Scanner; 255 | 256 | #[test] 257 | fn test_parses_use_statements() { 258 | let mut scanner = Scanner::new( 259 | " ExpressionResult { 16 | let ats = parser.consume(TokenType::AttributeStart)?; 17 | 18 | let mut expressions = Vec::with_capacity(5); 19 | expressions.push(expressions::expression(parser, 0)?); 20 | while parser.consume_or_ignore(TokenType::Comma).is_some() { 21 | expressions.push(expressions::expression(parser, 0)?); 22 | } 23 | 24 | let cb = parser.consume(TokenType::CloseBrackets)?; 25 | 26 | Ok(Node::Attribute { 27 | ats, 28 | expressions, 29 | cb, 30 | }) 31 | } 32 | 33 | pub fn attributes_block(parser: &mut Parser) -> ExpressionListResult { 34 | let mut attributes = Vec::new(); 35 | while parser.next_token_one_of(&[TokenType::AttributeStart]) { 36 | attributes.push(attribute(parser)?); 37 | } 38 | 39 | Ok(attributes) 40 | } 41 | 42 | #[cfg(test)] 43 | mod tests { 44 | use crate::formatter::{format_file, FormatterOptions}; 45 | use crate::parser::scanner::Scanner; 46 | use crate::parser::Parser; 47 | 48 | #[test] 49 | fn test_parses_attributes_on_classes() { 50 | let mut scanner = Scanner::new( 51 | " 'lol';", 337 | ); 338 | scanner.scan().unwrap(); 339 | 340 | let (ast, errors) = Parser::ast(scanner.tokens).unwrap(); 341 | eprintln!("{:?}", errors); 342 | assert_eq!(true, errors.is_empty()); 343 | 344 | let options = FormatterOptions { 345 | max_line_length: 100, 346 | indent: 4, 347 | }; 348 | 349 | let formatted = format_file(&ast, 0, 0, &options); 350 | 351 | let expected = "\ 352 | $x = #[Attr('lol')] fn (string $theAttr) => 'lol'; 353 | " 354 | .to_owned(); 355 | 356 | assert_eq!(expected, formatted); 357 | } 358 | 359 | #[test] 360 | fn test_parses_attributes_of_anonymous_class() { 361 | let mut scanner = Scanner::new( 362 | ", 15 | } 16 | #[derive(Clone, Debug, Default)] 17 | pub struct SymbolImportBlock(Vec); 18 | 19 | impl SymbolImportBlock { 20 | pub fn all(&self) -> std::slice::Iter { 21 | self.0.iter() 22 | } 23 | 24 | pub fn extend(&mut self, imports: Vec) { 25 | self.0.extend(imports); 26 | } 27 | } 28 | 29 | impl From> for SymbolImportBlock { 30 | fn from(block: Vec) -> SymbolImportBlock { 31 | SymbolImportBlock(block) 32 | } 33 | } 34 | 35 | #[derive(Clone, Debug)] 36 | pub enum TraitUseAlteration { 37 | As { 38 | visibility: Option, 39 | class: Option, 40 | member: String, 41 | alias: Option, 42 | }, 43 | InsteadOf { 44 | class: TypeRef, 45 | member: String, 46 | instead_ofs: Vec, 47 | }, 48 | } 49 | 50 | impl SymbolImport { 51 | pub fn name(&self) -> String { 52 | if let Some(alias) = &self.alias { 53 | alias.label.clone().unwrap() 54 | } else if let Some(tip) = self.path.tip() { 55 | tip.to_owned() 56 | } else { 57 | String::from("") 58 | } 59 | } 60 | 61 | pub fn full_name(&self) -> String { 62 | self.path.to_fqdn() 63 | } 64 | } 65 | 66 | impl From<&SymbolImport> for Symbol { 67 | fn from(symbol_import: &SymbolImport) -> Symbol { 68 | let start = symbol_import.path.range(); 69 | 70 | let range = if let Some(alias) = symbol_import.alias.as_ref() { 71 | NodeRange::from_range(&start, &alias.into()) 72 | } else { 73 | NodeRange::from_range(&start, &symbol_import.path.range()) 74 | }; 75 | 76 | let range = get_range(range); 77 | Symbol { 78 | name: symbol_import.full_name(), 79 | kind: PhpSymbolKind::Import, 80 | range, 81 | selection_range: range, 82 | ..Symbol::default() 83 | } 84 | } 85 | } 86 | 87 | pub fn collect_alterations(node: &Node) -> Vec { 88 | let mut rules = Vec::new(); 89 | match node { 90 | Node::UseTraitAlterationBlock { alterations, .. } => { 91 | // Aight, this section should output something like 92 | // - traits: A, B, C, D 93 | // - alterations / rules / modifiers: 94 | // - A::rofl instead of C, B <- check if C and B have a rofl, if a has a rofl and if D has no rofl 95 | // - rofl as private <- check if there is a rofl at all and if its only in one of the A, B, C or D 96 | // 97 | // Overall check if: 98 | // - no conflicts left unresolved 99 | 100 | for alteration in alterations { 101 | match alteration { 102 | Node::UseTraitAs { 103 | left, 104 | member, 105 | visibility, 106 | as_name, 107 | .. 108 | } => { 109 | let class = if let Some(Node::TypeRef(tr)) = left.as_deref() { 110 | Some(tr.clone()) 111 | } else { 112 | None 113 | }; 114 | 115 | rules.push(TraitUseAlteration::As { 116 | class, 117 | visibility: visibility.clone(), 118 | alias: as_name.clone(), 119 | member: member.name(), 120 | }); 121 | } 122 | Node::UseTraitInsteadOf { 123 | left, 124 | member, 125 | insteadof_list, 126 | .. 127 | } => { 128 | let class = if let Some(Node::TypeRef(tr)) = left.as_deref() { 129 | tr.clone() 130 | } else { 131 | // TODO: There must always be a class:: ... rewrite parser to enforce it 132 | continue; 133 | }; 134 | let instead_ofs = insteadof_list 135 | .iter() 136 | .map(|tr| match tr { 137 | Node::TypeRef(tr) => tr.clone(), 138 | _ => unreachable!("Impossibru"), 139 | }) 140 | .collect(); 141 | rules.push(TraitUseAlteration::InsteadOf { 142 | class, 143 | member: member.name(), 144 | instead_ofs, 145 | }); 146 | } 147 | _ => (), 148 | } 149 | } 150 | } 151 | Node::UseTraitStatement { traits_usages, .. } => { 152 | traits_usages 153 | .iter() 154 | .for_each(|n| rules.extend(collect_alterations(n))); 155 | } 156 | _ => (), 157 | } 158 | 159 | rules 160 | } 161 | 162 | /// Collect symbol imports underneath the current node 163 | pub fn collect_uses(node: &Node, prefix: &TypeRef) -> Vec { 164 | let mut collected_uses = Vec::new(); 165 | 166 | match node { 167 | Node::UseTraitAlterationBlock { 168 | alteration_group_type_refs, 169 | .. 170 | } => { 171 | alteration_group_type_refs 172 | .iter() 173 | .for_each(|n| collected_uses.extend(collect_uses(n, prefix))); 174 | } 175 | Node::UseTraitStatement { traits_usages, .. } => { 176 | traits_usages 177 | .iter() 178 | .for_each(|n| collected_uses.extend(collect_uses(n, prefix))); 179 | } 180 | Node::UseTrait { type_ref } => { 181 | collected_uses.extend(collect_uses(type_ref, prefix)); 182 | } 183 | 184 | Node::UseStatement { imports, .. } => { 185 | imports 186 | .iter() 187 | .for_each(|n| collected_uses.extend(collect_uses(n, prefix))); 188 | } 189 | Node::UseFunctionStatement { imports, .. } => { 190 | imports 191 | .iter() 192 | .for_each(|n| collected_uses.extend(collect_uses(n, prefix))); 193 | } 194 | Node::UseConstStatement { imports, .. } => { 195 | imports 196 | .iter() 197 | .for_each(|n| collected_uses.extend(collect_uses(n, prefix))); 198 | } 199 | Node::GroupedUse { parent, uses, .. } => { 200 | uses.iter().for_each(|n| { 201 | collected_uses.extend(collect_uses(n, &collect_uses(parent, prefix)[0].path)) 202 | }); 203 | } 204 | Node::UseDeclaration { 205 | declaration, alias, .. 206 | } => { 207 | let import = &collect_uses(declaration, prefix)[0]; 208 | 209 | collected_uses.push(SymbolImport { 210 | alias: alias.clone(), 211 | ..import.clone() 212 | }); 213 | } 214 | Node::UseFunction { 215 | function, alias, .. 216 | } => { 217 | let import = &collect_uses(function, prefix)[0]; 218 | 219 | collected_uses.push(SymbolImport { 220 | alias: alias.clone(), 221 | ..import.clone() 222 | }); 223 | } 224 | Node::UseConst { 225 | constant, alias, .. 226 | } => { 227 | let import = &collect_uses(constant, prefix)[0]; 228 | 229 | collected_uses.push(SymbolImport { 230 | alias: alias.clone(), 231 | ..import.clone() 232 | }); 233 | } 234 | Node::TypeRef(tokens) => { 235 | collected_uses.push(SymbolImport { 236 | path: TypeRef::append(prefix, tokens), 237 | alias: None, 238 | }); 239 | } 240 | _ => {} 241 | } 242 | 243 | collected_uses 244 | } 245 | 246 | #[cfg(test)] 247 | mod tests { 248 | use super::*; 249 | use crate::parser::token::{Token, TokenType}; 250 | 251 | #[test] 252 | fn test_collects_use_statement() { 253 | let use_statement = Node::UseStatement { 254 | token: Token::new(TokenType::Use, 1, 1, 0), 255 | imports: vec![Node::UseDeclaration { 256 | token: Some(Token::new(TokenType::Use, 1, 1, 0)), 257 | declaration: Box::new(Node::TypeRef( 258 | vec![Token::named( 259 | TokenType::Identifier, 260 | 1, 261 | 1, 262 | 0, 263 | "IncludedSymbol", 264 | )] 265 | .into(), 266 | )), 267 | aliased: None, 268 | alias: None, 269 | }], 270 | }; 271 | 272 | let expected = SymbolImport { 273 | path: vec![Token { 274 | col: 1, 275 | line: 1, 276 | t: TokenType::Identifier, 277 | label: Some("IncludedSymbol".to_owned()), 278 | offset: Some(0), 279 | }] 280 | .into(), 281 | alias: None, 282 | ..SymbolImport::default() 283 | }; 284 | assert_eq!(expected, collect_uses(&use_statement, &vec![].into())[0]); 285 | } 286 | 287 | #[test] 288 | fn test_collects_use_trait() { 289 | let trait_use = Node::UseTraitStatement { 290 | token: Token::new(TokenType::Use, 1, 1, 0), 291 | traits_usages: vec![Node::UseTrait { 292 | type_ref: Box::new(Node::TypeRef( 293 | vec![Token::named( 294 | TokenType::Identifier, 295 | 1, 296 | 1, 297 | 0, 298 | "IncludedSymbol", 299 | )] 300 | .into(), 301 | )), 302 | }], 303 | }; 304 | 305 | let expected = SymbolImport { 306 | path: vec![Token { 307 | col: 1, 308 | line: 1, 309 | t: TokenType::Identifier, 310 | label: Some("IncludedSymbol".to_owned()), 311 | offset: Some(0), 312 | }] 313 | .into(), 314 | alias: None, 315 | ..SymbolImport::default() 316 | }; 317 | assert_eq!(expected, collect_uses(&trait_use, &vec![].into())[0]); 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /src/formatter/v2.rs: -------------------------------------------------------------------------------- 1 | //! Based on http://journal.stuffwithstuff.com/2015/09/08/the-hardest-program-ive-ever-written 2 | 3 | use crate::{ 4 | formatter::{ 5 | classes::class_stmt_to_spans, 6 | expressions::{binary_to_spans, expression_stmt_to_spans}, 7 | loops, 8 | }, 9 | parser::{ 10 | node::Node, 11 | token::{Token, TokenType}, 12 | }, 13 | }; 14 | use loops::while_to_spans; 15 | use std::fmt::{Debug, Display, Formatter, Result}; 16 | 17 | // A chunk represents a set of token that will never ever split. 18 | #[derive(Clone, Debug)] 19 | pub(crate) struct Chunk { 20 | tokens: Vec, 21 | spaced: bool, 22 | space_after: bool, 23 | } 24 | 25 | impl Display for Chunk { 26 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 27 | let len = self.tokens.len(); 28 | 29 | for (i, token) in self.tokens.iter().enumerate() { 30 | let is_last = i == len - 1; 31 | if token.t == TokenType::Linebreak { 32 | continue; 33 | } 34 | 35 | // Infix operators need a space to the operand 36 | if token.t.is_infix_operator() 37 | || !is_last && (self.spaced || token.t == TokenType::Semicolon) 38 | || self.space_after 39 | { 40 | write!(f, "{} ", token)?; 41 | 42 | continue; 43 | } 44 | 45 | write!(f, "{}", token)?; 46 | } 47 | 48 | Ok(()) 49 | } 50 | } 51 | 52 | impl Chunk { 53 | pub fn new(tokens: &[Token]) -> Self { 54 | Self { 55 | tokens: tokens.into(), 56 | spaced: true, 57 | space_after: false, 58 | } 59 | } 60 | 61 | pub fn single(token: Token) -> Self { 62 | Self { 63 | tokens: vec![token], 64 | spaced: false, 65 | space_after: false, 66 | } 67 | } 68 | 69 | pub fn unspaced(tokens: &[Token]) -> Self { 70 | Self { 71 | tokens: tokens.into(), 72 | spaced: false, 73 | space_after: false, 74 | } 75 | } 76 | 77 | pub fn with_space_after(self) -> Self { 78 | Self { 79 | space_after: true, 80 | ..self 81 | } 82 | } 83 | 84 | // Return the offset of the right most token 85 | pub fn right_offset(&self) -> usize { 86 | self.tokens.last().unwrap().offset.unwrap() 87 | } 88 | 89 | // Return the offset of the left most token 90 | pub fn left_offset(&self) -> usize { 91 | self.tokens.first().unwrap().offset.unwrap() 92 | } 93 | } 94 | 95 | // A span is a set of chunks. 96 | // Spans may split but should rather not. There can be multiple spans 97 | // for a series of chunks. One span for each combination of breaks 98 | // we want to offer. They are generated on order of preference, starting 99 | // with the longest. All chunks in a span shall we written on the same 100 | // line with a following newline. For example, for the following snippet: 101 | // 102 | // foreach ($someCollection as $someVar) { 103 | // 104 | // we would generate the following sets of spans: 105 | // 1 span: [foreach ($someCollection as $someVar) {] 106 | // 3 spans: [foreach (,], [$someCollection as $someVar], [) {] 107 | #[derive(Clone)] 108 | pub(crate) struct Span { 109 | pub(crate) chunks: Vec, 110 | 111 | // Subspans this span may be broken down into 112 | pub(crate) spans: Vec, 113 | 114 | // Indentation level 115 | pub(crate) lvl: u8, 116 | 117 | pub(crate) spaced: bool, 118 | } 119 | 120 | impl Display for Span { 121 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 122 | write!( 123 | f, 124 | "{}{}", 125 | " ".repeat(self.lvl as usize * 4), 126 | self.chunks 127 | .iter() 128 | .map(std::string::ToString::to_string) 129 | .collect::>() 130 | .join(if self.spaced { " " } else { "" }) 131 | ) 132 | } 133 | } 134 | 135 | impl Debug for Span { 136 | fn fmt(&self, f: &mut Formatter<'_>) -> Result { 137 | f.debug_struct("Span") 138 | .field("lvl", &self.lvl) 139 | //.field("spaced", &self.spaced) 140 | .field( 141 | "chunks", 142 | &self 143 | .chunks 144 | .iter() 145 | .map(std::string::ToString::to_string) 146 | .collect::>() 147 | .join(if self.spaced { " " } else { "" }), 148 | ) 149 | .field("spans", &self.spans) 150 | .finish() 151 | } 152 | } 153 | 154 | impl Span { 155 | pub(crate) fn new(chunks: Vec, spans: Vec, lvl: u8) -> Self { 156 | Span { 157 | chunks, 158 | spans, 159 | lvl, 160 | spaced: true, 161 | } 162 | } 163 | 164 | pub(crate) fn unspaced(chunks: Vec, spans: Vec, lvl: u8) -> Self { 165 | Span { 166 | chunks, 167 | spans, 168 | lvl, 169 | spaced: false, 170 | } 171 | } 172 | 173 | pub(crate) fn leaf(chunks: Vec, lvl: u8) -> Self { 174 | Span { 175 | chunks, 176 | spans: vec![], 177 | lvl, 178 | spaced: false, 179 | } 180 | } 181 | 182 | // Return the offset of the right most token 183 | pub fn right_offset(&self) -> usize { 184 | self.chunks.last().unwrap().right_offset() 185 | } 186 | 187 | // Return the offset of the right most token 188 | pub fn left_offset(&self) -> usize { 189 | self.chunks.first().unwrap().left_offset() 190 | } 191 | 192 | // Extend all last chunks in all last spans recursively with the provided 193 | // chunk. 194 | // Usually it contains the ; at the end of a statement and maybe 195 | // also a comment at the end of the line. 196 | pub fn right_extend(&mut self, chunk: Chunk) { 197 | self.chunks 198 | .last_mut() 199 | .unwrap() 200 | .tokens 201 | .extend(chunk.tokens.iter().cloned()); 202 | 203 | if !self.spans.is_empty() { 204 | self.spans.last_mut().unwrap().right_extend(chunk); 205 | } 206 | } 207 | 208 | // Extend all first chunks in all first spans recursively with the provided 209 | // chunk 210 | // Usually it contains the operator of a binary assignment and 211 | // also a comment at the start of the line. 212 | pub fn left_extend(&mut self, chunk: Chunk) { 213 | self.chunks 214 | .first_mut() 215 | .unwrap() 216 | .tokens 217 | .splice(0..0, chunk.tokens.iter().cloned()); 218 | 219 | if !self.spans.is_empty() { 220 | self.spans.first_mut().unwrap().left_extend(chunk); 221 | } 222 | } 223 | } 224 | 225 | // Returns the offset of the next token that matches a predicate 226 | pub(crate) fn next_that(from: usize, tokens: &[Token], f: &dyn Fn(&Token) -> bool) -> usize { 227 | tokens[from + 1..] 228 | .iter() 229 | .find(|t| f(t)) 230 | .unwrap() 231 | .offset 232 | .unwrap() 233 | } 234 | 235 | // Returns the offset of the next token that matches a predicate 236 | pub(crate) fn prev_that(from: usize, tokens: &[Token], f: &dyn Fn(&Token) -> bool) -> usize { 237 | tokens[..from] 238 | .iter() 239 | .rev() 240 | .find(|t| f(t)) 241 | .unwrap() 242 | .offset 243 | .unwrap() 244 | } 245 | 246 | // Collect all chunks between the statement and the previous one. It basically 247 | // returns a vector of chunks, each containing a multiline comment 248 | pub(crate) fn pre_statement_span(token_offset: usize, tokens: &[Token], lvl: u8) -> Span { 249 | let prev_non_comment = prev_that(token_offset, tokens, &|t| { 250 | t.t != TokenType::MultilineComment 251 | }); 252 | 253 | return Span::new( 254 | tokens[prev_non_comment + 1..token_offset] 255 | .iter() 256 | .cloned() 257 | .map(Chunk::single) 258 | .collect(), 259 | vec![], 260 | lvl, 261 | ); 262 | } 263 | 264 | /// Turn the ast recursivly into a set of spansets. This requires reading the 265 | /// actual tokens from the tokenstream, as the tokenstream also contains comments! 266 | /// At the same time we need to traverse via the ast to know the context of 267 | /// the tokens as well as the current indentation level. 268 | pub(crate) fn ast_to_spans( 269 | ast: &[Node], 270 | stream: &[Token], 271 | lvl: u8, 272 | mut prev_right_offset: usize, 273 | next_left_offset: usize, 274 | ) -> Vec { 275 | let mut spans = Vec::new(); 276 | 277 | for node in ast { 278 | let mut node_spans = node_to_spans(node, stream, lvl); 279 | 280 | // Get all comments between the previously read node and this node and add them 281 | // between the previously read node and this node in the span collection. 282 | if let Some(first) = node_spans.first() { 283 | let cur_left_offset = first.left_offset(); 284 | 285 | let mut tokens_in_between = &stream[prev_right_offset + 1..cur_left_offset]; 286 | 287 | if tokens_in_between.len() > 1 { 288 | // Cut off leading newline from previous statement 289 | if tokens_in_between[0].t == TokenType::Linebreak { 290 | tokens_in_between = &tokens_in_between[1..]; 291 | } 292 | 293 | let len = tokens_in_between.len(); 294 | if tokens_in_between[len - 1].t == TokenType::Linebreak { 295 | tokens_in_between = &tokens_in_between[..len - 1]; 296 | } 297 | 298 | // Split everything in between the statements into one span per line 299 | spans.extend( 300 | tokens_in_between 301 | .split(|t| t.t == TokenType::Linebreak) 302 | .map(Chunk::new) 303 | .map(|c| Span::leaf(vec![c], lvl)), 304 | ); 305 | } 306 | } 307 | prev_right_offset = node_spans.last().unwrap().right_offset(); 308 | 309 | spans.append(&mut node_spans); 310 | } 311 | 312 | // Finally, get the tokens between the last read node and the delimiter of the block 313 | // whose offset was passed in 314 | let tokens_in_between = &stream[prev_right_offset + 1..next_left_offset]; 315 | let chunks_in_between = tokens_in_between 316 | .split(|t| t.t == TokenType::Linebreak) 317 | .map(Chunk::new) 318 | .collect(); 319 | spans.push(Span::leaf(chunks_in_between, lvl)); 320 | 321 | spans 322 | } 323 | 324 | /// Convert a single node into a vec of spans 325 | pub(crate) fn node_to_spans(node: &Node, tokens: &[Token], lvl: u8) -> Vec { 326 | // Get all tokens that encompass the node 327 | 328 | let mut spans = Vec::new(); 329 | 330 | match node { 331 | Node::WhileStatement { 332 | token, 333 | op, 334 | cp, 335 | condition, 336 | body, 337 | } => while_to_spans(&mut spans, tokens, token, op, cp, condition, body, lvl), 338 | Node::Literal(token) => { 339 | spans.push(Span::leaf(vec![Chunk::unspaced(&[token.clone()])], lvl)) 340 | } 341 | Node::Binary { left, right, .. } => binary_to_spans(&mut spans, tokens, left, right, lvl), 342 | Node::Variable(token) => { 343 | spans.push(Span::leaf(vec![Chunk::unspaced(&[token.clone()])], lvl)); 344 | } 345 | Node::ExpressionStatement { expression } => { 346 | expression_stmt_to_spans(&mut spans, tokens, expression, lvl) 347 | } 348 | Node::Block { 349 | oc, cc, statements, .. 350 | } => { 351 | // Blocks never take care of formatting the opening curly, because the blocks do 352 | // not know if its supposed to be on a new line or on the previous line 353 | spans.extend(ast_to_spans( 354 | statements, 355 | tokens, 356 | lvl, 357 | oc.offset.unwrap(), 358 | cc.offset.unwrap(), 359 | )); 360 | 361 | let cc_offset = cc.offset.unwrap(); 362 | let start_of_next = next_that(cc_offset, tokens, &|t| !t.is_comment()); 363 | 364 | // Add a chunk that contains the close curly plus all the comments up to the next 365 | // siginificant bit 366 | let chunks = vec![Chunk::new(&tokens[cc_offset..start_of_next])]; 367 | spans.push(Span::new(chunks, vec![], lvl - 1)); 368 | } 369 | Node::ClassStatement(stmt) => class_stmt_to_spans(&mut spans, tokens, stmt, lvl), 370 | _ => unimplemented!("{:#?}", node), 371 | } 372 | 373 | spans 374 | } 375 | 376 | #[cfg(test)] 377 | pub(crate) mod test { 378 | use super::*; 379 | use crate::parser::scanner::Scanner; 380 | use crate::parser::Parser; 381 | 382 | pub(crate) fn ast(source: &str) -> (Vec, Vec) { 383 | let mut scanner = Scanner::new(&format!(" ExpressionResult { 7 | let value = expression(parser, 0)?; 8 | 9 | Ok(Node::ExpressionStatement { 10 | expression: Box::new(value), 11 | }) 12 | } 13 | 14 | /// Parses an expression. This can be anything that evaluates to a value. A function call, a comparison or even an assignment 15 | 16 | /// Pratt parser which is heavily inspired by this awesome 17 | /// blogpost: https://matklad.github.io/2020/04/13/simple-but-powerful-pratt-parsing.html 18 | pub(crate) fn expression(parser: &mut Parser, min_bp: u8) -> ExpressionResult { 19 | // Start with the first left hand side, which can either be an operator 20 | // or an operand 21 | let mut lhs = if let Some(token) = parser.peek() { 22 | if let Some(rb) = token.t.prefix_binding_power() { 23 | let token = parser.next().unwrap(); 24 | let rhs = expression(parser, rb)?; 25 | Node::Unary { 26 | token, 27 | expr: Box::new(rhs), 28 | } 29 | // Also no parenthesis, so its an operand 30 | } else { 31 | calls::call(parser)? 32 | } 33 | } else { 34 | return Err(Error::Eof); 35 | }; 36 | 37 | while parser.next_is_operator() { 38 | let op = parser.peek().unwrap(); 39 | 40 | // is the next one a postfix operator? 41 | if let Some(lb) = op.t.postfix_binding_power() { 42 | if lb < min_bp { 43 | break; 44 | } 45 | 46 | let token = parser.next().unwrap(); 47 | 48 | lhs = Node::PostUnary { 49 | token, 50 | expr: Box::new(lhs), 51 | }; 52 | 53 | continue; 54 | } 55 | 56 | // If not, is it an infix operator? 57 | if let Some((lb, rb)) = op.t.infix_binding_power() { 58 | if lb < min_bp { 59 | break; 60 | } 61 | 62 | let op = parser.next().unwrap(); 63 | 64 | lhs = if op.t == TokenType::QuestionMark { 65 | if let Some(colon) = parser.consume_or_ignore(TokenType::Colon) { 66 | let rhs = expression(parser, rb)?; 67 | Node::Ternary { 68 | check: Box::new(lhs), 69 | true_arm: None, 70 | colon, 71 | qm: op, 72 | false_arm: Box::new(rhs), 73 | } 74 | } else { 75 | let mhs = expression(parser, 0)?; 76 | let colon = parser.consume(TokenType::Colon)?; 77 | let rhs = expression(parser, rb)?; 78 | Node::Ternary { 79 | check: Box::new(lhs), 80 | true_arm: Some(Box::new(mhs)), 81 | colon, 82 | qm: op, 83 | false_arm: Box::new(rhs), 84 | } 85 | } 86 | } else { 87 | let rhs = expression(parser, rb)?; 88 | Node::Binary { 89 | token: op, 90 | left: Box::new(lhs), 91 | right: Box::new(rhs), 92 | } 93 | }; 94 | 95 | continue; 96 | } 97 | 98 | // If its neither a postfix nor an infix operator we are done. 99 | break; 100 | } 101 | 102 | Ok(lhs) 103 | } 104 | 105 | pub(crate) fn primary(parser: &mut Parser) -> ExpressionResult { 106 | if parser.next_token_one_of(&[ 107 | TokenType::False, 108 | TokenType::True, 109 | TokenType::Null, 110 | TokenType::LongNumber, 111 | TokenType::DecimalNumber, 112 | TokenType::ExponentialNumber, 113 | TokenType::HexNumber, 114 | TokenType::BinaryNumber, 115 | TokenType::ConstantEncapsedString, 116 | TokenType::EncapsedAndWhitespaceString, 117 | TokenType::ShellEscape, 118 | TokenType::ConstDir, 119 | TokenType::ConstFile, 120 | TokenType::ConstFunction, 121 | TokenType::ConstLine, 122 | TokenType::ConstMethod, 123 | TokenType::ConstTrait, 124 | TokenType::ConstClass, 125 | TokenType::ConstNan, 126 | TokenType::ConstInf, 127 | ]) { 128 | return Ok(Node::Literal(parser.next().unwrap())); 129 | } 130 | 131 | if parser.next_token_one_of(&[TokenType::Variable]) { 132 | return variables::variable(parser); 133 | } 134 | 135 | if parser.next_token_one_of(&[TokenType::NamespaceSeparator, TokenType::Identifier]) { 136 | // Load path as identifier 137 | return types::non_empty_type_ref(parser); 138 | } 139 | 140 | if parser.next_token_one_of(&[TokenType::HereDocStart]) { 141 | parser.consume_or_ff_after(TokenType::HereDocStart, &[TokenType::HereDocStart])?; 142 | let string = parser.next().unwrap(); 143 | parser.consume_or_ff_after(TokenType::HereDocEnd, &[TokenType::HereDocEnd])?; 144 | 145 | return Ok(Node::Literal(string)); 146 | } 147 | 148 | if let Some(isset) = parser.consume_or_ignore(TokenType::Isset) { 149 | return Ok(Node::Isset { 150 | isset, 151 | op: parser.consume(TokenType::OpenParenthesis)?, 152 | parameters: functions::non_empty_parameter_list(parser)?, 153 | cp: parser.consume(TokenType::CloseParenthesis)?, 154 | }); 155 | } 156 | 157 | if let Some(exit) = parser.consume_or_ignore(TokenType::Exit) { 158 | if let Some(op) = parser.consume_or_ignore(TokenType::OpenParenthesis) { 159 | return Ok(Node::Exit { 160 | exit, 161 | op: Some(op), 162 | parameters: Some(functions::parameter_list(parser)?), 163 | cp: Some(parser.consume(TokenType::CloseParenthesis)?), 164 | }); 165 | } 166 | 167 | return Ok(Node::Exit { 168 | exit, 169 | op: None, 170 | parameters: None, 171 | cp: None, 172 | }); 173 | } 174 | 175 | if let Some(empty) = parser.consume_or_ignore(TokenType::Empty) { 176 | return Ok(Node::Empty { 177 | empty, 178 | op: parser.consume(TokenType::OpenParenthesis)?, 179 | parameters: functions::non_empty_parameter_list(parser)?, 180 | cp: parser.consume(TokenType::CloseParenthesis)?, 181 | }); 182 | } 183 | 184 | if parser.next_token_one_of(&[TokenType::TypeArray]) { 185 | return arrays::old_array(parser); 186 | } 187 | 188 | if parser.next_token_one_of(&[TokenType::List]) { 189 | return keywords::list(parser); 190 | } 191 | 192 | if let Some(include) = parser.consume_one_of_or_ignore(&[ 193 | TokenType::Require, 194 | TokenType::RequireOnce, 195 | TokenType::Include, 196 | TokenType::IncludeOnce, 197 | ]) { 198 | return Ok(Node::FileInclude { 199 | token: include, 200 | resource: Box::new(expression(parser, 0)?), 201 | }); 202 | } 203 | 204 | if parser.next_token_one_of(&[TokenType::OpenBrackets]) { 205 | let expr = arrays::array(parser)?; 206 | 207 | return Ok(expr); 208 | } 209 | 210 | if parser.next_token_one_of(&[TokenType::OpenParenthesis]) { 211 | parser.consume_or_ff_after(TokenType::OpenParenthesis, &[TokenType::OpenParenthesis])?; 212 | let expr = expression(parser, 0)?; 213 | parser.consume_or_ff_after(TokenType::CloseParenthesis, &[TokenType::Semicolon])?; 214 | 215 | return Ok(Node::Grouping(Box::new(expr))); 216 | } 217 | 218 | let attributes = attributes_block(parser)?; 219 | 220 | if parser.next_token_one_of(&[TokenType::Fn]) { 221 | return functions::arrow_function(parser, None, attributes); 222 | } 223 | 224 | if parser.next_token_one_of(&[TokenType::Function]) { 225 | return functions::anonymous_function(parser, None, attributes); 226 | } 227 | 228 | // Static is fun ... watch this ... 229 | if let Some(static_token) = parser.consume_or_ignore(TokenType::Static) { 230 | // Followed by ::? Probably a member access 231 | if parser.next_token_one_of(&[TokenType::PaamayimNekudayim]) { 232 | return Ok(Node::Literal(static_token)); 233 | } 234 | 235 | // Followed by "function"? Static function expression 236 | if parser.next_token_one_of(&[TokenType::Function]) { 237 | return functions::anonymous_function(parser, Some(static_token), attributes); 238 | } 239 | 240 | if parser.next_token_one_of(&[TokenType::Fn]) { 241 | return functions::arrow_function(parser, Some(static_token), attributes); 242 | } 243 | 244 | // Otherwise probably used in a instantiation context 245 | return Ok(Node::Literal(static_token)); 246 | } 247 | 248 | // self is like static but less mighty 249 | if let Some(parser_token) = 250 | parser.consume_one_of_or_ignore(&[TokenType::TypeSelf, TokenType::Parent]) 251 | { 252 | // Followed by ::? Probably a member access 253 | if parser.next_token_one_of(&[TokenType::PaamayimNekudayim]) { 254 | return Ok(Node::Literal(parser_token)); 255 | } 256 | 257 | // Otherwise ... no clue if an error after all. Need to check official grammar 258 | return Ok(Node::Literal(parser_token)); 259 | } 260 | 261 | if parser.next_token_one_of(&[TokenType::Class]) { 262 | return classes::anonymous_class(parser, attributes); 263 | } 264 | 265 | if let Some(new) = parser.consume_or_ignore(TokenType::New) { 266 | return Ok(Node::New { 267 | token: new, 268 | class: Box::new(calls::call(parser)?), 269 | }); 270 | } 271 | 272 | if let Some(token) = parser.consume_or_ignore(TokenType::Yield) { 273 | if parser.next_token_one_of(&[TokenType::Semicolon]) { 274 | return Ok(Node::Yield { token, expr: None }); 275 | } 276 | 277 | return Ok(Node::Yield { 278 | token, 279 | expr: Some(Box::new(arrays::array_pair(parser)?)), 280 | }); 281 | } 282 | 283 | if let Some(from) = parser.consume_or_ignore(TokenType::YieldFrom) { 284 | return Ok(Node::YieldFrom { 285 | token: from, 286 | expr: Box::new(expression(parser, 0)?), 287 | }); 288 | } 289 | 290 | if let Some(mtch) = parser.consume_or_ignore(TokenType::Match) { 291 | return Ok(Node::Match { 292 | mtch, 293 | op: parser.consume(TokenType::OpenParenthesis)?, 294 | condition: Box::new(expression(parser, 0)?), 295 | cp: parser.consume(TokenType::CloseParenthesis)?, 296 | oc: parser.consume(TokenType::OpenCurly)?, 297 | body: conditionals::match_body(parser)?, 298 | cc: parser.consume(TokenType::CloseCurly)?, 299 | }); 300 | } 301 | 302 | if let Some(next) = parser.next() { 303 | // Maybe some sort of other identifier? 304 | if next.is_identifier() { 305 | return Ok(Node::Literal(next)); 306 | } else { 307 | return Err(Error::UnexpectedTokenError { token: next }); 308 | } 309 | } 310 | 311 | Err(Error::Eof {}) 312 | } 313 | 314 | #[cfg(test)] 315 | mod tests { 316 | use super::expression; 317 | use crate::parser::scanner::Scanner; 318 | use crate::parser::Parser; 319 | 320 | #[test] 321 | fn test_parses_basic_math() { 322 | let code = ">()); 327 | 328 | dbg!(expression(&mut parser, 0).unwrap()); 329 | } 330 | 331 | #[test] 332 | fn test_parses_grouping() { 333 | let code = ">()); 338 | 339 | dbg!(expression(&mut parser, 0).unwrap()); 340 | } 341 | 342 | #[test] 343 | fn test_parses_assignment_chains() { 344 | let code = ">()); 349 | 350 | dbg!(expression(&mut parser, 0).unwrap()); 351 | } 352 | 353 | #[test] 354 | fn test_parses_the_ternary() { 355 | let code = ">()); 360 | 361 | dbg!(expression(&mut parser, 0).unwrap()); 362 | } 363 | 364 | #[test] 365 | fn test_parses_match_expression() { 366 | let code = " 3, 4, 5 => callme(), caller() => called() };"; 367 | let mut scanner = Scanner::new(code); 368 | let tokens = scanner.scan().unwrap(); 369 | let mut parser = Parser::new(tokens.iter().skip(1).map(|t| t.clone()).collect::>()); 370 | 371 | dbg!(expression(&mut parser, 0).unwrap()); 372 | } 373 | } 374 | --------------------------------------------------------------------------------