use lsp_types::{
Diagnostic, DiagnosticSeverity, Hover, HoverContents, MarkupContent, MarkupKind, Position,
Range,
};
use std::collections::HashMap;
use taplo::dom::node::IntegerValue;
use crate::schema::{PropertySchema, SchemaProvider, TypeInfo};
#[derive(Debug, Clone)]
pub struct TomlDocument {
pub root: taplo::dom::Node,
pub env_vars: Vec<EnvVarReference>,
pub config_sections: HashMap<String, ConfigSection>,
pub content: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EnvVarReference {
pub name: String,
pub default: Option<String>,
pub range: Range,
}
#[derive(Debug, Clone)]
pub struct ConfigSection {
pub prefix: String,
pub properties: HashMap<String, ConfigProperty>,
pub range: Range,
}
#[derive(Debug, Clone)]
pub struct ConfigProperty {
pub key: String,
pub value: ConfigValue,
pub range: Range,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ConfigValue {
String(String),
Integer(i64),
Float(f64),
Boolean(bool),
Array(Vec<ConfigValue>),
Table(HashMap<String, ConfigValue>),
}
pub struct TomlAnalyzer {
schema_provider: SchemaProvider,
}
impl TomlAnalyzer {
pub fn new(schema_provider: SchemaProvider) -> Self {
Self { schema_provider }
}
pub fn schema_provider(&self) -> &SchemaProvider {
&self.schema_provider
}
pub fn hover(&self, doc: &TomlDocument, position: Position) -> Option<Hover> {
if let Some(hover) = self.hover_env_var(doc, position) {
return Some(hover);
}
if let Some(hover) = self.hover_config_property(doc, position) {
return Some(hover);
}
None
}
fn hover_env_var(&self, doc: &TomlDocument, position: Position) -> Option<Hover> {
for env_var in &doc.env_vars {
if self.position_in_range(position, env_var.range) {
let mut hover_text = String::new();
hover_text.push_str("# 环境变量\n\n");
hover_text.push_str(&format!("**变量名**: `{}`\n\n", env_var.name));
if let Some(default) = &env_var.default {
hover_text.push_str(&format!("**默认值**: `{}`\n\n", default));
}
if let Ok(value) = std::env::var(&env_var.name) {
hover_text.push_str(&format!("**当前值**: `{}`\n\n", value));
} else {
hover_text.push_str("**当前值**: *未设置*\n\n");
}
hover_text.push_str("**说明**:\n\n");
hover_text.push_str("环境变量插值允许在配置文件中引用系统环境变量。\n\n");
hover_text.push_str("**格式**:\n");
hover_text.push_str("- `${VAR}` - 引用环境变量,如果未设置则报错\n");
hover_text.push_str("- `${VAR:default}` - 引用环境变量,如果未设置则使用默认值\n");
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: Some(env_var.range),
});
}
}
None
}
fn hover_config_property(&self, doc: &TomlDocument, position: Position) -> Option<Hover> {
for (prefix, section) in &doc.config_sections {
for (key, property) in §ion.properties {
if self.position_in_range(position, property.range) {
if let Some(plugin_schema) = self.schema_provider.get_plugin(prefix) {
if let Some(property_schema) = plugin_schema.properties.get(key) {
return Some(self.create_property_hover(
prefix,
key,
property,
property_schema,
));
}
}
let is_defined = self.schema_provider.has_property(prefix, key);
return Some(
self.create_basic_property_hover(prefix, key, property, is_defined),
);
}
}
}
None
}
fn create_property_hover(
&self,
prefix: &str,
key: &str,
property: &ConfigProperty,
schema: &PropertySchema,
) -> Hover {
let mut hover_text = String::new();
hover_text.push_str(&format!("# 配置项: `{}.{}`\n\n", prefix, key));
if !schema.description.is_empty() {
hover_text.push_str(&format!("{}\n\n", schema.description));
}
hover_text.push_str(&format!(
"**类型**: {}\n\n",
self.type_info_to_string(&schema.type_info)
));
hover_text.push_str(&format!(
"**当前值**: `{}`\n\n",
self.config_value_to_string(&property.value)
));
if let Some(default) = &schema.default {
hover_text.push_str(&format!(
"**默认值**: `{}`\n\n",
self.value_to_string(default)
));
}
if schema.required {
hover_text.push_str("**必需**: 是\n\n");
}
if let TypeInfo::String {
enum_values: Some(enum_vals),
..
} = &schema.type_info
{
hover_text.push_str("**允许的值**:\n");
for val in enum_vals {
hover_text.push_str(&format!("- `{}`\n", val));
}
hover_text.push('\n');
}
match &schema.type_info {
TypeInfo::Integer { min, max } => {
if min.is_some() || max.is_some() {
hover_text.push_str("**值范围**:\n");
if let Some(min_val) = min {
hover_text.push_str(&format!("- 最小值: `{}`\n", min_val));
}
if let Some(max_val) = max {
hover_text.push_str(&format!("- 最大值: `{}`\n", max_val));
}
hover_text.push('\n');
}
}
TypeInfo::Float { min, max } => {
if min.is_some() || max.is_some() {
hover_text.push_str("**值范围**:\n");
if let Some(min_val) = min {
hover_text.push_str(&format!("- 最小值: `{}`\n", min_val));
}
if let Some(max_val) = max {
hover_text.push_str(&format!("- 最大值: `{}`\n", max_val));
}
hover_text.push('\n');
}
}
TypeInfo::String {
min_length,
max_length,
..
} => {
if min_length.is_some() || max_length.is_some() {
hover_text.push_str("**长度限制**:\n");
if let Some(min_len) = min_length {
hover_text.push_str(&format!("- 最小长度: `{}`\n", min_len));
}
if let Some(max_len) = max_length {
hover_text.push_str(&format!("- 最大长度: `{}`\n", max_len));
}
hover_text.push('\n');
}
}
_ => {}
}
if let Some(example) = &schema.example {
hover_text.push_str("**示例**:\n\n");
hover_text.push_str("```toml\n");
hover_text.push_str(example);
hover_text.push_str("\n```\n\n");
}
if let Some(deprecated_msg) = &schema.deprecated {
hover_text.push_str(&format!("⚠️ **已废弃**: {}\n\n", deprecated_msg));
}
hover_text.push_str("---\n\n");
hover_text.push_str(&format!("*配置节*: `[{}]`\n", prefix));
hover_text.push_str("*配置文件*: `config/app.toml`\n");
Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: Some(property.range),
}
}
fn create_basic_property_hover(
&self,
prefix: &str,
key: &str,
property: &ConfigProperty,
is_defined: bool,
) -> Hover {
let mut hover_text = String::new();
hover_text.push_str(&format!("# 配置项: `{}.{}`\n\n", prefix, key));
hover_text.push_str(&format!(
"**当前值**: `{}`\n\n",
self.config_value_to_string(&property.value)
));
hover_text.push_str(&format!(
"**类型**: {}\n\n",
self.config_value_type_name(&property.value)
));
if !is_defined {
hover_text.push_str("⚠️ **警告**: 此配置项未在 Schema 中定义\n\n");
}
hover_text.push_str("---\n\n");
hover_text.push_str(&format!("*配置节*: `[{}]`\n", prefix));
hover_text.push_str("*配置文件*: `config/app.toml`\n");
Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: hover_text,
}),
range: Some(property.range),
}
}
fn position_in_range(&self, position: Position, range: Range) -> bool {
if position.line < range.start.line || position.line > range.end.line {
return false;
}
if position.line == range.start.line && position.character < range.start.character {
return false;
}
if position.line == range.end.line && position.character > range.end.character {
return false;
}
true
}
fn config_value_to_string(&self, value: &ConfigValue) -> String {
match value {
ConfigValue::String(s) => format!("\"{}\"", s),
ConfigValue::Integer(i) => i.to_string(),
ConfigValue::Float(f) => f.to_string(),
ConfigValue::Boolean(b) => b.to_string(),
ConfigValue::Array(arr) => {
let items: Vec<String> =
arr.iter().map(|v| self.config_value_to_string(v)).collect();
format!("[{}]", items.join(", "))
}
ConfigValue::Table(table) => {
let items: Vec<String> = table
.iter()
.map(|(k, v)| format!("{} = {}", k, self.config_value_to_string(v)))
.collect();
format!("{{ {} }}", items.join(", "))
}
}
}
fn value_to_string(&self, value: &crate::schema::Value) -> String {
match value {
crate::schema::Value::String(s) => format!("\"{}\"", s),
crate::schema::Value::Integer(n) => n.to_string(),
crate::schema::Value::Float(f) => f.to_string(),
crate::schema::Value::Boolean(b) => b.to_string(),
crate::schema::Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(|v| self.value_to_string(v)).collect();
format!("[{}]", items.join(", "))
}
crate::schema::Value::Table(obj) => {
let items: Vec<String> = obj
.iter()
.map(|(k, v)| format!("{} = {}", k, self.value_to_string(v)))
.collect();
format!("{{ {} }}", items.join(", "))
}
}
}
pub fn validate(&self, doc: &TomlDocument) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
diagnostics.extend(self.validate_env_var_syntax(&doc.env_vars));
for (prefix, section) in &doc.config_sections {
if let Some(plugin_schema) = self.schema_provider.get_plugin(prefix) {
diagnostics.extend(self.validate_section(section, &plugin_schema));
diagnostics.extend(self.validate_required_properties(section, &plugin_schema));
} else {
diagnostics.push(Diagnostic {
range: section.range,
severity: Some(DiagnosticSeverity::WARNING),
code: Some(lsp_types::NumberOrString::String(
"undefined-section".to_string(),
)),
message: format!("配置节 '{}' 未在 Schema 中定义", prefix),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
diagnostics
}
#[allow(dead_code)]
fn validate_section_properties(&self, section: &ConfigSection) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (key, property) in §ion.properties {
if !self.schema_provider.has_property(§ion.prefix, key) {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::WARNING),
code: Some(lsp_types::NumberOrString::String(
"undefined-property".to_string(),
)),
message: format!("配置项 '{}' 未在 Schema 中定义", key),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
diagnostics
}
fn validate_env_var_syntax(&self, env_vars: &[EnvVarReference]) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for env_var in env_vars {
if env_var.name.is_empty() {
diagnostics.push(Diagnostic {
range: env_var.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"empty-var-name".to_string(),
)),
message: "环境变量名不能为空".to_string(),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
if !env_var
.name
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
{
diagnostics.push(Diagnostic {
range: env_var.range,
severity: Some(DiagnosticSeverity::WARNING),
code: Some(lsp_types::NumberOrString::String(
"invalid-var-name".to_string(),
)),
message: format!(
"环境变量名 '{}' 不符合命名规范,建议使用大写字母、数字和下划线",
env_var.name
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
diagnostics
}
fn validate_section(
&self,
section: &ConfigSection,
plugin_schema: &crate::schema::PluginSchema,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (key, property) in §ion.properties {
if let Some(property_schema) = plugin_schema.properties.get(key) {
if let Some(deprecated_msg) = &property_schema.deprecated {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::WARNING),
code: Some(lsp_types::NumberOrString::String(
"deprecated-property".to_string(),
)),
message: format!("配置项 '{}' 已废弃: {}", key, deprecated_msg),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
diagnostics.extend(self.validate_property_type(property, property_schema));
diagnostics.extend(self.validate_property_range(property, property_schema));
if let (
ConfigValue::Table(table),
crate::schema::TypeInfo::Object {
properties: nested_props,
},
) = (&property.value, &property_schema.type_info)
{
diagnostics.extend(self.validate_nested_table(
table,
nested_props,
property.range,
));
}
} else {
if !matches!(property.value, ConfigValue::Table(_)) {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::HINT),
code: Some(lsp_types::NumberOrString::String(
"undefined-property".to_string(),
)),
message: format!("配置项 '{}' 未在 Schema 中定义", key),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
}
diagnostics
}
fn validate_nested_table(
&self,
table: &HashMap<String, ConfigValue>,
schema_properties: &HashMap<String, crate::schema::PropertySchema>,
parent_range: Range,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (key, value) in table {
if let Some(property_schema) = schema_properties.get(key) {
let temp_property = ConfigProperty {
key: key.clone(),
value: value.clone(),
range: parent_range, };
diagnostics.extend(self.validate_property_type(&temp_property, property_schema));
diagnostics.extend(self.validate_property_range(&temp_property, property_schema));
if let (
ConfigValue::Table(nested_table),
crate::schema::TypeInfo::Object {
properties: nested_props,
},
) = (value, &property_schema.type_info)
{
diagnostics.extend(self.validate_nested_table(
nested_table,
nested_props,
parent_range,
));
}
}
}
diagnostics
}
fn validate_property_type(
&self,
property: &ConfigProperty,
schema: &PropertySchema,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
let type_matches = matches!(
(&property.value, &schema.type_info),
(ConfigValue::String(_), TypeInfo::String { .. })
| (ConfigValue::Integer(_), TypeInfo::Integer { .. })
| (ConfigValue::Float(_), TypeInfo::Float { .. })
| (ConfigValue::Boolean(_), TypeInfo::Boolean)
| (ConfigValue::Array(_), TypeInfo::Array { .. })
| (ConfigValue::Table(_), TypeInfo::Object { .. })
);
if !type_matches {
let expected_type = self.type_info_to_string(&schema.type_info);
let actual_type = self.config_value_type_name(&property.value);
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"type-mismatch".to_string(),
)),
message: format!(
"配置项 '{}' 的类型不匹配:期望 {},实际 {}",
property.key, expected_type, actual_type
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
diagnostics
}
fn validate_property_range(
&self,
property: &ConfigProperty,
schema: &PropertySchema,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
match (&property.value, &schema.type_info) {
(
ConfigValue::String(s),
TypeInfo::String {
enum_values,
min_length,
max_length,
},
) => {
if let Some(enum_vals) = enum_values {
if !self.contains_env_var(s) && !enum_vals.contains(s) {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"invalid-enum-value".to_string(),
)),
message: format!(
"配置项 '{}' 的值 '{}' 不在允许的枚举值中:{:?}",
property.key, s, enum_vals
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
if let Some(min_len) = min_length {
if !self.contains_env_var(s) && s.len() < *min_len {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"string-too-short".to_string(),
)),
message: format!(
"配置项 '{}' 的值长度 {} 小于最小长度 {}",
property.key,
s.len(),
min_len
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
if let Some(max_len) = max_length {
if !self.contains_env_var(s) && s.len() > *max_len {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"string-too-long".to_string(),
)),
message: format!(
"配置项 '{}' 的值长度 {} 超过最大长度 {}",
property.key,
s.len(),
max_len
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
}
(ConfigValue::Integer(i), TypeInfo::Integer { min, max }) => {
if let Some(min_val) = min {
if *i < *min_val {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"value-too-small".to_string(),
)),
message: format!(
"配置项 '{}' 的值 {} 小于最小值 {}",
property.key, i, min_val
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
if let Some(max_val) = max {
if *i > *max_val {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"value-too-large".to_string(),
)),
message: format!(
"配置项 '{}' 的值 {} 超过最大值 {}",
property.key, i, max_val
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
}
(ConfigValue::Float(f), TypeInfo::Float { min, max }) => {
if let Some(min_val) = min {
if *f < *min_val {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"value-too-small".to_string(),
)),
message: format!(
"配置项 '{}' 的值 {} 小于最小值 {}",
property.key, f, min_val
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
if let Some(max_val) = max {
if *f > *max_val {
diagnostics.push(Diagnostic {
range: property.range,
severity: Some(DiagnosticSeverity::ERROR),
code: Some(lsp_types::NumberOrString::String(
"value-too-large".to_string(),
)),
message: format!(
"配置项 '{}' 的值 {} 超过最大值 {}",
property.key, f, max_val
),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
}
_ => {}
}
diagnostics
}
fn validate_required_properties(
&self,
section: &ConfigSection,
plugin_schema: &crate::schema::PluginSchema,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
for (key, property_schema) in &plugin_schema.properties {
if property_schema.required && !section.properties.contains_key(key) {
diagnostics.push(Diagnostic {
range: section.range,
severity: Some(DiagnosticSeverity::WARNING),
code: Some(lsp_types::NumberOrString::String(
"missing-required-property".to_string(),
)),
message: format!("缺少必需的配置项 '{}'", key),
source: Some("summer-lsp".to_string()),
..Default::default()
});
}
}
diagnostics
}
fn type_info_to_string(&self, type_info: &TypeInfo) -> String {
match type_info {
TypeInfo::String { .. } => "字符串".to_string(),
TypeInfo::Integer { .. } => "整数".to_string(),
TypeInfo::Float { .. } => "浮点数".to_string(),
TypeInfo::Boolean => "布尔值".to_string(),
TypeInfo::Array { .. } => "数组".to_string(),
TypeInfo::Object { .. } => "对象".to_string(),
}
}
fn config_value_type_name(&self, value: &ConfigValue) -> String {
match value {
ConfigValue::String(_) => "字符串".to_string(),
ConfigValue::Integer(_) => "整数".to_string(),
ConfigValue::Float(_) => "浮点数".to_string(),
ConfigValue::Boolean(_) => "布尔值".to_string(),
ConfigValue::Array(_) => "数组".to_string(),
ConfigValue::Table(_) => "对象".to_string(),
}
}
fn contains_env_var(&self, s: &str) -> bool {
s.contains("${") && s.contains('}')
}
pub fn parse(&self, content: &str) -> Result<TomlDocument, String> {
let (preprocessed_content, env_vars) = self.preprocess_env_vars(content);
let parse_result = taplo::parser::parse(&preprocessed_content);
if !parse_result.errors.is_empty() {
let error_messages: Vec<String> = parse_result
.errors
.iter()
.map(|e| format!("{:?}:{:?} - {}", e.range.start(), e.range.end(), e.message))
.collect();
return Err(format!("TOML 语法错误: {}", error_messages.join("; ")));
}
let root = parse_result.into_dom();
let config_sections = self.extract_config_sections(&root, content);
Ok(TomlDocument {
root,
env_vars,
config_sections,
content: content.to_string(),
})
}
fn preprocess_env_vars(&self, content: &str) -> (String, Vec<EnvVarReference>) {
let mut result = String::with_capacity(content.len());
let mut env_vars = Vec::new();
let mut line = 0u32;
let mut line_start = 0;
let mut i = 0;
let chars: Vec<char> = content.chars().collect();
let mut in_string = false; let mut in_multiline_string = false; let mut escape_next = false;
while i < chars.len() {
if chars[i] == '\n' {
line += 1;
line_start = i + 1;
}
if escape_next {
result.push(chars[i]);
escape_next = false;
i += 1;
continue;
}
if chars[i] == '\\' && (in_string || in_multiline_string) {
escape_next = true;
result.push(chars[i]);
i += 1;
continue;
}
if i + 2 < chars.len() && chars[i] == '"' && chars[i + 1] == '"' && chars[i + 2] == '"'
{
in_multiline_string = !in_multiline_string;
result.push_str("\"\"\"");
i += 3;
continue;
}
if chars[i] == '"' && !in_multiline_string {
in_string = !in_string;
result.push(chars[i]);
i += 1;
continue;
}
if !in_string && !in_multiline_string {
if i + 1 < chars.len() && chars[i] == '$' && chars[i + 1] == '{' {
let mut j = i + 2;
while j < chars.len() && chars[j] != '}' {
j += 1;
}
if j < chars.len() {
let var_content: String = chars[i + 2..j].iter().collect();
let (name, default) = if let Some(colon_pos) = var_content.find(':') {
let name = var_content[..colon_pos].to_string();
let default = Some(var_content[colon_pos + 1..].to_string());
(name, default)
} else {
(var_content.to_string(), None)
};
let start_char = i - line_start;
let end_char = j + 1 - line_start;
env_vars.push(EnvVarReference {
name: name.clone(),
default: default.clone(),
range: Range {
start: Position {
line,
character: start_char as u32,
},
end: Position {
line,
character: end_char as u32,
},
},
});
let placeholder = if let Some(default_val) = &default {
if default_val == "true"
|| default_val == "false"
|| default_val.parse::<i64>().is_ok()
|| default_val.parse::<f64>().is_ok()
{
default_val.clone()
} else {
format!("\"{}\"", default_val.replace('"', "\\\""))
}
} else {
"\"\"".to_string()
};
result.push_str(&placeholder);
i = j + 1;
continue;
}
}
}
result.push(chars[i]);
i += 1;
}
(result, env_vars)
}
fn extract_config_sections(
&self,
root: &taplo::dom::Node,
content: &str,
) -> HashMap<String, ConfigSection> {
let mut sections: HashMap<String, ConfigSection> = HashMap::new();
if let Some(table) = root.as_table() {
let entries = table.entries();
let entries_arc = entries.get();
for (key, value) in entries_arc.iter() {
let key_str = key.value().to_string();
let prefix = if let Some(dot_pos) = key_str.find('.') {
key_str[..dot_pos].to_string()
} else {
key_str.clone()
};
if value.as_table().is_some() {
let properties = self.extract_properties(value, content);
let range = self.node_to_range(value, content);
if let Some(existing_section) = sections.get_mut(&prefix) {
if key_str.contains('.') {
let nested_key = key_str[prefix.len() + 1..].to_string();
let nested_table = self.properties_to_value_table(&properties);
existing_section.properties.insert(
nested_key,
ConfigProperty {
key: key_str.clone(),
value: ConfigValue::Table(nested_table),
range,
},
);
} else {
existing_section.properties.extend(properties);
}
} else {
let mut section_properties = HashMap::new();
if key_str.contains('.') {
let nested_key = key_str[prefix.len() + 1..].to_string();
let nested_table = self.properties_to_value_table(&properties);
section_properties.insert(
nested_key,
ConfigProperty {
key: key_str.clone(),
value: ConfigValue::Table(nested_table),
range,
},
);
} else {
section_properties = properties;
}
sections.insert(
prefix.clone(),
ConfigSection {
prefix,
properties: section_properties,
range,
},
);
}
}
}
}
sections
}
fn properties_to_value_table(
&self,
properties: &HashMap<String, ConfigProperty>,
) -> HashMap<String, ConfigValue> {
properties
.iter()
.map(|(key, prop)| (key.clone(), prop.value.clone()))
.collect()
}
fn extract_properties(
&self,
node: &taplo::dom::Node,
content: &str,
) -> HashMap<String, ConfigProperty> {
let mut properties = HashMap::new();
if let Some(table) = node.as_table() {
let entries = table.entries();
let entries_arc = entries.get();
for (key, value) in entries_arc.iter() {
let key_str = key.value().to_string();
let config_value = self.node_to_config_value(value);
let range = self.node_to_range(value, content);
properties.insert(
key_str.clone(),
ConfigProperty {
key: key_str,
value: config_value,
range,
},
);
}
}
properties
}
fn node_to_config_value(&self, node: &taplo::dom::Node) -> ConfigValue {
match node {
taplo::dom::Node::Bool(b) => ConfigValue::Boolean(b.value()),
taplo::dom::Node::Str(s) => ConfigValue::String(s.value().to_string()),
taplo::dom::Node::Integer(i) => {
match i.value() {
IntegerValue::Positive(v) => ConfigValue::Integer(v as i64),
IntegerValue::Negative(v) => ConfigValue::Integer(v),
}
}
taplo::dom::Node::Float(f) => ConfigValue::Float(f.value()),
taplo::dom::Node::Array(arr) => {
let items = arr.items();
let mut values = Vec::new();
let items_arc = items.get();
for item in items_arc.iter() {
values.push(self.node_to_config_value(item));
}
ConfigValue::Array(values)
}
taplo::dom::Node::Table(table) => {
let entries = table.entries();
let mut map = HashMap::new();
let entries_arc = entries.get();
for (key, value) in entries_arc.iter() {
let key_str = key.value().to_string();
map.insert(key_str, self.node_to_config_value(value));
}
ConfigValue::Table(map)
}
_ => ConfigValue::String(String::new()), }
}
fn node_to_range(&self, node: &taplo::dom::Node, content: &str) -> Range {
let mut text_ranges = node.text_ranges();
if let Some(first_range) = text_ranges.next() {
let start: usize = first_range.start().into();
let end: usize = first_range.end().into();
let start_pos = self.byte_offset_to_position(content, start);
let end_pos = self.byte_offset_to_position(content, end);
Range {
start: start_pos,
end: end_pos,
}
} else {
Range {
start: lsp_types::Position {
line: 0,
character: 0,
},
end: lsp_types::Position {
line: 0,
character: 0,
},
}
}
}
fn byte_offset_to_position(&self, content: &str, byte_offset: usize) -> lsp_types::Position {
let mut line = 0;
let mut character = 0;
let mut current_offset = 0;
for ch in content.chars() {
if current_offset >= byte_offset {
break;
}
if ch == '\n' {
line += 1;
character = 0;
} else {
character += 1;
}
current_offset += ch.len_utf8();
}
lsp_types::Position {
line: line as u32,
character: character as u32,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_preprocess_env_vars_in_quotes() {
let schema_provider = SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"test_pay_amount = "${TEST_PAY_AMOUNT:false}""#;
let (preprocessed, env_vars) = analyzer.preprocess_env_vars(content);
println!("原始: {}", content);
println!("预处理后: {}", preprocessed);
println!("环境变量数量: {}", env_vars.len());
assert_eq!(env_vars.len(), 0, "引号内的环境变量不应该被提取");
assert_eq!(preprocessed, content, "引号内的内容不应该被修改");
}
#[test]
fn test_preprocess_env_vars_without_quotes() {
let schema_provider = SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"test_pay_amount = ${TEST_PAY_AMOUNT:false}"#;
let (preprocessed, env_vars) = analyzer.preprocess_env_vars(content);
println!("原始: {}", content);
println!("预处理后: {}", preprocessed);
println!("环境变量数量: {}", env_vars.len());
assert_eq!(env_vars.len(), 1);
assert_eq!(env_vars[0].name, "TEST_PAY_AMOUNT");
assert_eq!(env_vars[0].default, Some("false".to_string()));
assert_eq!(preprocessed, "test_pay_amount = false");
}
#[test]
fn test_parse_with_quoted_env_var() {
let schema_provider = SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[pay]
test_pay_amount = "${TEST_PAY_AMOUNT:false}"
api_key = "${API_KEY:test_key}"
"#;
let result = analyzer.parse(content);
assert!(
result.is_ok(),
"带引号的环境变量应该能正常解析: {:?}",
result.err()
);
}
#[test]
fn test_parse_without_quoted_env_var() {
let schema_provider = SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[pay]
test_pay_amount = ${TEST_PAY_AMOUNT:false}
port = ${PORT:8080}
"#;
let result = analyzer.parse(content);
assert!(
result.is_ok(),
"不带引号的环境变量应该能正常解析: {:?}",
result.err()
);
let doc = result.unwrap();
assert_eq!(doc.env_vars.len(), 2, "应该提取到 2 个环境变量");
}
}
#[cfg(test)]
mod env_var_validation_tests {
use super::*;
use lsp_types::DiagnosticSeverity;
#[test]
fn test_env_var_in_enum_should_not_error() {
let mut schema = crate::schema::ConfigSchema {
plugins: std::collections::HashMap::new(),
};
schema.plugins.insert(
"logger".to_string(),
serde_json::json!({
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": ["trace", "debug", "info", "warn", "error"],
"description": "日志级别"
}
}
}),
);
let schema_provider = crate::schema::SchemaProvider::from_schema(schema);
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[logger]
level = "${RUST_LOG:info}"
"#;
let doc = analyzer.parse(content).unwrap();
let diagnostics = analyzer.validate(&doc);
let enum_errors: Vec<_> = diagnostics
.iter()
.filter(|d| {
d.code.as_ref().and_then(|c| match c {
lsp_types::NumberOrString::String(s) => Some(s.as_str()),
_ => None,
}) == Some("invalid-enum-value")
})
.collect();
assert!(
enum_errors.is_empty(),
"环境变量不应该触发枚举值错误,但发现了: {:?}",
enum_errors
);
}
#[test]
fn test_invalid_enum_without_env_var_should_error() {
let mut schema = crate::schema::ConfigSchema {
plugins: std::collections::HashMap::new(),
};
schema.plugins.insert(
"logger".to_string(),
serde_json::json!({
"type": "object",
"properties": {
"level": {
"type": "string",
"enum": ["trace", "debug", "info", "warn", "error"],
"description": "日志级别"
}
}
}),
);
let schema_provider = crate::schema::SchemaProvider::from_schema(schema);
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[logger]
level = "invalid_level"
"#;
let doc = analyzer.parse(content).unwrap();
let diagnostics = analyzer.validate(&doc);
let enum_errors: Vec<_> = diagnostics
.iter()
.filter(|d| {
d.severity == Some(DiagnosticSeverity::ERROR)
&& d.code.as_ref().and_then(|c| match c {
lsp_types::NumberOrString::String(s) => Some(s.as_str()),
_ => None,
}) == Some("invalid-enum-value")
})
.collect();
assert!(!enum_errors.is_empty(), "无效的枚举值应该触发错误");
}
#[test]
fn test_env_var_in_string_length_should_not_error() {
let mut schema = crate::schema::ConfigSchema {
plugins: std::collections::HashMap::new(),
};
schema.plugins.insert(
"web".to_string(),
serde_json::json!({
"type": "object",
"properties": {
"host": {
"type": "string",
"minLength": 5,
"maxLength": 20,
"description": "主机地址"
}
}
}),
);
let schema_provider = crate::schema::SchemaProvider::from_schema(schema);
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[web]
host = "${HOST:0.0.0.0}"
"#;
let doc = analyzer.parse(content).unwrap();
let diagnostics = analyzer.validate(&doc);
let length_errors: Vec<_> = diagnostics
.iter()
.filter(|d| {
d.code
.as_ref()
.and_then(|c| match c {
lsp_types::NumberOrString::String(s) => Some(s.as_str()),
_ => None,
})
.map(|s| s.contains("string-too-short") || s.contains("string-too-long"))
.unwrap_or(false)
})
.collect();
assert!(
length_errors.is_empty(),
"环境变量不应该触发长度错误,但发现了: {:?}",
length_errors
);
}
#[test]
fn test_contains_env_var() {
let schema_provider = crate::schema::SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
assert!(analyzer.contains_env_var("${VAR}"));
assert!(analyzer.contains_env_var("${VAR:default}"));
assert!(analyzer.contains_env_var("prefix_${VAR}_suffix"));
assert!(analyzer.contains_env_var("${VAR1}_${VAR2}"));
assert!(!analyzer.contains_env_var("normal_string"));
assert!(!analyzer.contains_env_var("$VAR"));
assert!(!analyzer.contains_env_var("{VAR}"));
assert!(!analyzer.contains_env_var(""));
}
}
#[cfg(test)]
mod nested_config_tests {
use super::*;
#[test]
fn test_nested_section_parsing() {
let schema_provider = crate::schema::SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[web.middlewares]
compression = { enable = true }
cors = { enable = true, allow_origins = ["https://example.com"], max_age = 60 }
"#;
let result = analyzer.parse(content);
assert!(
result.is_ok(),
"嵌套配置段应该能正常解析: {:?}",
result.err()
);
let doc = result.unwrap();
assert!(
doc.config_sections.contains_key("web"),
"应该提取到 'web' 配置段"
);
let web_section = &doc.config_sections["web"];
assert!(
web_section.properties.contains_key("middlewares"),
"应该包含 'middlewares' 嵌套属性"
);
let middlewares_prop = &web_section.properties["middlewares"];
match &middlewares_prop.value {
ConfigValue::Table(table) => {
assert!(
table.contains_key("compression"),
"middlewares 应该包含 'compression' 属性"
);
assert!(
table.contains_key("cors"),
"middlewares 应该包含 'cors' 属性"
);
}
_ => panic!("middlewares 应该是 Table 类型"),
}
}
#[test]
fn test_inline_table_parsing() {
let schema_provider = crate::schema::SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[opendal]
options = { endpoint = "${WEB_DAV_HOST:https://example.com}", username = "${WEB_DAV_USERNAME:user}", password = "${WEB_DAV_PASSWORD:pass}" }
"#;
let result = analyzer.parse(content);
assert!(result.is_ok(), "内联表应该能正常解析: {:?}", result.err());
let doc = result.unwrap();
assert!(
doc.config_sections.contains_key("opendal"),
"应该提取到 'opendal' 配置段"
);
let opendal_section = &doc.config_sections["opendal"];
assert!(
opendal_section.properties.contains_key("options"),
"应该包含 'options' 属性"
);
let options_prop = &opendal_section.properties["options"];
match &options_prop.value {
ConfigValue::Table(table) => {
assert!(
table.contains_key("endpoint"),
"options 应该包含 'endpoint' 属性"
);
assert!(
table.contains_key("username"),
"options 应该包含 'username' 属性"
);
assert!(
table.contains_key("password"),
"options 应该包含 'password' 属性"
);
if let ConfigValue::String(endpoint) = &table["endpoint"] {
assert!(
endpoint.contains("${WEB_DAV_HOST") || endpoint.starts_with("https://"),
"endpoint 应该包含环境变量引用或默认值,实际值: {}",
endpoint
);
}
}
_ => panic!("options 应该是 Table 类型"),
}
println!("提取到的环境变量数量: {}", doc.env_vars.len());
}
#[test]
fn test_multiple_nested_sections() {
let schema_provider = crate::schema::SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[web]
host = "0.0.0.0"
port = 8080
[web.middlewares]
compression = { enable = true }
cors = { enable = true }
[web.routes]
prefix = "/api"
"#;
let result = analyzer.parse(content);
assert!(
result.is_ok(),
"多个嵌套配置段应该能正常解析: {:?}",
result.err()
);
let doc = result.unwrap();
assert!(
doc.config_sections.contains_key("web"),
"应该提取到 'web' 配置段"
);
let web_section = &doc.config_sections["web"];
assert!(
web_section.properties.contains_key("host"),
"应该包含 'host' 属性"
);
assert!(
web_section.properties.contains_key("port"),
"应该包含 'port' 属性"
);
assert!(
web_section.properties.contains_key("middlewares"),
"应该包含 'middlewares' 嵌套属性"
);
assert!(
web_section.properties.contains_key("routes"),
"应该包含 'routes' 嵌套属性"
);
}
#[test]
fn test_nested_section_with_env_vars() {
let schema_provider = crate::schema::SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[web.middlewares]
compression = { enable = ${ENABLE_COMPRESSION:true} }
cors = { allow_origins = ["${CORS_ORIGIN:https://example.com}"] }
"#;
let result = analyzer.parse(content);
assert!(
result.is_ok(),
"嵌套配置段中的环境变量应该能正常解析: {:?}",
result.err()
);
let doc = result.unwrap();
assert!(!doc.env_vars.is_empty(), "应该提取到环境变量");
assert!(
doc.config_sections.contains_key("web"),
"应该提取到 'web' 配置段"
);
}
#[test]
fn test_nested_config_validation_warning() {
let schema_provider = crate::schema::SchemaProvider::new();
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[web]
port = 8080
[web.middlewares]
compression = { enable = true }
"#;
let doc = analyzer.parse(content).unwrap();
let diagnostics = analyzer.validate(&doc);
let middlewares_diagnostics: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.contains("middlewares"))
.collect();
assert!(
middlewares_diagnostics.is_empty(),
"嵌套配置不应该产生诊断信息,但发现了: {:?}",
middlewares_diagnostics
);
}
#[test]
fn test_undefined_plain_property_still_errors() {
let mut schema = crate::schema::ConfigSchema {
plugins: std::collections::HashMap::new(),
};
schema.plugins.insert(
"web".to_string(),
serde_json::json!({
"type": "object",
"properties": {
"port": {
"type": "integer",
"description": "Web server port"
}
}
}),
);
let schema_provider = crate::schema::SchemaProvider::from_schema(schema);
let analyzer = TomlAnalyzer::new(schema_provider);
let content = r#"
[web]
port = 8080
unknown_plain_property = "test"
"#;
let doc = analyzer.parse(content).unwrap();
let diagnostics = analyzer.validate(&doc);
let property_diagnostics: Vec<_> = diagnostics
.iter()
.filter(|d| d.message.contains("unknown_plain_property"))
.collect();
assert!(
!property_diagnostics.is_empty(),
"未定义的普通属性应该产生诊断"
);
for diag in &property_diagnostics {
assert_eq!(
diag.severity,
Some(DiagnosticSeverity::HINT),
"未定义的普通属性应该是提示级别"
);
}
}
}