lsp_core/systems/
shapes.rs

1use std::{cell::OnceCell, collections::HashMap};
2
3use bevy_ecs::prelude::*;
4use lsp_types::{DiagnosticSeverity, TextDocumentItem};
5use ropey::Rope;
6use rudof_lib::{
7    shacl_ast::{compiled::shape::CompiledShape, ShaclParser},
8    shacl_validation::{
9        shacl_processor::{GraphValidation, ShaclProcessor},
10        shape::Validate,
11        store::graph::Graph,
12        validation_report::result::ValidationResult,
13    },
14    srdf::{Object, SRDFGraph},
15    RdfData,
16};
17use sophia_api::quad::Quad as _;
18use tracing::{debug, error, info, instrument};
19
20use crate::prelude::*;
21
22fn get_reader<'a>(rope: &'a Rope) -> impl std::io::Read + 'a {
23    use std::io::prelude::*;
24    let reader: Box<dyn Read> = rope
25        .chunks()
26        .map(|x| std::io::Cursor::new(x.as_bytes()))
27        .fold(Box::new(std::io::Cursor::new(&[])), |acc, chunk| {
28            Box::new(acc.chain(chunk))
29        });
30    reader
31}
32
33type ShaclShape = CompiledShape<RdfData>;
34type ShaclShapes = Wrapped<Vec<CompiledShape<RdfData>>>;
35pub fn derive_shapes(
36    query: Query<(Entity, &RopeC, &Label), (Changed<Triples>, Without<Dirty>)>,
37    mut commands: Commands,
38) {
39    for (e, rope, label) in &query {
40        commands.entity(e).remove::<ShaclShapes>();
41
42        match SRDFGraph::from_reader(
43            get_reader(&rope),
44            &rudof_lib::RDFFormat::Turtle,
45            Some(label.0.as_str()),
46            &rudof_lib::ReaderMode::Lax,
47        )
48        .map_err(|x| x.to_string())
49        .and_then(|data| RdfData::from_graph(data).map_err(|e| e.to_string()))
50        .and_then(|data| {
51            let mut parser = ShaclParser::new(data.clone());
52            let result = parser.parse().map_err(|e| e.to_string());
53
54            if !parser.errors().is_empty() {
55                info!("Parsing shapes had some errors");
56                for e in parser.errors() {
57                    info!("Error: {}", e);
58                }
59            }
60
61            result
62        }) {
63            Ok(schema) => {
64                // .map(|shacl| {
65                //     out
66                //     // 2 + 2; return None;
67                // }) {
68                let compiled: Vec<_> = schema
69                    .iter()
70                    .flat_map(
71                        |(node, shape)| match ShaclShape::compile(shape.clone(), &schema) {
72                            Ok(x) => Some(x),
73                            Err(err) => {
74                                info!("Failed to parse shacl shape {}: {}", node, err);
75                                None
76                            }
77                        },
78                    )
79                    .collect();
80                let is_some = !compiled.is_empty();
81
82                debug!(
83                    "Compiled shapes for {} (is some {})",
84                    label.as_str(),
85                    is_some
86                );
87                if !compiled.is_empty() {
88                    commands.entity(e).insert(Wrapped(compiled));
89                }
90            }
91            Err(reason) => {
92                error!(
93                    "Failed to compile shapes for {} ({})",
94                    label.as_str(),
95                    reason
96                );
97            }
98        }
99    }
100}
101
102fn get_path(
103    source: Option<&Object>,
104    s: &CompiledShape<RdfData>,
105    prefixes: &Prefixes,
106) -> Option<String> {
107    let source = source?;
108    let property = s
109        .property_shapes()
110        .iter()
111        .find(|x| match (x.id(), source) {
112            (rudof_lib::oxrdf::Term::NamedNode(named_node), Object::Iri(iri_s)) => {
113                named_node.as_str() == iri_s.as_str()
114            }
115            (rudof_lib::oxrdf::Term::BlankNode(blank_node), Object::BlankNode(st)) => {
116                blank_node.as_str() == st.as_str()
117            }
118            _ => false,
119        })?;
120    let path = property.path_str()?;
121    Some(prefixes.shorten(&path).unwrap_or(path))
122}
123
124fn group_per_fn_per_path(
125    res: &Vec<ValidationResult>,
126    s: &CompiledShape<RdfData>,
127    triples: &Triples,
128    prefixes: &Prefixes,
129) -> HashMap<std::ops::Range<usize>, HashMap<String, Vec<String>>> {
130    let mut per_fn_per_path = HashMap::new();
131    for r in res {
132        let foc = r.focus_node().to_string();
133
134        let mut done = std::collections::HashSet::new();
135        for t in &triples.0 {
136            if t.s().as_str() == &foc && !done.contains(t.s()) {
137                done.insert(t.s().to_owned());
138
139                let entry: &mut HashMap<String, Vec<String>> =
140                    per_fn_per_path.entry(t.s().span.clone()).or_default();
141
142                let path = get_path(r.source(), s, prefixes).unwrap_or(String::new());
143                let entry = entry.entry(path).or_default();
144
145                let component = r.component().to_string();
146                let component = prefixes.shorten(&component).unwrap_or(component);
147
148                entry.push(component);
149            }
150        }
151    }
152
153    per_fn_per_path
154}
155
156fn push_diagnostics(
157    rope: &Rope,
158    res: &Vec<ValidationResult>,
159    s: &CompiledShape<RdfData>,
160    triples: &Triples,
161    prefixes: &Prefixes,
162    diagnostics: &mut Vec<lsp_types::Diagnostic>,
163) {
164    for (range, per_path) in group_per_fn_per_path(&res, s, triples, prefixes) {
165        if let Some(range) = range_to_range(&range, &rope) {
166            for (path, components) in per_path {
167                let mut comps = components[0].clone();
168                for c in components.into_iter().skip(1) {
169                    comps += ", ";
170                    comps += &c;
171                }
172
173                diagnostics.push(lsp_types::Diagnostic {
174                    range: range.clone(),
175                    severity: Some(DiagnosticSeverity::ERROR),
176                    source: Some(String::from("SWLS")),
177                    message: format!("Path {} violates {}", path, comps),
178                    related_information: None,
179                    ..Default::default()
180                });
181            }
182        }
183    }
184}
185
186fn derive_shapes_diagnostics_for(
187    rope: &RopeC,
188    label: &Label,
189    links: &DocumentLinks,
190    item: &Wrapped<TextDocumentItem>,
191    triples: &Triples,
192    other: &Query<(&Label, &ShaclShapes, &Prefixes)>,
193    client: &mut DiagnosticPublisher,
194) {
195    let mut diagnostics: Vec<lsp_types::Diagnostic> = Vec::new();
196
197    let build_validator = || {
198        SRDFGraph::from_reader(
199            get_reader(&rope),
200            &rudof_lib::RDFFormat::Turtle,
201            Some(label.0.as_str()),
202            &rudof_lib::ReaderMode::Lax,
203        )
204        .ok()
205        .and_then(|data| RdfData::from_graph(data).ok())
206        .map(|data| {
207            debug!("Created graph validator for {}", label.as_str());
208            GraphValidation::from_graph(
209                Graph::from_data(data),
210                rudof_lib::ShaclValidationMode::Native,
211            )
212        })
213    };
214
215    // Delayed building, maybe no shapes are linked to this document, and we don't need to build a
216    // validator
217    let validator = OnceCell::<Option<GraphValidation>>::new();
218    for (other_label, schema, prefixes) in other {
219        if links
220            .iter()
221            .find(|link| link.0.as_str().starts_with(other_label.0.as_str()))
222            .is_none()
223            && label.0 != other_label.0
224        {
225            continue;
226        }
227
228        if let Some(validator) = validator.get_or_init(build_validator) {
229            debug!("Schema {}", other_label.as_str());
230            for s in schema.iter() {
231                if let Ok(res) = s.validate(validator.store(), validator.runner(), None, None) {
232                    if !res.is_empty() {
233                        push_diagnostics(rope, &res, s, triples, prefixes, &mut diagnostics);
234                    }
235                }
236            }
237        } else {
238            break;
239        }
240    }
241
242    let _ = client.publish(&item.0, diagnostics, "shacl_validation");
243}
244
245/// System evaluates linked shapes
246// #[instrument(skip(query, other, client))]
247pub fn validate_shapes(
248    query: Query<
249        (
250            &RopeC,
251            &Label,
252            &DocumentLinks,
253            &Wrapped<TextDocumentItem>,
254            &Triples,
255        ),
256        (Changed<Triples>, Without<Dirty>, With<Open>),
257    >,
258    other: Query<(&Label, &ShaclShapes, &Prefixes)>,
259    mut client: ResMut<DiagnosticPublisher>,
260) {
261    for (rope, label, links, item, triples) in &query {
262        info!("Validate shapes {}", label.as_str());
263        derive_shapes_diagnostics_for(rope, label, links, item, triples, &other, &mut client);
264    }
265}
266
267/// System checks what entities should retrigger a shape evaluation when a shape changes
268#[instrument(skip(changed_schemas, query, other, client))]
269pub fn validate_with_updated_shapes(
270    changed_schemas: Query<&Label, (Changed<Wrapped<ShaclShape>>, With<Open>)>,
271    query: Query<
272        (
273            &RopeC,
274            &Label,
275            &DocumentLinks,
276            &Wrapped<TextDocumentItem>,
277            &Triples,
278        ),
279        With<Open>,
280    >,
281    other: Query<(&Label, &ShaclShapes, &Prefixes)>,
282    mut client: ResMut<DiagnosticPublisher>,
283) {
284    for l in &changed_schemas {
285        info!("Changed schema {}", l.as_str());
286        for (rope, label, links, item, triples) in &query {
287            if links
288                .iter()
289                .find(|(url, _)| url.as_str().starts_with(l.as_str()))
290                .is_some()
291            {
292                debug!("Found reverse linked document! {}", label.as_str());
293                derive_shapes_diagnostics_for(
294                    rope,
295                    label,
296                    links,
297                    item,
298                    triples,
299                    &other,
300                    &mut client,
301                );
302            }
303        }
304    }
305}