diff --git a/ginko/src/dts/analysis.rs b/ginko/src/dts/analysis.rs index b84b459..42c9680 100644 --- a/ginko/src/dts/analysis.rs +++ b/ginko/src/dts/analysis.rs @@ -1,18 +1,20 @@ use itertools::Itertools; use crate::dts::ast::{ - ABSOLUTE_ROOT, AbsolutePath, AnyDirective, Cell, DtsFile, Include, LabelRelativePath, Node, - NodeItem, NodeName, NodePayload, Path, Primary, Property, PropertyPath, PropertyValue, - Reference, ReferencedNode, WithToken, + AbsolutePath, AnyDirective, Cell, DtsFile, Include, LabelRelativePath, Node, NodeItem, + NodeName, NodePayload, Path, Primary, Property, PropertyPath, PropertyValue, Reference, + ReferencedNode, WithToken, ABSOLUTE_ROOT, }; use crate::dts::data::{HasSource, HasSpan, Span}; use crate::dts::error_codes::ErrorCode; use crate::dts::import_guard::ImportGuard; use crate::dts::loader::IncludeLoaderGuard; use crate::dts::resolved::ResolvedTree; +use crate::dts::tokens::IncludeKind; use crate::dts::visitor::ReferenceContext; use crate::dts::{Diagnostic, FileType, Position, Project}; -use std::collections::HashMap; +use crate::header::{self, Define, HeaderFile, IncludeStyle, TokenKind}; +use std::collections::{BTreeMap, HashMap}; use std::path::{Path as StdPath, PathBuf}; use std::sync::Arc; @@ -60,6 +62,7 @@ pub(crate) struct Analysis { import_guard: ImportGuard, loader: IncludeLoaderGuard, context: AnalysisContext, + headers: HashMap, } impl Analysis { @@ -76,6 +79,7 @@ impl Analysis { import_guard: ImportGuard::default(), loader: loader.clone(), context, + headers: HashMap::new(), } } @@ -93,6 +97,7 @@ pub struct AnalysisContext { flat_nodes: HashMap>, includes: HashMap, resolved: ResolvedTree, + defines: BTreeMap, } pub(crate) struct FileAnalysisResult { @@ -109,6 +114,10 @@ impl AnalysisContext { self.labels.keys().cloned().collect() } + pub fn get_define(&self, name: &str) -> Option { + self.defines.get(name).cloned() + } + pub fn get_resolved_tree(&self) -> &ResolvedTree { &self.resolved } @@ -269,6 +278,10 @@ impl AnalysisContext { } } } + + pub fn resolve_value(&self, name: &str) -> Option { + crate::header::resolve_value_str(&self.defines, name) + } } struct FileAnalysisContext<'a> { @@ -353,7 +366,6 @@ impl Analysis { self.analyze_referenced_node(&mut ctx, referenced_node.clone()); ctx.first_non_include = true } - Primary::CStyleInclude(_) => {} } } if !ctx.dts_header_seen && ctx.file_type == FileType::DtSource { @@ -373,34 +385,115 @@ impl Analysis { parent: &DtsFile, include: &Include, ) { - let path = match self.loader.load(&parent.source, &include.file_name()) { + let path = match self + .loader + .load(&parent.source, &include.file_name(), include.kind()) + { Ok(path) => path, Err(err) => { ctx.add_diagnostic(Diagnostic::io_error(include.span(), include.source(), err)); return; } }; - if let Err(err) = self - .import_guard - .add(path.clone(), &[parent.source.clone().to_path_buf()]) - { - ctx.add_diagnostic(Diagnostic::cyclic_dependency_error( - include.span(), - include.source(), - err, - )); + if include.follow_as_dts(&path) { + if let Err(err) = self + .import_guard + .add(path.clone(), &[parent.source.clone().to_path_buf()]) + { + ctx.add_diagnostic(Diagnostic::cyclic_dependency_error( + include.span(), + include.source(), + err, + )); + return; + } + let Some(proj_file) = ctx.project.get_file(&path) else { + return; + }; + if proj_file.has_errors(&ctx.project.severities) { + ctx.add_diagnostic(Diagnostic::new( + include.span(), + include.source(), + ErrorCode::ErrorsInInclude, + "Included file contains errors", + )); + } + } + + if include.kind() == IncludeKind::Dts { + // dts-kind includes should not be followed for defines return; } - let Some(proj_file) = ctx.project.get_file(&path) else { + + self.follow_header( + ctx, + path, + &mut vec![], + include.span(), + include.source().clone(), + ); + } + + fn follow_header( + &mut self, + ctx: &mut FileAnalysisContext<'_>, + path: PathBuf, + chain: &mut Vec, + span: Span, + source: Arc, + ) { + let Ok(canonical) = std::fs::canonicalize(&path) else { + // TODO: add diag return; }; - if proj_file.has_errors(&ctx.project.severities) { - ctx.add_diagnostic(Diagnostic::new( - include.span(), - include.source(), - ErrorCode::ErrorsInInclude, - "Included file contains errors", - )); + + // cycle guard + if chain.contains(&canonical) { + return; + } + + if let Some(_header) = self.headers.get(&canonical) { + // header already extracted, will get reused later + } else { + chain.push(canonical.clone()); + let Ok(text) = std::fs::read_to_string(&canonical) else { + ctx.add_diagnostic(Diagnostic::new( + span, + source.clone(), + ErrorCode::IOError, + "failed to read header file", + )); + return; + }; + + let header = header::parse_header(&text, canonical.clone().into()); + for include in &header.includes { + let kind = match include.style { + IncludeStyle::Quoted => IncludeKind::CQuoted, + IncludeStyle::Chevron => IncludeKind::CAngled, + }; + if let Ok(path) = self.loader.load(&canonical, &include.path, kind) { + self.follow_header( + ctx, + path, + chain, + include.target.span(), + include.target.source().clone(), + ); + } + // TODO: handle load failure + } + chain.pop(); + self.headers.insert(canonical.clone(), header); + } + + for define in &self.headers[&canonical].defines { + if let TokenKind::Ident(name) = &define.name.kind { + self.context + .defines + .entry(name.clone()) + .or_insert(define.clone()); + } } } @@ -682,6 +775,16 @@ impl Analysis { Cell::Number(_, _) => {} Cell::Reference(reference) => self.analyze_reference(ctx, reference), Cell::Expression(_) => {} + Cell::Macro(token) => { + if self.context.get_define(token.item()).is_none() { + ctx.add_diagnostic(Diagnostic::new( + token.span(), + token.source(), + ErrorCode::UnknownMacro, + "Unknown macro", + )); + } + } } } @@ -694,7 +797,10 @@ impl Analysis { } fn analyze_incbin(&mut self, ctx: &mut FileAnalysisContext<'_>, include: &Include, span: Span) { - if let Err(err) = self.loader.load(&ctx.source, &include.file_name()) { + if let Err(err) = self + .loader + .load(&ctx.source, &include.file_name(), include.kind()) + { ctx.add_diagnostic(Diagnostic::io_error(span, include.source(), err)); } } @@ -1410,4 +1516,37 @@ labeled_referenced: &some_label { ] ) } + + #[test] + pub fn c_style_include_diagnostic() { + let code = Code::new( + r#" +/dts-v1/; + +#include "x.h" +#include + +/ { +}; +"#, + ); + let (diagnostics, _) = code.get_analyzed_file(); + assert_eq_unordered!( + diagnostics, + vec![ + Diagnostic::new( + code.s1("#include \"x.h\"").span(), + code.source(), + ErrorCode::IOError, + "file not found: x.h" + ), + Diagnostic::new( + code.s1("#include ").span(), + code.source(), + ErrorCode::IOError, + "file not found: x.h" + ), + ] + ) + } } diff --git a/ginko/src/dts/ast.rs b/ginko/src/dts/ast.rs index c58833b..a8a21cc 100644 --- a/ginko/src/dts/ast.rs +++ b/ginko/src/dts/ast.rs @@ -1,6 +1,6 @@ use crate::dts::data::HasSource; -use crate::dts::tokens::Token; -use crate::dts::{HasSpan, Position, Span}; +use crate::dts::tokens::{IncludeKind, IncludeName, Token}; +use crate::dts::{FileType, HasSpan, Position, Span}; use itertools::Itertools; use std::fmt::{Display, Formatter, LowerHex, Write}; use std::ops::Deref; @@ -394,6 +394,7 @@ pub enum Cell { Number(WithToken, NumberRepr), Reference(WithToken), Expression(WithToken), + Macro(WithToken), } impl Cell { @@ -413,6 +414,7 @@ impl Display for Cell { Cell::Number(num, NumberRepr::Decimal) => write!(f, "{num}"), Cell::Reference(reference) => write!(f, "{reference}"), Cell::Expression(exp) => write!(f, "({exp})"), + Cell::Macro(name) => write!(f, "{name}"), } } } @@ -701,7 +703,7 @@ impl Display for DtsFile { #[derive(Eq, PartialEq, Debug, Clone)] pub struct Include { pub include_token: Token, - pub file_name: WithToken, + pub file_name: WithToken, } impl HasSpan for Include { @@ -718,13 +720,33 @@ impl HasSource for Include { impl Display for Include { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "/include/ \"{}\"", self.file_name) + write!( + f, + "{} {}", + self.include_token.kind.stringify(), + self.file_name + ) } } impl Include { pub fn file_name(&self) -> String { - self.file_name.to_string() + self.file_name.path.to_string() + } + + pub fn kind(&self) -> IncludeKind { + self.file_name.kind + } + + pub fn follow_as_dts(&self, resolved: &std::path::Path) -> bool { + match self.kind() { + IncludeKind::Dts => true, + IncludeKind::CAngled => false, + IncludeKind::CQuoted => matches!( + FileType::from(resolved), + FileType::DtSource | FileType::DtSourceInclude | FileType::DtSourceOverlay + ), + } } } @@ -792,8 +814,6 @@ pub enum Primary { Directive(AnyDirective), Root(Arc), ReferencedNode(Arc), - // C-style includes should be put into a separate pass - CStyleInclude(String), } impl Primary { @@ -811,7 +831,6 @@ impl Display for Primary { Primary::Directive(directive) => write!(f, "{directive}"), Primary::Root(node) => write!(f, "{node}"), Primary::ReferencedNode(node) => write!(f, "{node}"), - Primary::CStyleInclude(include) => write!(f, "#include {include}"), } } } diff --git a/ginko/src/dts/diagnostics.rs b/ginko/src/dts/diagnostics.rs index 8405882..0a356cd 100644 --- a/ginko/src/dts/diagnostics.rs +++ b/ginko/src/dts/diagnostics.rs @@ -68,6 +68,10 @@ impl Display for TokenKind { TokenKind::Ref(_) => write!(f, "reference"), TokenKind::Comment(_) => write!(f, "comment"), TokenKind::Unknown(_) => write!(f, "unknown"), + TokenKind::PreprocessorDirective(preprocessor_directive) => { + write!(f, "{preprocessor_directive}") + } + TokenKind::IncludeName(_) => write!(f, "include name"), } } } diff --git a/ginko/src/dts/error_codes.rs b/ginko/src/dts/error_codes.rs index c855d44..ae6d32f 100644 --- a/ginko/src/dts/error_codes.rs +++ b/ginko/src/dts/error_codes.rs @@ -33,6 +33,7 @@ pub enum ErrorCode { NotAPropertyContext, UnexpectedSize, UnexpectedFormat, + UnknownMacro, CustomError, CustomWarning, } @@ -80,6 +81,7 @@ impl Default for SeverityMap { | UnexpectedSize | UnexpectedFormat | DuplicateDirective + | UnknownMacro | CustomWarning => Severity::Warning }; SeverityMap { inner: map } diff --git a/ginko/src/dts/loader.rs b/ginko/src/dts/loader.rs index 25a9a80..2592633 100644 --- a/ginko/src/dts/loader.rs +++ b/ginko/src/dts/loader.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use std::sync::{Arc, Mutex}; use crate::dts::project::{ExternalAnalysis, ExternalAnalysisContext}; +use crate::dts::tokens::IncludeKind; pub enum IncludeLoaderNotifyAction { None, @@ -10,7 +11,12 @@ pub enum IncludeLoaderNotifyAction { } pub trait IncludeLoader { - fn load(&mut self, relative_to: &Path, name: &str) -> Result; + fn load( + &mut self, + relative_to: &Path, + target: &str, + kind: IncludeKind, + ) -> Result; fn notify(&mut self, _path: &Path) -> IncludeLoaderNotifyAction { IncludeLoaderNotifyAction::None @@ -45,9 +51,14 @@ impl IncludeLoaderGuard { } } - pub fn load(&mut self, relative_to: &Path, file_name: &str) -> Result { + pub fn load( + &mut self, + relative_to: &Path, + target: &str, + kind: IncludeKind, + ) -> Result { let mut loader = self.inner.lock().expect("could not lock guard"); - loader.load(relative_to, file_name) + loader.load(relative_to, target, kind) } pub fn notify(&mut self, path: &Path) -> IncludeLoaderNotifyAction { @@ -87,34 +98,109 @@ pub struct DefaultIncludeLoader { } impl IncludeLoader for DefaultIncludeLoader { - fn load(&mut self, relative_to: &Path, file_name: &str) -> Result { - let include_resolved = self.include_paths.iter().find_map(|include_path| { - let full_path = include_path.join(file_name); - if let Ok(true) = std::fs::exists(&full_path) { - Some(full_path) - } else { - None - } - }); - if let Some(include_resolved) = include_resolved { - Ok(include_resolved) - } else { - let full_path = relative_to + fn load( + &mut self, + relative_to: &Path, + target: &str, + kind: IncludeKind, + ) -> Result { + let in_include_paths = || { + self.include_paths.iter().find_map(|include_path| { + let full_path = include_path.join(target); + if let Ok(true) = std::fs::exists(&full_path) { + Some(full_path) + } else { + None + } + }) + }; + + let relative = || { + let full = relative_to .parent() - .map(|p| p.join(file_name)) - .unwrap_or_else(|| PathBuf::from(file_name)); - if let Ok(true) = std::fs::exists(&full_path) { - Ok(full_path) - } else { - Err(io::Error::new( - io::ErrorKind::NotFound, - format!("file not found: {file_name}"), - )) - } - } + .map(|p| p.join(target)) + .unwrap_or_else(|| PathBuf::from(target)); + std::fs::exists(&full).is_ok_and(|v| v).then_some(full) + }; + + let resolved = match kind { + IncludeKind::Dts | IncludeKind::CQuoted => relative().or_else(in_include_paths), + IncludeKind::CAngled => in_include_paths(), + }; + + resolved.ok_or_else(|| { + io::Error::new(io::ErrorKind::NotFound, format!("file not found: {target}")) + }) } fn set_include_paths(&mut self, include_paths: Vec) { self.include_paths = include_paths.iter().map(PathBuf::from).collect(); } } + +#[cfg(test)] +mod tests { + use crate::dts::{DefaultIncludeLoader, IncludeLoader}; + + #[test] + fn loading_priority() { + let root = tempfile::tempdir().unwrap(); + let global_header = root.path().join("header.h"); + std::fs::write(&global_header, b"a").unwrap(); + + let project = tempfile::tempdir().unwrap(); + let local_header = project.path().join("header.h"); + std::fs::write(&local_header, b"a").unwrap(); + + let project_file = project.path().join("device.dts"); + std::fs::write(&project_file, b"a").unwrap(); + + let project_local_header = project.path().join("lheader.h"); + std::fs::write(project_local_header, b"a").unwrap(); + + let mut loader = DefaultIncludeLoader::default(); + + loader.set_include_paths(vec![root.path().to_str().unwrap().to_string()]); + + assert_eq!( + loader + .load( + &project_file, + "header.h", + crate::dts::tokens::IncludeKind::Dts + ) + .unwrap(), + local_header + ); + + assert_eq!( + loader + .load( + &project_file, + "header.h", + crate::dts::tokens::IncludeKind::CQuoted + ) + .unwrap(), + local_header + ); + + assert_eq!( + loader + .load( + &project_file, + "header.h", + crate::dts::tokens::IncludeKind::CAngled + ) + .unwrap(), + global_header + ); + + assert!(loader + .load( + &project_file, + "lheader.h", + crate::dts::tokens::IncludeKind::CAngled + ) + .is_err(),) + } +} diff --git a/ginko/src/dts/mod.rs b/ginko/src/dts/mod.rs index 753c811..16d5d05 100644 --- a/ginko/src/dts/mod.rs +++ b/ginko/src/dts/mod.rs @@ -16,7 +16,7 @@ mod tokens; mod visitor; pub use ast::{AnyDirective, Node, NodeItem, NodePayload, Primary}; -pub use data::{FileType, HasSpan, Position, Span}; +pub use data::{FileType, HasSource, HasSpan, Position, Span}; pub use diagnostics::{Diagnostic, DiagnosticPrinter, Severity}; pub use error_codes::{ErrorCode, SeverityMap}; pub use loader::{ @@ -25,5 +25,6 @@ pub use loader::{ pub use parser::Parser; pub use parser::ParserConfig; pub use project::{ExternalAnalysis, ExternalAnalysisContext, ExternalDiagnostic, Project}; +pub use reader::{ByteReader, Reader}; pub use resolved::{ResolvedContext, ResolvedNode, ResolvedProperty}; pub use visitor::ItemAtCursor; diff --git a/ginko/src/dts/parser.rs b/ginko/src/dts/parser.rs index 47835d7..ac9e027 100644 --- a/ginko/src/dts/parser.rs +++ b/ginko/src/dts/parser.rs @@ -6,12 +6,16 @@ use crate::dts::data::{HasSource, Span}; use crate::dts::diagnostics::{Diagnostic, NameContext}; use crate::dts::error_codes::ErrorCode; use crate::dts::reader::{ByteReader, Reader}; -use crate::dts::tokens::{CompilerDirective, Lexer, PeekingLexer, Reference, Token, TokenKind}; +use crate::dts::tokens::{ + CompilerDirective, Lexer, PeekingLexer, PreprocessorDirective, Reference, Token, TokenKind, +}; use crate::dts::HasSpan; use itertools::Itertools; use std::path::Path as StdPath; use std::sync::Arc; +use super::tokens::IncludeKind; + #[derive(Clone, Default)] pub struct ParserConfig { pub unlimited_property_length: bool, @@ -248,6 +252,7 @@ where )) } } + TokenKind::Ident(name) => Ok(Cell::Macro(WithToken::new(name.clone(), tok))), _ => Err(Diagnostic::expected( tok.span(), tok.source(), @@ -255,6 +260,7 @@ where TokenKind::UnparsedNumber("".to_string()), TokenKind::Ref(Reference::Simple("".to_string())), TokenKind::OpenParen, + TokenKind::Ident("macro".to_string()), ], )), } @@ -452,13 +458,13 @@ where } TokenKind::Directive(CompilerDirective::Incbin) => { let open_paren = self.expect_next_token(TokenKind::OpenParen)?; - let path = self.lexer.expect_next()?; - let string = if let TokenKind::String(string) = &path.kind { + let path_token = self.lexer.expect_next()?; + let string = if let TokenKind::String(string) = &path_token.kind { string.clone() } else { return Err(Diagnostic::expected( - path.span(), - path.source(), + path_token.span(), + path_token.source(), &[TokenKind::String("".to_string())], )); }; @@ -468,7 +474,13 @@ where open_paren, Include { include_token: tok, - file_name: WithToken::new(string, path), + file_name: WithToken::new( + super::tokens::IncludeName { + path: string, + kind: IncludeKind::Dts, + }, + path_token, + ), }, close_paren, )) @@ -577,6 +589,10 @@ where let mut node_discovered = false; loop { let tok = self.lexer.expect_next()?; + if matches!(&tok.kind, TokenKind::Ident(w) if Self::is_cpp_directive(w)) { + self.skip_cpp_directive_line(&tok)?; + continue; + } if tok.kind == TokenKind::Directive(CompilerDirective::DeleteNode) { let node_name = self.node_name()?; self.expect_semicolon()?; @@ -724,12 +740,14 @@ where | TokenKind::UnparsedNumber(_) | TokenKind::UnparsedExpression(_) | TokenKind::Directive(_) + | TokenKind::PreprocessorDirective(_) | TokenKind::Ref(_) | TokenKind::CloseBracket | TokenKind::ChevronRight | TokenKind::CloseBrace | TokenKind::Equal | TokenKind::CloseParen + | TokenKind::IncludeName(_) | TokenKind::ChevronLeft => false, TokenKind::Unknown(_) | TokenKind::Comment(_) => true, }; @@ -751,6 +769,11 @@ where pub fn file(&mut self) -> Result { let mut elements: Vec = vec![]; while self.lexer.peek().is_some() { + if self.peek_is_cpp_directive() { + let token = self.lexer.expect_next()?; + self.skip_cpp_directive_line(&token)?; + continue; + } elements.push(self.primary()?); } Ok(DtsFile { @@ -818,23 +841,12 @@ where Memreserve::new(address, length), ))) } - TokenKind::Ident(str) if str == "#include" => { - let tok = self.lexer.expect_next()?; - match tok.kind { - TokenKind::String(include_str) => Ok(Primary::CStyleInclude(include_str)), - _ => Err(Diagnostic::expected( - tok.span(), - tok.source(), - &[TokenKind::String("".to_string())], - )), - } - } - TokenKind::Directive(CompilerDirective::Include) => { + TokenKind::PreprocessorDirective(PreprocessorDirective::Include) + | TokenKind::Directive(CompilerDirective::Include) => { let include_token = token; let string_tok = self.lexer.expect_next()?; - // let tok_span = string_tok.span(); - let path = match string_tok.kind.clone() { - TokenKind::String(string) => string, + let include = match string_tok.kind.clone() { + TokenKind::IncludeName(include) => include, _ => { return Err(Diagnostic::expected( string_tok.span(), @@ -849,11 +861,11 @@ where tok, ErrorCode::ParserError, "Include directive must not end with a semicolon", - )) + )); } Ok(Primary::Directive(AnyDirective::Include(Include { include_token, - file_name: WithToken::new(path, string_tok.clone()), + file_name: WithToken::new(include, string_tok.clone()), }))) } TokenKind::Slash => { @@ -915,10 +927,47 @@ where )), } } + + fn peek_is_cpp_directive(&mut self) -> bool { + self.lexer.peek().is_some_and( + |tok| matches!(&tok.kind, TokenKind::Ident(w) if Self::is_cpp_directive(w)), + ) + } + fn is_cpp_directive(word: &str) -> bool { + matches!( + word, + "#define" + | "#ifdef" + | "#ifndef" + | "#if" + | "#else" + | "#elif" + | "#endif" + | "#undef" + | "#line" + | "#error" + | "#warning" + | "#pragma" + ) + } + + // skip rest of a line that contained a C directive + fn skip_cpp_directive_line(&mut self, start: &Token) -> Result<()> { + let line = start.span.start().line(); + while let Some(tok) = self.lexer.peek() { + if tok.span.start().line() != line { + break; + } + self.lexer.expect_next()?; + } + Ok(()) + } } #[cfg(test)] mod test { + use itertools::Itertools; + use crate::dts::ast::{ Cell, DtsFile, Include, Memreserve, Node, NodeItem, NodeName, NodePayload, Path, Property, PropertyPath, PropertyValue, Reference, WithToken, @@ -929,7 +978,8 @@ mod test { use crate::dts::parser::Parser; use crate::dts::test::Code; use crate::dts::tokens::CompilerDirective::OmitIfNoRef; - use crate::dts::tokens::TokenKind::{Directive, Equal, OpenBrace, Semicolon}; + use crate::dts::tokens::TokenKind::{self, Directive, Equal, OpenBrace, Semicolon}; + use crate::dts::tokens::{IncludeKind, IncludeName, PreprocessorDirective}; use crate::dts::{AnyDirective, HasSpan, Position, Primary, Span}; use std::sync::Arc; use std::vec; @@ -1041,6 +1091,25 @@ mod test { code.s1(">").token(), ) ); + let code = Code::new(""); + assert_eq!( + code.parse_ok_no_diagnostics(Parser::property_value), + PropertyValue::Cells( + code.s1("<").token(), + vec![ + Cell::Macro(WithToken::new( + "GIC_SPI".to_string(), + code.s1("GIC_SPI").token() + )), + Cell::decimal(WithToken::new(94, code.s1("94").token())), + Cell::Macro(WithToken::new( + "IRQ_TYPE_LEVEL_HIGH".to_string(), + code.s1("IRQ_TYPE_LEVEL_HIGH").token() + )), + ], + code.s1(">").token(), + ) + ); } #[test] @@ -1248,7 +1317,13 @@ mod test { code.s1("(").token(), Include { include_token: code.s1("/").token(), - file_name: WithToken::new("file".into(), code.s1("\"").token(),) + file_name: WithToken::new( + IncludeName { + path: "file".to_string(), + kind: IncludeKind::Dts + }, + code.s1("\"").token(), + ) }, code.s1(")").token() ) @@ -1571,16 +1646,37 @@ mod test { #[test] fn c_style_includes() { - // Should simply parse; we ignore c-style includes for now - let _ = Code::new( + let code = Code::new( "\ /dts-v1/; #include \"some_header\" + #include / {}; ", ) .parse_ok_no_diagnostics(Parser::file); + let includes = code + .elements + .iter() + .filter_map(|f| match f { + Primary::Directive(AnyDirective::Include(incl)) => Some(incl), + _ => None, + }) + .collect_vec(); + assert_eq!( + includes[0].include_token.kind, + TokenKind::PreprocessorDirective(PreprocessorDirective::Include) + ); + assert_eq!(includes[0].file_name.kind, IncludeKind::CQuoted); + assert_eq!(includes[0].file_name.path, "some_header"); + + assert_eq!( + includes[1].include_token.kind, + TokenKind::PreprocessorDirective(PreprocessorDirective::Include) + ); + assert_eq!(includes[1].file_name.kind, IncludeKind::CAngled); + assert_eq!(includes[1].file_name.path, "some_header2"); } #[test] @@ -1807,4 +1903,33 @@ ${/not/allowed} { ) ); } + + #[test] + pub fn include_parsing() { + let code = Code::new( + "\ +/dts-v1/ + +/ { + some_prop = <5> +}", + ); + let (_, diagnostics) = code.parse_ok(Parser::file); + assert_eq!( + diagnostics, + vec![ + Diagnostic::expected( + Position::new(0, 8).as_char_span(), + code.source(), + &[Semicolon], + ), + Diagnostic::expected( + Position::new(3, 19).as_char_span(), + code.source(), + &[Semicolon], + ), + Diagnostic::expected(Position::new(4, 1).as_span(), code.source(), &[Semicolon],), + ] + ) + } } diff --git a/ginko/src/dts/project.rs b/ginko/src/dts/project.rs index 131c8eb..c044bae 100644 --- a/ginko/src/dts/project.rs +++ b/ginko/src/dts/project.rs @@ -344,8 +344,9 @@ impl Project { /// Find the analysis root for a given file. It will either return the file itself, /// or the root of the include-tree it is a part of. fn analysis_root_for(&self, file: &Path) -> Option<(PathBuf, &AnalysisStatus)> { + let canonical = self.canonicalize(file).ok()?; Some({ - let mut current = file; + let mut current = &canonical; loop { let proj_file = self.files.get(current)?; if let Some(parent) = &proj_file.parent { @@ -474,7 +475,7 @@ impl Project { path: &Path, position: &Position, ) -> Option> { - let project_file = self.files.get(path)?; + let project_file = self.get_file(path)?; let reference = project_file.reference_at(position)?; let ctx = self.get_analysis(path)?; @@ -614,9 +615,19 @@ impl Project { loader: &mut IncludeLoaderGuard, ) -> Option { let canonicalized_path = self - .canonicalize(loader.load(parent, &include.file_name()).ok()?) + .canonicalize( + loader + .load(parent, &include.file_name(), include.kind()) + .ok()?, + ) .ok()?; + // parse only dts-like files + // C headers parsed by follow_header + if !include.follow_as_dts(&canonicalized_path) { + return None; + } + // Avoids duplicate insertion and cyclic dependencies if !self.files.contains_key(&canonicalized_path) { match fs::read_to_string(&canonicalized_path) { @@ -1138,6 +1149,54 @@ mod tests { assert!(project.get_file(&file3).is_some()); } + #[test] + pub fn cpp_include_gate() { + let includes_dir = TempDir::new(); + let (_, angled) = includes_dir.add_file("angled.h", "#define B_LOCAL 2"); + let temp_dir = TempDir::new(); + let (_, main) = temp_dir.add_file( + "test.dts", + r#" +/dts-v1/; +#include "frag.dtsi" +#include "head.h" +#include +/ { prop = ; }; +"#, + ); + let (_, frag) = + temp_dir.add_file("frag.dtsi", "#define C_LOCAL 3\n/ { frag_node: frag{ };};"); + let (_, head) = temp_dir.add_file("head.h", "#define A_LOCAL 1"); + + let mut project = Project::default(); + let mut loader = IncludeLoaderGuard::default(); + loader.set_include_paths(vec![angled.parent().unwrap().display().to_string()]); + + project + .add_file( + main.clone().into_os_string().into_string().unwrap(), + &mut loader, + ) + .expect("Cannot add file"); + + // quoted .dtsi fragment is followed (and its own #define is collected) + assert!(project.get_file(&frag).is_some()); + // headers never enter the DTS parse + assert!(project.get_file(&head).is_none()); + assert!(project.get_file(&angled).is_none()); + assert_eq!(project.files().count(), 2); // main + frag only + project.assert_no_diagnostics(); + + let analysis = project.get_analysis(&main).unwrap(); + assert!(project + .get_analysis(&main) + .and_then(|a| a.get_node_by_label("frag_node")) + .is_some()); + assert!(analysis.get_define("A_LOCAL").is_some()); // quoted .h + assert!(analysis.get_define("B_LOCAL").is_some()); // angled via include path + assert!(analysis.get_define("C_LOCAL").is_some()); // defined in the fragment itself + } + #[test] pub fn file_with_dot_relative_references() { let temp_dir = TempDir::new(); @@ -1203,4 +1262,23 @@ mod tests { None => panic!("References does not reference nodes"), } } + + #[test] + #[cfg(unix)] + fn accept_non_canonical_paths() { + use std::os::unix::fs::symlink; + + let temp_dir = TempDir::new(); + let (_, main) = temp_dir.add_file("test.dts", "/dts-v1/;\n/ { prop = <1>; };"); + let link = main.parent().unwrap().join("link.dts"); + symlink(&main, &link).expect("cannot create symlink"); + + let mut project = Project::default(); + let mut loader = IncludeLoaderGuard::default(); + project + .add_file(link.display().to_string(), &mut loader) + .expect("Cannot add file"); + + assert!(project.get_analysis(&link).is_some()); + } } diff --git a/ginko/src/dts/tokens/lexer.rs b/ginko/src/dts/tokens/lexer.rs index 88efc57..4a9975c 100644 --- a/ginko/src/dts/tokens/lexer.rs +++ b/ginko/src/dts/tokens/lexer.rs @@ -1,15 +1,22 @@ use crate::dts::data::{HasSource, Position}; use crate::dts::error_codes::ErrorCode; use crate::dts::reader::{ByteReader, Reader}; -use crate::dts::tokens::token::{Token, TokenKind}; -use crate::dts::tokens::Reference; +use crate::dts::tokens::token::{PreprocessorDirective, Token, TokenKind}; +use crate::dts::tokens::{IncludeKind, Reference}; use crate::dts::Diagnostic; use std::path::Path; use std::sync::Arc; +#[derive(Clone, Copy, Debug)] +enum IncludeOrigin { + DtDirective, + CPreprocessor, +} + enum LexerState { ExpectingNodeOrPropertyName, Other, + ExpectingIncludePath(IncludeOrigin), } pub struct Lexer @@ -170,6 +177,73 @@ where } } + // precondition: cursor is past # + // #include + // ^~~ cursor is here + fn preprocessor_or_ident(&mut self, start: Position) -> Token { + let word = + String::from_utf8(self.read_while( + |ch| matches!(ch, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_' | b'-'), + )) + .unwrap(); + + if word != "include" { + return Token { + kind: TokenKind::Ident(format!("#{word}")), + span: start.to(self.reader.pos()), + source: self.source(), + }; + } + + self.state = LexerState::ExpectingIncludePath(IncludeOrigin::CPreprocessor); + + Token { + kind: TokenKind::PreprocessorDirective(PreprocessorDirective::Include), + span: start.to(self.reader.pos()), + source: self.source(), + } + } + + fn include_name(&mut self, start: Position, origin: IncludeOrigin) -> Token { + self.state = LexerState::ExpectingNodeOrPropertyName; + + let (closing, angled) = match self.reader.peek() { + Some(b'"') => (b'"', false), + Some(b'<') => (b'>', true), + _ => { + return Token { + kind: TokenKind::Unknown(0), + span: start.to(self.reader.pos()), + source: self.source(), + }; + } + }; + + self.reader.skip(); + let path = self.read_while(|ch| ch != b'\n' && ch != closing); + let path = String::from_utf8(path).unwrap(); + self.reader.skip(); + + let kind = match (origin, angled) { + (IncludeOrigin::DtDirective, false) => IncludeKind::Dts, + (IncludeOrigin::CPreprocessor, false) => IncludeKind::CQuoted, + (IncludeOrigin::CPreprocessor, true) => IncludeKind::CAngled, + (IncludeOrigin::DtDirective, true) => { + return Token { + kind: TokenKind::Unknown(0), + span: start.to(self.reader.pos()), + source: self.source(), + }; + } + }; + + return Token { + span: start.to(self.reader.pos()), + kind: TokenKind::IncludeName(super::IncludeName { path, kind }), + source: self.source(), + }; + } + // precondition: cursor is past last slash // // comment // ^~~ cursor is here @@ -229,6 +303,9 @@ where "incbin" => Incbin, other => Other(other.into()), }; + if directive == Include { + self.state = LexerState::ExpectingIncludePath(IncludeOrigin::DtDirective); + } Token { span: pos.to(self.reader.pos()), kind: TokenKind::Directive(directive), @@ -308,8 +385,11 @@ where fn consume(&mut self) -> Option { self.last_pos = self.pos(); self.skip_whitespace(); - let ch = self.reader.peek()?; let start_pos = self.reader.pos(); + if let LexerState::ExpectingIncludePath(origin) = self.state { + return Some(self.include_name(start_pos, origin)); + } + let ch = self.reader.peek()?; let source = self.source(); let simple_token = |kind: TokenKind| -> Option { self.reader.skip(); @@ -321,8 +401,11 @@ where }; use TokenKind::*; if let LexerState::ExpectingNodeOrPropertyName = self.state { - if matches!(ch, b'a'..=b'z' | b'A'..=b'Z' | b',' | b'.' | b'_' | b'+' | b'-' | b'#' | b'?') - { + if ch == b'#' { + self.reader.skip(); + return Some(self.preprocessor_or_ident(start_pos)); + } + if matches!(ch, b'a'..=b'z' | b'A'..=b'Z' | b',' | b'.' | b'_' | b'+' | b'-' | b'?') { return Some(self.ident_or_label(start_pos)); } } diff --git a/ginko/src/dts/tokens/mod.rs b/ginko/src/dts/tokens/mod.rs index d3585c7..dcabd97 100644 --- a/ginko/src/dts/tokens/mod.rs +++ b/ginko/src/dts/tokens/mod.rs @@ -2,4 +2,6 @@ mod lexer; mod token; pub use lexer::{Lexer, PeekingLexer}; -pub use token::{CompilerDirective, Reference, Token, TokenKind}; +pub use token::{ + CompilerDirective, IncludeKind, IncludeName, PreprocessorDirective, Reference, Token, TokenKind, +}; diff --git a/ginko/src/dts/tokens/token.rs b/ginko/src/dts/tokens/token.rs index 688b0c7..e703675 100644 --- a/ginko/src/dts/tokens/token.rs +++ b/ginko/src/dts/tokens/token.rs @@ -40,11 +40,48 @@ pub enum TokenKind { UnparsedNumber(String), UnparsedExpression(String), Directive(CompilerDirective), + PreprocessorDirective(PreprocessorDirective), + IncludeName(IncludeName), Ref(Reference), Comment(String), Unknown(u8), } +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct IncludeName { + pub path: String, + pub kind: IncludeKind, +} + +impl Display for IncludeName { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self.kind { + IncludeKind::CAngled => write!(f, "<{}>", self.path), + _ => write!(f, "\"{}\"", self.path), + } + } +} + +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +pub enum IncludeKind { + Dts, + CQuoted, + CAngled, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum PreprocessorDirective { + Include, +} + +impl Display for PreprocessorDirective { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + PreprocessorDirective::Include => write!(f, "#include"), + } + } +} + impl TokenKind { pub fn stringify(&self) -> String { match self { @@ -66,6 +103,7 @@ impl TokenKind { TokenKind::UnparsedNumber(str) => str.clone(), TokenKind::UnparsedExpression(str) => str.clone(), TokenKind::Directive(compiler_directive) => format!("{}", compiler_directive), + TokenKind::PreprocessorDirective(directive) => format!("{}", directive), TokenKind::Ref(reference) => match reference { Reference::Simple(simple) => format!("&{simple}"), Reference::Path(path) => format!("&{{{path}}}"), @@ -73,6 +111,7 @@ impl TokenKind { }, TokenKind::Comment(comment) => format!("// {comment}\n"), TokenKind::Unknown(u) => format!("{}", *u as char), + TokenKind::IncludeName(include_name) => format!("{}", include_name), } } } diff --git a/ginko/src/dts/visitor.rs b/ginko/src/dts/visitor.rs index e7c58a2..71df017 100644 --- a/ginko/src/dts/visitor.rs +++ b/ginko/src/dts/visitor.rs @@ -19,6 +19,7 @@ pub enum ItemAtCursor<'a> { Reference(&'a Reference, ReferenceContext<'a>), Label(&'a WithToken), Include(&'a Include), + Macro(&'a WithToken), } impl DtsFile { @@ -38,7 +39,6 @@ impl Primary { Primary::Directive(directive) => directive.item_at_cursor(cursor), Primary::Root(root) => root.item_at_cursor(cursor), Primary::ReferencedNode(node) => node.item_at_cursor(cursor), - Primary::CStyleInclude(_) => None, } } } @@ -171,6 +171,11 @@ impl PropertyValue { )); } } + Cell::Macro(name) => { + if name.span().contains(cursor) { + return Some(ItemAtCursor::Macro(name)); + } + } } } None diff --git a/ginko/src/header/lexer.rs b/ginko/src/header/lexer.rs new file mode 100644 index 0000000..5957c10 --- /dev/null +++ b/ginko/src/header/lexer.rs @@ -0,0 +1,388 @@ +use std::{path::Path, sync::Arc}; + +use crate::dts::{ByteReader, HasSource, HasSpan, Position, Reader, Span}; + +#[derive(Eq, PartialEq, Debug, Clone)] +pub enum TokenKind { + Ident(String), + RValue(String), +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct HeaderToken { + pub kind: TokenKind, + pub span: Span, + pub source: Arc, +} + +impl HasSpan for HeaderToken { + fn span(&self) -> Span { + self.span + } +} + +impl HasSource for HeaderToken { + fn source(&self) -> Arc { + self.source.clone() + } +} + +#[derive(Debug, PartialEq, Eq)] +struct Line { + bytes: Vec, + end: Position, +} + +pub(super) struct Lexer { + reader: R, + source: Arc, +} + +impl Lexer { + pub fn from_text(text: impl Into, source: Arc) -> Self { + Self { + reader: ByteReader::from_string(text.into()), + source, + } + } + + /// Returns next found directive identifier or None on EOF + /// # as a standalone keyword tolerated + pub fn next_directive(&mut self) -> Option { + loop { + self.skip_whitespace(); + if self.reader.peek() != Some(b'#') { + self.read_till_newline()?; + continue; + } + let start = self.reader.pos(); + self.reader.skip(); + let ident = self.read_while(Self::is_ident); + if ident.is_empty() { + self.read_till_newline()?; + continue; + } + let token = HeaderToken { + kind: TokenKind::Ident(String::from_utf8(ident).unwrap()), + span: start.to(self.reader.pos()), + source: self.source.clone(), + }; + return Some(token); + } + } + + /// Returns the first identifier after a directive or none when the identifier is empty + pub fn name(&mut self) -> Option { + self.skip_whitespace(); + let start = self.reader.pos(); + let ident = self.read_while(Self::is_ident); + if ident.is_empty() { + return None; + } + + Some(HeaderToken { + kind: TokenKind::Ident(String::from_utf8(ident).unwrap()), + span: start.to(self.reader.pos()), + source: self.source.clone(), + }) + } + + /// Returns the remaining directive contents until a terminating newline is found. + pub fn rvalue(&mut self) -> Option { + self.skip_whitespace(); + let start = self.reader.pos(); + let Line { bytes, end } = self.read_till_newline()?; + // An empty rest-of-line is legal (a body-less `#define`, or an + // argument-less directive like `#endif`) and yields an empty span; + // `Position::to` asserts on equal positions, hence the special case. + let span = if end > start { + start.to(end) + } else { + Span::new(start, end) + }; + + Some(HeaderToken { + kind: TokenKind::RValue(String::from_utf8(bytes).unwrap()), + span, + source: self.source.clone(), + }) + } +} + +impl Lexer { + fn read_while(&mut self, f: impl Fn(u8) -> bool) -> Vec { + let mut vec = vec![]; + while let Some(c) = self.reader.peek() { + if f(c) { + vec.push(c); + self.reader.skip(); + } else { + break; + } + } + vec + } + + fn is_whitespace(c: u8) -> bool { + c == b' ' || c == b'\t' + } + + fn is_ident(c: u8) -> bool { + c == b'_' || c.is_ascii_alphanumeric() + } + + fn read_till_newline(&mut self) -> Option { + let mut line = { + let mut vec = vec![]; + let mut end; + loop { + end = self.reader.pos(); + let Some(c) = self.reader.consume() else { + if vec.is_empty() { + break None; + } else { + break Some((vec, end)); + } + }; + + match c { + b'\n' => { + break Some((vec, end)); + } + b'\\' => { + let mut skipped = vec![]; + while let Some(c) = self.reader.peek() { + if Self::is_whitespace(c) { + skipped.push(c); + self.reader.skip(); + } else { + break; + } + } + if let Some(b'\n') = self.reader.peek() { + self.reader.skip(); + } else { + vec.push(c); + vec.append(&mut skipped); + } + continue; + } + b'/' if let Some(b'/') = self.reader.peek() => { + while self.reader.skip_if(|c| c != b'\n') {} + end = self.reader.pos(); + self.reader.skip(); // \n + break Some((vec, end)); + } + b'/' if let Some(b'*') = self.reader.peek() => { + self.reader.skip(); // * + loop { + match self.reader.consume() { + Some(b'*') if self.reader.peek() == Some(b'/') => { + self.reader.skip(); + break; + } + Some(_) => {} + None => break, + } + } + } + c => { + vec.push(c); + } + } + } + }; + + if let Some((vec, _)) = line.as_mut() { + // remove trailing whitespace + // end not modified deliberately to highlight the whitespace as well + while matches!(vec.last(), Some(b' ' | b'\t' | b'\r')) { + vec.pop(); + } + } + + return line.map(|(bytes, end)| Line { bytes, end }); + } + + fn skip_whitespace(&mut self) { + while self.reader.skip_if(Self::is_whitespace) {} + } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use super::{Lexer, Line, TokenKind}; + use crate::dts::{ByteReader, Position}; + + #[test] + fn smoke() { + let code = r#" +#define A 10 +#define B (1<<10) +#define F(a) a +#include "h.h" +#include + "#; + + macro_rules! tokens { + ($dir:literal, $value:literal) => { + [ + Some(TokenKind::Ident($dir.to_string())), + None, + Some(TokenKind::RValue($value.to_string())), + ] + }; + ($dir:literal, $name:literal, $value:literal) => { + [ + Some(TokenKind::Ident($dir.to_string())), + Some(TokenKind::Ident($name.to_string())), + Some(TokenKind::RValue($value.to_string())), + ] + }; + } + + let mut lexer = Lexer::from_text(code.to_string(), Path::new("a").into()); + + macro_rules! assert_next_directive { + ($dir:literal, $name: literal) => { + assert_eq!( + [ + lexer.next_directive().map(|dir| dir.kind), + lexer.name().map(|n| n.kind), + lexer.rvalue().map(|v| v.kind) + ], + tokens!($dir, $name) + ) + }; + ($dir:literal, $name: literal, $value:literal) => { + assert_eq!( + [ + lexer.next_directive().map(|dir| dir.kind), + lexer.name().map(|n| n.kind), + lexer.rvalue().map(|v| v.kind) + ], + tokens!($dir, $name, $value) + ) + }; + } + + assert_next_directive!("define", "A", "10"); + assert_next_directive!("define", "B", "(1<<10)"); + assert_next_directive!("define", "F", "(a) a"); + assert_next_directive!("include", "\"h.h\""); + assert_next_directive!("include", ""); + } + + #[test] + fn collapser() { + let input = r#" +A +B\ +C D +// comment +E F\ +G + + +Y Z +void main() {} +main("\n") +/** + * + + */ + "#; + let mut lex = Lexer { + reader: ByteReader::from_string(input.to_string()), + source: Path::new("a").into(), + }; + + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: vec![], + end: Position::new(0, 0) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: vec![b'A'], + end: Position::new(1, 1) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: b"BC D".to_vec(), + end: Position::new(3, 3) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: vec![], + end: Position::new(4, 10) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: b"E FG".to_vec(), + end: Position::new(6, 1) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: vec![], + end: Position::new(7, 0) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: vec![], + end: Position::new(8, 0) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: b"Y Z".to_vec(), + end: Position::new(9, 3) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: b"void main() {}".to_vec(), + end: Position::new(10, 14) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: b"main(\"\\n\")".to_vec(), + end: Position::new(11, 10) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: vec![], + end: Position::new(15, 3) + }) + ); + assert_eq!( + lex.read_till_newline(), + Some(Line { + bytes: vec![], + end: Position::new(16, 8) + }) + ); + assert_eq!(lex.read_till_newline(), None) + } +} diff --git a/ginko/src/header/mod.rs b/ginko/src/header/mod.rs new file mode 100644 index 0000000..9167cd0 --- /dev/null +++ b/ginko/src/header/mod.rs @@ -0,0 +1,341 @@ +use std::{ + collections::{BTreeMap, BTreeSet}, + path::Path, + sync::Arc, +}; + +use crate::dts::HasSpan; + +mod lexer; + +use lexer::Lexer; +pub use lexer::{HeaderToken, TokenKind}; + +/// A `#define` in a header file. +/// +/// The body is the raw replacement text — splices folded, comments stripped. +/// The goal of the LSP is to provide navigation/completion, not to reimplement c/cpp preprocessor. +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Define { + pub name: HeaderToken, + pub body: String, + pub fn_like: bool, +} + +pub fn resolve_value_str(defines: &BTreeMap, name: &str) -> Option { + let mut visited = BTreeSet::new(); + let mut current = name.to_string(); + + loop { + let mut body = current.trim(); + while let Some(inner) = strip_circumfix(body, '(', ')') { + body = inner.trim(); + } + if !visited.insert(body.to_string()) { + return None; + } + if parse_number(body).is_some() { + return Some(body.to_string()); + } + if let Some(define) = defines.get(body) { + if define.fn_like { + return None; + } + current = define.body.clone(); + } else { + return None; + } + } +} + +fn parse_number(input: &str) -> Option { + let input = input.trim_end_matches(|c| matches!(c, 'u' | 'U' | 'l' | 'L')); + if let Some(hex) = input + .strip_prefix("0x") + .or_else(|| input.strip_prefix("0X")) + { + u64::from_str_radix(hex, 16).ok() + } else { + input.parse().ok() + } +} + +#[derive(Eq, PartialEq, Debug, Clone, Copy)] +pub enum IncludeStyle { + /// `#include "x.h"` + Quoted, + /// `#include ` + Chevron, +} + +#[derive(Eq, PartialEq, Debug, Clone)] +pub struct Include { + pub directive: HeaderToken, + pub target: HeaderToken, + pub path: String, + pub style: IncludeStyle, +} + +/// The result of scanning a header file. +/// +/// Pure data: no I/O. +#[derive(Eq, PartialEq, Debug, Clone, Default)] +pub struct HeaderFile { + pub includes: Vec, + pub defines: Vec, +} + +/// Strip a matching pair of delimiters, e.g. `` → `x.h`. +/// Returns `None` if the text is not delimited by the given pair. +fn strip_circumfix(text: &str, open: char, close: char) -> Option<&str> { + if !text.starts_with(open) || !text.ends_with(close) { + return None; + } + text.get(open.len_utf8()..text.len() - close.len_utf8()) +} + +/// Scan a C header for `#define` and `#include` directives. +/// +/// Unknown directives and non-directive lines are skipped silently — the +/// scanner is not a C diagnostics engine. An `#include` whose target is not +/// a plain quoted/chevron literal (e.g. `#include MACRO_HEADER`) cannot be +/// followed and is dropped. +pub fn parse_header(text: &str, source: Arc) -> HeaderFile { + let mut includes = Vec::new(); + let mut defines = Vec::new(); + + let mut lexer = Lexer::from_text(text, source); + + while let Some(directive) = lexer.next_directive() { + let dir = if let TokenKind::Ident(dir) = &directive.kind { + dir + } else { + unreachable!("next_directive only yields Ident tokens") + }; + + match dir.as_str() { + "define" => { + let Some(name) = lexer.name() else { + // `#define` with no macro name: nothing to record + let _ = lexer.rvalue(); + continue; + }; + let body_token = lexer.rvalue(); + let body = body_token + .as_ref() + .map(|rv| match &rv.kind { + TokenKind::RValue(body) => body.clone(), + _ => unreachable!("rvalue only yields RValue tokens"), + }) + .unwrap_or_default(); + let adjacent = body_token + .as_ref() + .map(|rv| rv.span().start() == name.span().end()) + .unwrap_or(false); + let fn_like = body.starts_with('(') && adjacent; + defines.push(Define { + name, + body, + fn_like, + }); + } + "include" => { + // A missing target (`#include` at EOF) is not an input error; + // there is simply nothing to record. + let Some(target_token) = lexer.rvalue() else { + continue; + }; + let TokenKind::RValue(target) = &target_token.kind else { + continue; + }; + let (style, path) = if target.starts_with('<') { + (IncludeStyle::Chevron, strip_circumfix(target, '<', '>')) + } else { + (IncludeStyle::Quoted, strip_circumfix(target, '"', '"')) + }; + // End the borrow of `target` before moving `target_token`. + let Some(path) = path.map(str::to_string) else { + continue; + }; + includes.push(Include { + directive, + target: target_token, + path, + style, + }); + } + // Any other directive (`#ifndef`, `#if`, `#pragma`, line + // markers, ...): consume the rest of its line and ignore it. + _ => { + let _ = lexer.rvalue(); + } + } + } + + HeaderFile { includes, defines } +} + +#[cfg(test)] +mod test { + use std::path::Path; + + use super::*; + use crate::dts::{Position, Span}; + + fn source() -> Arc { + Path::new("a.h").into() + } + + fn ident(name: &str) -> TokenKind { + TokenKind::Ident(name.to_string()) + } + + #[test] + fn scan_assembles() { + let code = r#" +#define A 10 +#define F(a) a +#define ALIAS A +#define EMPTY +#include +#include "y.h" +#include MACRO +#pragma once +#endif +just some C code, not a directive +"#; + let file = parse_header(code, source()); + + assert_eq!(file.defines.len(), 4); + assert_eq!(file.defines[0].name.kind, ident("A")); + assert_eq!(file.defines[0].body, "10"); + assert!(!file.defines[0].fn_like); + assert_eq!(file.defines[1].name.kind, ident("F")); + assert_eq!(file.defines[1].body, "(a) a"); + assert!(file.defines[1].fn_like); + assert_eq!(file.defines[2].name.kind, ident("ALIAS")); + assert_eq!(file.defines[2].body, "A"); + assert!(!file.defines[2].fn_like); + // `#define EMPTY`: the empty rest-of-line must not panic on an + // empty span (`#endif` below exercises the same path). + assert_eq!(file.defines[3].name.kind, ident("EMPTY")); + assert_eq!(file.defines[3].body, ""); + + assert_eq!(file.includes.len(), 2); + assert_eq!(file.includes[0].path, "dt-bindings/x.h"); + assert_eq!(file.includes[0].style, IncludeStyle::Chevron); + assert_eq!(file.includes[1].path, "y.h"); + assert_eq!(file.includes[1].style, IncludeStyle::Quoted); + } + + #[test] + fn define_spans_and_fn_like_adjacency() { + let file = parse_header("#define F(a) a\n#define V 10\n", source()); + + // function-like: the '(' is adjacent to the name + assert_eq!( + file.defines[0].name.span(), + Span::new(Position::new(0, 8), Position::new(0, 9)) + ); + assert!(file.defines[0].fn_like); + + // object-like: whitespace between name and body ⇒ not fn-like + assert_eq!( + file.defines[1].name.span(), + Span::new(Position::new(1, 8), Position::new(1, 9)) + ); + assert_eq!(file.defines[1].body, "10"); + assert!(!file.defines[1].fn_like); + } + + #[test] + fn include_tokens() { + let file = parse_header("#include \"y.h\"", source()); + let include = &file.includes[0]; + // The directive token covers the whole `#include` keyword — the + // span starts at the `#`. + assert_eq!( + include.directive.span(), + Span::new(Position::new(0, 0), Position::new(0, 8)) + ); + assert_eq!( + include.target.span(), + Span::new(Position::new(0, 9), Position::new(0, 14)) + ); + assert_eq!(include.path, "y.h"); + assert_eq!(include.style, IncludeStyle::Quoted); + } + + #[test] + fn define_value_lookup() { + macro_rules! btmap { + [$(#define $name:literal $body:literal)+] => { + { + + let mut defines = BTreeMap::new(); + $( + defines.insert($name.to_string(), + Define { + name: HeaderToken { + kind: TokenKind::Ident($name.to_string()), + span: Span::new(Position::zero(), Position::zero()), + source: Path::new("a").into(), + }, + body: $body.to_string(), + fn_like: false, + }, + ); + )+ + defines + } + }; + } + + let defines = btmap!( + #define "A" "B" + #define "B" "C" + #define "C" "10" + ); + + let resolved = resolve_value_str(&defines, "A"); + + assert_eq!(resolved, Some("10".to_string())); + + let defines = btmap!( + #define "A" "B" + #define "B" "C" + #define "C" "A" + ); + + let resolved = resolve_value_str(&defines, "A"); + + assert_eq!(resolved, None); + + let defines = btmap!( + #define "A" "F(x) 3" + #define "F(x)" "1" + ); + + let resolved = resolve_value_str(&defines, "A"); + + assert_eq!(resolved, None); + + let defines = btmap!( + #define "A" "B" + #define "B" "C + 1" + ); + + let resolved = resolve_value_str(&defines, "B"); + + assert_eq!(resolved, None); + + let defines = btmap!( + #define "A" "B" + #define "B" "(0x42UL)" + ); + + let resolved = resolve_value_str(&defines, "A"); + + assert_eq!(resolved, Some("0x42UL".to_string())); + } +} diff --git a/ginko/src/lib.rs b/ginko/src/lib.rs index 664d030..b5a271a 100644 --- a/ginko/src/lib.rs +++ b/ginko/src/lib.rs @@ -1,2 +1,4 @@ #![warn(rust_2018_idioms)] pub mod dts; + +pub mod header; diff --git a/ginko_ls/src/main.rs b/ginko_ls/src/main.rs index 9fb8c15..1f21ee8 100644 --- a/ginko_ls/src/main.rs +++ b/ginko_ls/src/main.rs @@ -11,7 +11,8 @@ struct Args { #[tokio::main] pub async fn main() { - Args::parse(); + let args = Args::parse(); + let include_paths = args.include.unwrap_or_default(); // Log to stderr instead of stdout since communication with the language server client // happens through stdout tracing_subscriber::fmt() @@ -20,6 +21,8 @@ pub async fn main() { let (stdin, stdout) = (tokio::io::stdin(), tokio::io::stdout()); - let (service, socket) = LspService::new(|client| Backend::new("ginko_ls", client)); + let (service, socket) = LspService::new(move |client| { + Backend::new("ginko_ls", client).with_include_paths(include_paths) + }); Server::new(stdin, stdout, socket).serve(service).await; } diff --git a/ginko_server/src/lib.rs b/ginko_server/src/lib.rs index 8aa51d7..850e1c7 100644 --- a/ginko_server/src/lib.rs +++ b/ginko_server/src/lib.rs @@ -73,6 +73,14 @@ impl Backend { self } + /// Configure the include search paths (the `-i/--include` values passed + /// to the binary). Without this, CLI-provided paths never reach the + /// project's include loader. + pub fn with_include_paths(self, include_paths: Vec) -> Self { + self.loader.write().set_include_paths(include_paths); + self + } + pub fn with_parser_config(self, config: ParserConfig) -> Self { self.project.write().set_parser_config(config); self @@ -476,17 +484,30 @@ impl LanguageServer for Backend { match self .loader .write() - .load(&file_path, &include.file_name()) + .load(&file_path, &include.file_name(), include.kind()) .ok() .and_then(|path| Url::from_file_path(path).ok()) { Some(url) => Ok(Some(GotoDefinitionResponse::Scalar(Location::new( url, - ginko_span_to_range(include.span()), + Range::new(Position::new(0, 0), Position::new(0, 0)), )))), _ => Ok(None), } } + ItemAtCursor::Macro(token) => { + let Some(define) = project + .get_analysis(&file_path) + .and_then(|ctx| ctx.get_define(token.item())) + else { + return Ok(None); + }; + + Ok(Some(GotoDefinitionResponse::Scalar(Location::new( + Url::from_file_path(&define.name.source).unwrap(), + ginko_span_to_range(define.name.span()), + )))) + } _ => Ok(None), } } @@ -515,7 +536,24 @@ impl LanguageServer for Backend { project.document_reference(&file_path, reference, &ctx) } ItemAtCursor::Label(name) => Some(name.item().clone()), - ItemAtCursor::Include(include) => Some(include.file_name.item().clone()), + ItemAtCursor::Include(include) => Some(include.file_name.item().clone().path), + ItemAtCursor::Macro(token) => 'block: { + let Some(ctx) = project.get_analysis(&file_path) else { + break 'block None; + }; + let Some(define) = ctx.get_define(token.item()) else { + break 'block None; + }; + let base = format!("#define {} {}", token.item(), define.body); + + match ctx.resolve_value(token) { + Some(resolved) if resolved != define.body => { + Some(format!("{}\n{}", base, resolved)) + } + Some(_) | None => Some(base), + } + } + _ => None, }) else { return Ok(None);