дзеркало
https://github.com/l0ng-ai/openidea.git
синхронізовано 2026-11-09 03:05:40 +02:00
Intelephense checked PHP code against its default 8.5, whatever version the project targets. Settings › Languages & Frameworks › PHP gets a PHP language level, Auto (default: composer.json config.platform.php, else the lowest version require.php allows) or 5.6–8.5, sent as environment.phpVersion; nothing is sent without a requirement. Intelephense applies the version only to documents it parses after reading its configuration, so servers that opt in (resync-on-configuration, Intelephense only) get their open documents again right after workspace/configuration is answered (new jsonrpc Handler::after_response), and Intelephense keeps one index per level because it fixes the built-in functions when it indexes. A change to a root or workspace member composer.json reconfigures the servers. Co-authored-by: DanielADK <[email protected]>
211 рядки
9.2 KiB
Rust
211 рядки
9.2 KiB
Rust
//! The PHP language level: the one chosen in Settings › PHP, else the one the root
|
||
//! `composer.json` declares (`config.platform.php`, else the lowest version `require.php` allows);
|
||
//! and the Intelephense storage that goes with it.
|
||
|
||
use crate::apply::{merge, merge_init_options};
|
||
use ide_core::settings::Settings;
|
||
use ide_language::LanguageServerConfig;
|
||
use serde_json::{Value, json};
|
||
use std::path::Path;
|
||
|
||
/// The project's PHP version as `X.Y.Z`; `None` when neither the settings nor
|
||
/// `composer.json` name one.
|
||
pub fn language_level(root: &Path, s: &Settings) -> Option<String> {
|
||
let explicit = s.languages.php.language_level.trim();
|
||
if !explicit.is_empty() {
|
||
return version(explicit).map(dotted);
|
||
}
|
||
let text = std::fs::read_to_string(root.join("composer.json")).ok()?;
|
||
let composer: Value = serde_json::from_str(&text).ok()?;
|
||
let platform = composer.pointer("/config/platform/php").and_then(Value::as_str).and_then(version);
|
||
platform.or_else(|| lowest_allowed(composer.pointer("/require/php")?.as_str()?)).map(dotted)
|
||
}
|
||
|
||
/// Gives each PHP version its own Intelephense workspace storage (below `data_dir`).
|
||
/// Intelephense keeps the built-in functions of the version a workspace was first indexed
|
||
/// with in that storage, and reuses them after the version changes. Without a version, or
|
||
/// with a `storagePath` of the user's, the server keeps its own storage.
|
||
pub fn configure_intelephense_storage(cfg: &mut LanguageServerConfig, data_dir: &Path) {
|
||
if cfg.name != "intelephense" {
|
||
return;
|
||
}
|
||
let mut options =
|
||
cfg.initialization_options.as_ref().and_then(|v| serde_json::to_value(v).ok()).unwrap_or(Value::Null);
|
||
if options.get("storagePath").is_some() {
|
||
return;
|
||
}
|
||
// The version Intelephense reads: `workspace/configuration` is answered from the
|
||
// settings merged over the initialization options (`ide_lsp::handler::all_settings`).
|
||
if let Some(settings) = &cfg.settings {
|
||
merge(&mut options, settings.clone());
|
||
}
|
||
let section = options.pointer("/settings/intelephense").unwrap_or(&options);
|
||
let version = section.pointer("/environment/phpVersion").and_then(Value::as_str);
|
||
let Some(version) = version.filter(|v| !v.is_empty() && v.chars().all(|c| c.is_ascii_digit() || c == '.')) else {
|
||
return;
|
||
};
|
||
let storage = data_dir.join("lsp").join("intelephense").join(version);
|
||
merge_init_options(cfg, json!({ "storagePath": storage.to_string_lossy() }));
|
||
}
|
||
|
||
type Version = (u32, u32, u32);
|
||
|
||
fn dotted((major, minor, patch): Version) -> String {
|
||
format!("{major}.{minor}.{patch}")
|
||
}
|
||
|
||
/// `7`, `7.4`, `v7.4.3`, `8.1.*`, `7.4.0-dev`: missing parts are 0.
|
||
fn version(s: &str) -> Option<Version> {
|
||
let s = s.trim().trim_start_matches(['v', 'V']);
|
||
let mut parts = s.split('.').map(|p| {
|
||
let digits: &str = &p[..p.find(|c: char| !c.is_ascii_digit()).unwrap_or(p.len())];
|
||
digits.parse::<u32>().ok()
|
||
});
|
||
let major = parts.next()??;
|
||
let minor = parts.next().flatten().unwrap_or(0);
|
||
let patch = parts.next().flatten().unwrap_or(0);
|
||
Some((major, minor, patch))
|
||
}
|
||
|
||
/// The lowest version a Composer constraint allows: the lowest lower bound of its
|
||
/// `||` alternatives. `None` when an alternative has no lower bound (`*`, `<8.0`).
|
||
fn lowest_allowed(constraint: &str) -> Option<Version> {
|
||
let bounds: Option<Vec<Version>> =
|
||
constraint.split('|').filter(|alt| !alt.trim().is_empty()).map(lower_bound).collect();
|
||
bounds?.into_iter().min()
|
||
}
|
||
|
||
/// The lower bound of constraints that must all hold (`>=7.2 <8.4`, `^7.4, !=7.4.1`,
|
||
/// `7.2 - 8.0`): the highest of their lower bounds.
|
||
fn lower_bound(alternative: &str) -> Option<Version> {
|
||
// A hyphen range: its first version.
|
||
let alternative = alternative.split(" - ").next().unwrap_or(alternative);
|
||
let mut tokens: Vec<String> = Vec::new();
|
||
for token in alternative.split([' ', ',']).filter(|t| !t.is_empty()) {
|
||
// `>= 7.2`: the operator and its version.
|
||
match tokens.last_mut() {
|
||
Some(op) if op.chars().all(|c| "<>=!~^".contains(c)) => op.push_str(token),
|
||
_ => tokens.push(token.to_owned()),
|
||
}
|
||
}
|
||
tokens
|
||
.iter()
|
||
.filter_map(|t| {
|
||
let t = t.split('@').next().unwrap_or(t);
|
||
if t.starts_with('<') || t.starts_with("!=") {
|
||
return None;
|
||
}
|
||
version(t.trim_start_matches(['>', '=', '^', '~']))
|
||
})
|
||
.max()
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
|
||
fn level(composer: Option<&str>, explicit: &str) -> Option<String> {
|
||
let tmp = tempfile::tempdir().unwrap();
|
||
if let Some(composer) = composer {
|
||
std::fs::write(tmp.path().join("composer.json"), composer).unwrap();
|
||
}
|
||
let mut s = Settings::default();
|
||
s.languages.php.language_level = explicit.into();
|
||
language_level(tmp.path(), &s)
|
||
}
|
||
|
||
fn required(constraint: &str) -> Option<String> {
|
||
level(Some(&serde_json::json!({ "require": { "php": constraint } }).to_string()), "")
|
||
}
|
||
|
||
#[test]
|
||
fn lowest_version_require_php_allows() {
|
||
assert_eq!(required("^7.4 || ^8.0").as_deref(), Some("7.4.0"));
|
||
assert_eq!(required("^8.0 || ^7.4").as_deref(), Some("7.4.0"));
|
||
assert_eq!(required(">=7.2 <8.4").as_deref(), Some("7.2.0"));
|
||
assert_eq!(required(">= 7.2, < 8.4").as_deref(), Some("7.2.0"));
|
||
assert_eq!(required("8.1.*").as_deref(), Some("8.1.0"));
|
||
assert_eq!(required("~7.4.3").as_deref(), Some("7.4.3"));
|
||
assert_eq!(required("7.2 - 8.0").as_deref(), Some("7.2.0"));
|
||
assert_eq!(required("^7.3|^8.0").as_deref(), Some("7.3.0"));
|
||
assert_eq!(required(">=8.1@dev").as_deref(), Some("8.1.0"));
|
||
assert_eq!(required("8").as_deref(), Some("8.0.0"));
|
||
}
|
||
|
||
#[test]
|
||
fn no_lower_bound_gives_nothing() {
|
||
for constraint in ["*", "<8.0", "", "^7.4 || *", "dev-main"] {
|
||
assert_eq!(required(constraint), None, "{constraint}");
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn platform_overrides_require() {
|
||
let composer = r#"{"require": {"php": "^8.2"}, "config": {"platform": {"php": "7.2.34"}}}"#;
|
||
assert_eq!(level(Some(composer), "").as_deref(), Some("7.2.34"));
|
||
}
|
||
|
||
#[test]
|
||
fn no_requirement_gives_nothing() {
|
||
assert_eq!(level(None, ""), None);
|
||
assert_eq!(level(Some(r#"{"require": {"ext-json": "*"}}"#), ""), None);
|
||
assert_eq!(level(Some("not json"), ""), None);
|
||
}
|
||
|
||
#[test]
|
||
fn each_php_version_gets_its_own_intelephense_storage() {
|
||
let data = Path::new("/data");
|
||
let server = |name: &str, options: Value| {
|
||
let mut cfg: LanguageServerConfig =
|
||
toml::from_str(&format!("name = \"{name}\"\ncommand = \"{name}\"")).unwrap();
|
||
if !options.is_null() {
|
||
merge_init_options(&mut cfg, options);
|
||
}
|
||
cfg
|
||
};
|
||
let options = |cfg: &LanguageServerConfig| serde_json::to_value(&cfg.initialization_options).unwrap();
|
||
|
||
let mut cfg = server("intelephense", json!({"environment": {"phpVersion": "7.4.0"}, "clearCache": false}));
|
||
configure_intelephense_storage(&mut cfg, data);
|
||
let storage = data.join("lsp").join("intelephense").join("7.4.0");
|
||
assert_eq!(
|
||
options(&cfg),
|
||
json!({"environment": {"phpVersion": "7.4.0"}, "clearCache": false, "storagePath": storage.to_string_lossy()})
|
||
);
|
||
|
||
// The version Intelephense reads wins: Settings > Language Servers `settings` over the options.
|
||
let storage_of = |init: Value, settings: Value| {
|
||
let mut cfg = server("intelephense", init);
|
||
cfg.settings = Some(settings);
|
||
configure_intelephense_storage(&mut cfg, data);
|
||
options(&cfg)["storagePath"].clone()
|
||
};
|
||
let path = |v: &str| json!(data.join("lsp").join("intelephense").join(v).to_string_lossy());
|
||
let init = json!({"environment": {"phpVersion": "7.4.0"}});
|
||
assert_eq!(storage_of(init.clone(), json!({"environment": {"phpVersion": "8.3.0"}})), path("8.3.0"));
|
||
assert_eq!(storage_of(init, json!({"other": 1})), path("7.4.0"));
|
||
let nested = json!({"settings": {"intelephense": {"environment": {"phpVersion": "8.0.0"}}}});
|
||
assert_eq!(storage_of(Value::Null, nested), path("8.0.0"));
|
||
|
||
// No version, the user's own storage, not a version, another server: unchanged.
|
||
for unchanged in [
|
||
server("intelephense", Value::Null),
|
||
server("intelephense", json!({"environment": {"includePaths": []}})),
|
||
server("intelephense", json!({"environment": {"phpVersion": "8.1.0"}, "storagePath": "/mine"})),
|
||
server("intelephense", json!({"environment": {"phpVersion": "../8.1"}})),
|
||
server("phpactor", json!({"environment": {"phpVersion": "8.1.0"}})),
|
||
] {
|
||
let mut cfg = unchanged.clone();
|
||
configure_intelephense_storage(&mut cfg, data);
|
||
assert_eq!(cfg, unchanged);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn explicit_level_wins() {
|
||
let composer = r#"{"require": {"php": "^7.4"}, "config": {"platform": {"php": "7.4.33"}}}"#;
|
||
assert_eq!(level(Some(composer), "8.1").as_deref(), Some("8.1.0"));
|
||
assert_eq!(level(None, " 5.6 ").as_deref(), Some("5.6.0"));
|
||
}
|
||
}
|