relay-knowledge 1.1.9

Graph-database-based knowledge graph project.
Documentation
use super::{
    languages::{json, properties, yaml},
    model::ConfigFact,
    source::{
        config_key_prefix, push_boolean_definition, push_definition, source_lines, valid_config_key,
    },
};

pub(super) fn facts(language_id: &str, content: &str, definitions: &mut Vec<ConfigFact>) {
    let mut yaml_block = yaml::BlockScalarTracker::default();
    let mut properties_continuation = false;
    for line in source_lines(content) {
        let trimmed = line.text.trim();
        if language_id == "yaml" && yaml_block.should_skip(line.text, trimmed) {
            continue;
        }
        if language_id == "properties"
            && properties::skip_continued_value_line(
                line.text,
                trimmed,
                &mut properties_continuation,
            )
        {
            continue;
        }
        if trimmed.is_empty()
            || trimmed.starts_with('#')
            || trimmed.starts_with('!')
            || trimmed.starts_with("//")
        {
            continue;
        }
        for key in boolean_config_keys(language_id, trimmed) {
            push_boolean_definition(definitions, key, "config_key", line.range());
        }
        if language_id == "json" {
            for key in json::object_keys(trimmed)
                .into_iter()
                .filter(|key| valid_config_key(key))
            {
                push_definition(definitions, key, "config_key", line.range());
            }
            continue;
        }
        if language_id == "toml"
            && let Some(section) = toml_section_name(trimmed)
        {
            push_definition(definitions, section, "section", line.range());
            continue;
        }
        if language_id == "ini" && trimmed.starts_with('[') && trimmed.ends_with(']') {
            push_definition(
                definitions,
                &trimmed[1..trimmed.len() - 1],
                "section",
                line.range(),
            );
            continue;
        }
        let key = if language_id == "yaml" {
            yaml::mapping_key(trimmed).map(config_key_prefix)
        } else {
            trimmed
                .split_once('=')
                .or_else(|| trimmed.split_once(':'))
                .map(|(key, _)| config_key_prefix(key))
                .or_else(|| properties_space_key(language_id, trimmed))
        };
        if let Some(key) = key.filter(|key| valid_config_key(key)) {
            push_definition(definitions, key, "config_key", line.range());
        }
    }
}

fn boolean_config_keys(language_id: &str, line: &str) -> Vec<String> {
    let mut keys = Vec::new();
    if let Some(key) = direct_boolean_config_key(line) {
        push_unique(&mut keys, key);
    }
    if let Some(key) = properties_boolean_config_key(language_id, line) {
        push_unique(&mut keys, key);
    }
    for key in inline_boolean_config_keys(line) {
        push_unique(&mut keys, key);
    }

    keys
}

fn direct_boolean_config_key(line: &str) -> Option<String> {
    let separator = line
        .find('=')
        .or_else(|| line.find(':'))
        .filter(|index| *index > 0)?;
    let (key, value) = line.split_at(separator);
    boolean_config_key(key, &value[1..])
}

fn properties_boolean_config_key(language_id: &str, line: &str) -> Option<String> {
    if language_id != "properties" {
        return None;
    }
    let (key, value) = line.split_once(char::is_whitespace)?;
    boolean_config_key(key, value)
}

fn boolean_config_key(key: &str, value: &str) -> Option<String> {
    let key = config_key_prefix(key);
    if !valid_config_key(key) {
        return None;
    }
    let value = value.trim().trim_end_matches(',');
    if value.starts_with('"') || value.starts_with('\'') {
        return None;
    }
    boolean_literal(value).then(|| key.to_owned())
}

fn inline_boolean_config_keys(line: &str) -> Vec<String> {
    let mut keys = Vec::new();
    for value_start in boolean_value_starts(line) {
        let prefix = line[..value_start].trim_end();
        let Some(separator) = prefix.rfind(['=', ':']) else {
            continue;
        };
        if !prefix[separator.saturating_add(1)..].trim().is_empty() {
            continue;
        }
        let Some(key) = prefix[..separator]
            .trim_end()
            .rsplit(|character: char| {
                character.is_whitespace() || matches!(character, '{' | '[' | ',')
            })
            .next()
        else {
            continue;
        };
        let key = key.trim().trim_matches('"').trim_matches('\'');
        if valid_config_key(key) {
            push_unique(&mut keys, key.to_owned());
        }
    }

    keys
}

fn boolean_value_starts(line: &str) -> Vec<usize> {
    let mut starts = Vec::new();
    let mut quote = None;
    let mut escaped = false;
    let mut index = 0usize;
    while index < line.len() {
        let rest = &line[index..];
        let Some(character) = rest.chars().next() else {
            break;
        };
        if let Some(quote_character) = quote {
            if escaped {
                escaped = false;
            } else if character == '\\' {
                escaped = true;
            } else if character == quote_character {
                quote = None;
            }
            index = index.saturating_add(character.len_utf8());
            continue;
        }
        if character == '"' || character == '\'' {
            quote = Some(character);
            index = index.saturating_add(character.len_utf8());
            continue;
        }
        for marker in ["true", "false", "enabled", "disabled"] {
            if rest.starts_with(marker)
                && token_boundary_before(line, index)
                && token_boundary_after(line, index + marker.len())
            {
                starts.push(index);
            }
        }
        index = index.saturating_add(character.len_utf8());
    }
    starts.sort_unstable();
    starts
}

fn boolean_literal(value: &str) -> bool {
    matches!(value, "true" | "false" | "enabled" | "disabled")
}

fn token_boundary_before(line: &str, start: usize) -> bool {
    line[..start]
        .chars()
        .next_back()
        .is_none_or(|character| !is_config_token_character(character))
}

fn token_boundary_after(line: &str, end: usize) -> bool {
    line[end..]
        .chars()
        .next()
        .is_none_or(|character| !is_config_token_character(character))
}

fn is_config_token_character(character: char) -> bool {
    character.is_ascii_alphanumeric() || matches!(character, '_' | '-' | '.')
}

fn push_unique(values: &mut Vec<String>, value: String) {
    if !values.iter().any(|existing| existing == &value) {
        values.push(value);
    }
}

fn toml_section_name(value: &str) -> Option<&str> {
    value
        .strip_prefix("[[")
        .and_then(|section| section.strip_suffix("]]"))
        .or_else(|| {
            value
                .strip_prefix('[')
                .and_then(|section| section.strip_suffix(']'))
        })
        .map(str::trim)
        .filter(|section| valid_config_key(section))
}

fn properties_space_key<'a>(language_id: &str, value: &'a str) -> Option<&'a str> {
    (language_id == "properties")
        .then(|| value.split_whitespace().next())
        .flatten()
}