use super::config::Config; use super::diagnostics; use super::entities::*; use crate::types; use anyhow::{Context, Result}; use clang::{Clang, CompilationDatabase, Index, TranslationUnit}; use thiserror::Error; use std::convert::{TryFrom, TryInto}; use std::path::{Path, PathBuf}; // TODO: check for use of Extend instead of loop+insert pub(crate) fn parse_compile_commands(config: &Config) -> Result { let clang = Clang::new().unwrap(); let index = Index::new( &clang, /* exclude from pch = */ false, /* print diagnostics = */ false, ); debug!("Extra libclang argument: {:?}", config.extra_args); debug!( "Loading compile commands from: {:?}", config.compile_commands_location ); let database = CompilationDatabase::from_directory(&config.compile_commands_location).map_err(|()| { CompileCommandsLoadError { path: config.compile_commands_location.clone(), } })?; let toplevel_directory = std::env::current_dir().context("Cannot read current directory")?; let mut entities = TopLevel::default(); for command in database.get_all_compile_commands().get_commands() { let directory = command.get_directory(); trace!("Changing directory to: {:?}", directory); std::env::set_current_dir(&directory) .with_context(|| format!("Cannot change current directory to: {:?}", directory))?; let filename = command.get_filename(); trace!("Parsing file: {:?}", filename); // The file name is passed as an argument in the compile commands let mut parser = index.parser(""); parser.skip_function_bodies(true); let mut clang_arguments = command.get_arguments(); clang_arguments.extend_from_slice(&config.extra_args); trace!("Parsing with libclang arguments: {:?}", clang_arguments); parser.arguments(&clang_arguments); parse_unit( &parser .parse() .with_context(|| format!("Could not parse file: {:?}", filename))?, &mut entities, &toplevel_directory, )?; trace!("Changing directory to: {:?}", toplevel_directory); std::env::set_current_dir(&toplevel_directory).with_context(|| { format!( "Cannot change current directory to: {:?}", toplevel_directory ) })?; } Ok(entities.into()) } pub(crate) fn parse_file(path: T, config: &Config) -> Result where T: Into, T: AsRef, T: ToString, { let clang = Clang::new().unwrap(); let index = Index::new(&clang, true, false); // as provided in the command line let filename = path.to_string(); let path = path.as_ref().canonicalize()?; let toplevel_directory = std::env::current_dir().context("Cannot read current directory")?; let maybe_commands = CompilationDatabase::from_directory(&config.compile_commands_location) .and_then(|database| database.get_compile_commands(&path)) .ok(); let maybe_command = maybe_commands .as_ref() .and_then(|commands| commands.get_commands().pop()); let mut clang_arguments = maybe_command .map(|command| command.get_arguments()) .unwrap_or_default(); clang_arguments.extend_from_slice(&config.extra_args); let mut parser = if let Some(command) = maybe_command { let directory = command.get_directory(); trace!("Changing directory to: {:?}", directory); std::env::set_current_dir(&directory) .with_context(|| format!("Cannot change current directory to: {:?}", directory))?; // If it has the parameters provided by the compilation database, // then it knows which file to parse index.parser("") } else { index.parser(path) }; parser.skip_function_bodies(true); parser.arguments(&clang_arguments); trace!("Parsing with libclang arguments: {:?}", clang_arguments); let mut entities = TopLevel::default(); parse_unit( &parser .parse() .with_context(|| format!("Could not parse file: {:?}", filename))?, &mut entities, &toplevel_directory, )?; trace!("Changing directory to: {:?}", toplevel_directory); std::env::set_current_dir(&toplevel_directory).with_context(|| { format!( "Cannot change current directory to: {:?}", toplevel_directory ) })?; Ok(entities.into()) } fn parse_unit( trans_unit: &TranslationUnit, entities: &mut TopLevel, base_dir: impl AsRef, ) -> Result<()> { trans_unit.get_entity().visit_children(|entity, _parent| { if is_in_system_header(entity, &base_dir) { trace!("Entity is in system header, skipping: {:?}", entity); return clang::EntityVisitResult::Continue; } // TODO: wrap this callback in another function so that we can use the // "?" operator instead of all these `match`es let usr = match entity.get_usr() { Some(usr) => usr, None => return clang::EntityVisitResult::Continue, }; trace!("Entity with USR = {:?}", usr); debug!("Parsing toplevel entity: {:?}", entity); add_entity(entity, entities) }); for diagnostic in trans_unit.get_diagnostics() { let diags = diagnostics::clang_diagnostic_to_diagnostics(diagnostic); diagnostics::emit(&diags); /* let sub_diags = diagnostic .get_children() .into_iter() .filter_map(|diagnostic| clang_diag_to_codemap_diag(&diagnostic, file_span)); ); */ } Ok(()) } fn is_in_system_header(entity: clang::Entity, base_dir: impl AsRef) -> bool { if entity.is_in_system_header() { true } else if let Some(location) = entity.get_location() { if let Some(file) = location.get_file_location().file { // !file // .get_path() // .canonicalize() // .unwrap() // .starts_with(base_dir) false } else { // Not defined in a file? probably shouldn't document true } } else { // Not defined anywhere? probably shouldn't document true } } // Entries encountered in the toplevel lexical context fn add_entity(libclang_entity: clang::Entity, toplevel: &mut TopLevel) -> clang::EntityVisitResult { let usr = match libclang_entity.get_usr() { None => return clang::EntityVisitResult::Continue, Some(usr) => usr, }; let kind = match EntityKind::try_from(libclang_entity.get_kind()) { Ok(kind) => kind, Err(err) => { diagnostics::error(format!("{}", err), libclang_entity); return clang::EntityVisitResult::Continue; } }; match toplevel.entry_dyn(libclang_entity) { Err(err) => diagnostics::error(format!("{}", err), libclang_entity), Ok(EntityEntry::Occupied { entity }) => { // if current.has_documentation && !tree.has_documentation { // append_documentation // } // // if !curent.has_children && tree.has_children { // parse(tree) // } } Ok(EntityEntry::Vacant { parent: dyn_parent }) => { if let Err(err) = libclang_entity .try_into() .and_then(|entity| dyn_parent.entity.insert(usr, entity)) { diagnostics::error(format!("{}", err), libclang_entity); return ::clang::EntityVisitResult::Continue; } } } if kind == EntityKind::Namespace && libclang_entity.get_name().is_some() { // Recurse here since namespace definitions are allowed to change between translation units. ::clang::EntityVisitResult::Recurse } else { ::clang::EntityVisitResult::Continue } } pub fn parse_comment(raw: String) -> String { #[derive(Debug)] enum CommentStyle { // Comments of type `/**` or `/*!` Starred, // Comments of type `///` or `//!` SingleLine, } let mut chars = raw.chars(); let style = match &chars.as_str()[..3] { "/*!" | "/**" => CommentStyle::Starred, "///" | "//!" => CommentStyle::SingleLine, _ => panic!("Comment is empty or doesn't start with either `///`, `//!`, `/**`, or `/*!`"), }; chars.nth(2); let mut result = String::new(); 'parse_loop: loop { let maybe_space = chars.next(); let mut empty_line = false; match maybe_space { // TODO: Warn on empty comments None => break, Some(' ') => {} Some('\n') => { empty_line = true; result.push('\n'); } Some(ch) => result.push(ch), } if !empty_line { let rest = chars.as_str(); match rest.find('\n') { None => { result.push_str(rest); break; } Some(position) => { result.push_str(&rest[..=position]); chars.nth(position); } } } // Beginning of the line let first_non_ws_ch = 'ws_loop: loop { let maybe_whitespace = chars.next(); match maybe_whitespace { None => break 'parse_loop, Some(ch) if ch.is_whitespace() => continue, Some(ch) => break 'ws_loop ch, } }; // `chars` is right after the first non whitespace character match style { CommentStyle::Starred if first_non_ws_ch == '*' => { if &chars.as_str()[..1] == "/" { break; } } CommentStyle::Starred => result.push(first_non_ws_ch), CommentStyle::SingleLine => { assert!(first_non_ws_ch == '/'); let rest = chars.as_str(); if &rest[..2] == "//" || &rest[..2] == "/!" { chars.nth(1); } else if &rest[..1] == "/" { chars.nth(0); } else { panic!("Could not parse comment"); } } } } result } #[derive(Debug, Clone, Error)] #[error("Failed to load 'compile_commands.json' at path: {:?}", path)] pub(crate) struct CompileCommandsLoadError { path: PathBuf, }