├── .gitignore ├── .travis.yml ├── src ├── lib.rs ├── fsutil.rs ├── main.rs ├── snippet.rs ├── config.rs ├── writer.rs └── parser.rs ├── LICENSE-MIT ├── Cargo.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | .vs 5 | /.idea 6 | Cargo.lock 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - stable 4 | script: 5 | - rustup component add rustfmt 6 | - cargo build 7 | - cargo test 8 | - cargo build --features="binaries" 9 | - cargo test --features="binaries" -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | use crate::proc_macro::TokenStream; 4 | 5 | #[proc_macro_attribute] 6 | pub fn snippet(_attr: TokenStream, item: TokenStream) -> TokenStream { 7 | // Just register `snippet` attribute. 8 | // We do nothing here. 9 | item 10 | } 11 | -------------------------------------------------------------------------------- /src/fsutil.rs: -------------------------------------------------------------------------------- 1 | use std::env; 2 | use std::fs; 3 | use std::path::PathBuf; 4 | 5 | // Find project root directory from current directory 6 | pub fn project_root_path() -> Option { 7 | env::current_dir().ok().and_then(|mut cwd| loop { 8 | cwd.push("Cargo.toml"); 9 | if fs::metadata(cwd.as_path()) 10 | .map(|meta| meta.is_file()) 11 | .unwrap_or(false) 12 | { 13 | cwd.pop(); 14 | return Some(cwd); 15 | } 16 | 17 | cwd.pop(); 18 | if !cwd.pop() { 19 | return None; 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 hatoo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "cargo-snippet" 3 | version = "0.6.5" 4 | authors = ["hatoo "] 5 | repository = "https://github.com/hatoo/cargo-snippet.git" 6 | keywords = ["subcommand", "cargo-subcommand", "cargo", "snippet", "competitive"] 7 | license = "MIT" 8 | readme = "README.md" 9 | description = "A snippet extractor for competitive programmers" 10 | categories = ["template-engine"] 11 | edition = "2018" 12 | 13 | [lib] 14 | proc-macro = true 15 | path = "src/lib.rs" 16 | 17 | [[bin]] 18 | name = "cargo-snippet" 19 | path = "src/main.rs" 20 | required-features = ["binaries"] 21 | 22 | [dependencies] 23 | syn = { version = "1", features = ["full", "parsing", "extra-traits", "printing"], optional = true } 24 | quote = { version = "1", optional = true } 25 | proc-macro2 = { version = "1", optional = true } 26 | rustfmt-nightly = { version = "1", optional = true } 27 | glob = { version = "0.3", optional = true } 28 | clap = { version = "2.29", optional = true } 29 | serde = { version = "1.0", optional = true} 30 | serde_derive = { version = "1.0", optional = true} 31 | serde_json = { version = "1.0", optional = true} 32 | log = { version= "0.4", optional = true } 33 | env_logger = { version= "0.7", optional = true } 34 | regex = { version = "1.3.5", optional = true } 35 | lazy_static = { version = "1.4.0", optional = true } 36 | 37 | [features] 38 | binaries = ["syn", "quote", "proc-macro2", "glob", "clap", "serde", "serde_derive", "serde_json", "log", "env_logger", "regex", "lazy_static"] 39 | inner_rustfmt = ["rustfmt-nightly"] 40 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | mod config; 2 | mod fsutil; 3 | mod parser; 4 | mod snippet; 5 | mod writer; 6 | 7 | use std::fs; 8 | use std::io::Read; 9 | 10 | use clap::{crate_authors, crate_version, App, AppSettings, Arg, SubCommand}; 11 | use log::error; 12 | 13 | use std::error::Error; 14 | 15 | /// Report error and continue. 16 | fn report_error(result: Result) -> Option { 17 | match result { 18 | Ok(x) => Some(x), 19 | Err(e) => { 20 | error!("{}", e); 21 | None 22 | } 23 | } 24 | } 25 | 26 | fn main() { 27 | env_logger::init(); 28 | 29 | // Setup for cargo subcommand 30 | let matches = App::new("cargo-snippet") 31 | .version(crate_version!()) 32 | .bin_name("cargo") 33 | .settings(&[AppSettings::GlobalVersion, AppSettings::SubcommandRequired]) 34 | .subcommand( 35 | SubCommand::with_name("snippet") 36 | .author(crate_authors!()) 37 | .about("Extract code snippet from cargo projects") 38 | .arg(Arg::with_name("PATH").multiple(true).help( 39 | "The files or directories (including children) \ 40 | to extract snippet (defaults to /src when omitted)", 41 | )) 42 | .arg( 43 | Arg::with_name("output_type") 44 | .long("type") 45 | .short("t") 46 | .default_value("neosnippet") 47 | .possible_values(&["neosnippet", "vscode", "ultisnips"]), 48 | ), 49 | ) 50 | .get_matches(); 51 | 52 | let config = config::Config::from_matches(&matches); 53 | 54 | // Alphabetical order 55 | let mut snippets = Vec::new(); 56 | 57 | let mut buf = String::new(); 58 | for path in config.target.iter_paths() { 59 | buf.clear(); 60 | log::info!("Start read {:?}", &path); 61 | if let Some(mut file) = report_error(fs::File::open(path)) { 62 | if report_error(file.read_to_string(&mut buf)).is_some() { 63 | if let Some(mut parsed) = report_error(parser::parse_snippet(&buf)) { 64 | snippets.append(&mut parsed); 65 | } 66 | } 67 | } 68 | } 69 | 70 | config 71 | .output_type 72 | .write(&snippet::process_snippets(&snippets)); 73 | } 74 | -------------------------------------------------------------------------------- /src/snippet.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, BTreeSet, HashSet}; 2 | 3 | #[derive(Debug)] 4 | pub struct SnippetAttributes { 5 | // A snippet with multiple names is allowed but using dependency is recommended. 6 | pub names: HashSet, 7 | // Dependencies 8 | pub uses: HashSet, 9 | // Prefix for snippet. It's will be emitted prior to the snippet. 10 | pub prefix: String, 11 | // Whether doc comments associated with this snippet should be hidden or not. 12 | pub doc_hidden: bool, 13 | } 14 | 15 | #[derive(Debug)] 16 | pub struct Snippet { 17 | pub attrs: SnippetAttributes, 18 | // Snippet content (Not formated) 19 | pub content: String, 20 | } 21 | 22 | pub fn process_snippets(snips: &[Snippet]) -> BTreeMap { 23 | #[derive(Default, Clone, Debug)] 24 | struct Snip { 25 | prefix: String, 26 | content: String, 27 | } 28 | 29 | let mut pre: BTreeMap = BTreeMap::new(); 30 | let mut deps: BTreeMap> = BTreeMap::new(); 31 | 32 | for snip in snips { 33 | for name in &snip.attrs.names { 34 | let s = pre.entry(name.clone()).or_default(); 35 | s.prefix += &snip.attrs.prefix; 36 | s.content += &snip.content; 37 | 38 | for dep in &snip.attrs.uses { 39 | deps.entry(name.clone()) 40 | .or_insert_with(BTreeSet::new) 41 | .insert(dep.clone()); 42 | } 43 | } 44 | } 45 | 46 | let mut res: BTreeMap = BTreeMap::new(); 47 | 48 | for (name, uses) in &deps { 49 | let mut used = HashSet::new(); 50 | used.insert(name.clone()); 51 | let mut stack = uses.iter().cloned().collect::>(); 52 | 53 | while let Some(dep) = stack.pop() { 54 | if !used.contains(&dep) { 55 | used.insert(dep.clone()); 56 | if let Some(c) = &pre.get(&dep) { 57 | // *res.entry(name.clone()).or_insert_with(String::new) += c.as_str(); 58 | let s = res.entry(name.clone()).or_default(); 59 | s.prefix += &c.prefix; 60 | s.content += &c.content; 61 | 62 | if let Some(ds) = deps.get(&dep) { 63 | for d in ds { 64 | if !used.contains(d) { 65 | stack.push(d.clone()); 66 | } 67 | } 68 | } 69 | } else { 70 | log::warn!("Dependency {} is missing", &dep); 71 | } 72 | } 73 | } 74 | } 75 | 76 | for (name, snip) in pre { 77 | // Dependency first 78 | let s = res.entry(name).or_default(); 79 | s.prefix += snip.prefix.as_str(); 80 | s.content += snip.content.as_str(); 81 | } 82 | 83 | res.into_iter() 84 | .map(|(k, v)| (k, v.prefix + v.content.as_str())) 85 | .collect() 86 | } 87 | -------------------------------------------------------------------------------- /src/config.rs: -------------------------------------------------------------------------------- 1 | use clap::ArgMatches; 2 | use std::collections::BTreeMap; 3 | use std::fs; 4 | use std::iter; 5 | use std::path::{Path, PathBuf}; 6 | 7 | use crate::fsutil; 8 | use crate::writer; 9 | use glob::glob; 10 | 11 | #[derive(Debug)] 12 | pub struct Config<'a> { 13 | pub target: Target<'a>, 14 | pub output_type: OutputType, 15 | } 16 | 17 | #[derive(Debug)] 18 | pub enum Target<'a> { 19 | // /src. Default 20 | ProjectSrc, 21 | // Args 22 | Paths(Vec<&'a str>), 23 | } 24 | 25 | #[derive(Debug)] 26 | pub enum OutputType { 27 | Neosnippet, 28 | VScode, 29 | Ultisnips, 30 | } 31 | 32 | impl<'a> Config<'a> { 33 | pub fn from_matches(matches: &'a ArgMatches) -> Self { 34 | Config { 35 | target: Target::from_matches(matches), 36 | output_type: OutputType::from_matches(matches), 37 | } 38 | } 39 | } 40 | 41 | impl<'a> Target<'a> { 42 | fn from_matches(matches: &'a ArgMatches) -> Self { 43 | matches 44 | .subcommand_matches("snippet") 45 | .and_then(|m| { 46 | m.values_of("PATH") 47 | .map(|path| Target::Paths(path.collect())) 48 | }) 49 | .unwrap_or(Target::ProjectSrc) 50 | } 51 | 52 | pub fn iter_paths(&self) -> Box + 'a> { 53 | match self { 54 | Target::ProjectSrc => fsutil::project_root_path() 55 | .and_then(|mut path| { 56 | path.push("src"); 57 | path.push("**"); 58 | path.push("*.rs"); 59 | glob(&format!("{}", path.display())).ok().map(|paths| { 60 | Box::new(paths.filter_map(|e| e.ok())) as Box> 61 | }) 62 | }) 63 | .unwrap_or_else(|| Box::new(iter::empty())), 64 | Target::Paths(ref v) => Box::new( 65 | v.clone() 66 | .into_iter() 67 | .filter_map(|s| { 68 | fs::metadata(Path::new(s)).ok().and_then(|meta| { 69 | let path = if meta.is_dir() { 70 | let mut path = Path::new(s).to_path_buf(); 71 | path.push("**"); 72 | path.push("*.rs"); 73 | path 74 | } else { 75 | Path::new(s).to_path_buf() 76 | }; 77 | glob(&format!("{}", path.display())) 78 | .ok() 79 | .map(|paths| paths.filter_map(|e| e.ok())) 80 | }) 81 | }) 82 | .flat_map(|i| i), 83 | ), 84 | } 85 | } 86 | } 87 | 88 | impl OutputType { 89 | fn from_matches(matches: &ArgMatches) -> Self { 90 | matches 91 | .subcommand_matches("snippet") 92 | .and_then(|m| { 93 | m.value_of("output_type").map(|t| match t { 94 | "vscode" => OutputType::VScode, 95 | "ultisnips" => OutputType::Ultisnips, 96 | _ => OutputType::Neosnippet, 97 | }) 98 | }) 99 | .unwrap_or(OutputType::Neosnippet) 100 | } 101 | 102 | pub fn write(&self, snippets: &BTreeMap) { 103 | match self { 104 | OutputType::Neosnippet => { 105 | writer::write_neosnippet(snippets); 106 | } 107 | OutputType::VScode => { 108 | writer::write_vscode(snippets); 109 | } 110 | OutputType::Ultisnips => { 111 | writer::write_ultisnips(snippets); 112 | } 113 | } 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /src/writer.rs: -------------------------------------------------------------------------------- 1 | use serde_derive::Serialize; 2 | use std::collections::BTreeMap; 3 | 4 | #[derive(Serialize)] 5 | struct VScode { 6 | prefix: String, 7 | body: Vec, 8 | } 9 | 10 | #[cfg(feature = "inner_rustfmt")] 11 | pub fn format_src(src: &str) -> Option { 12 | let mut rustfmt_config = rustfmt_nightly::Config::default(); 13 | rustfmt_config 14 | .set() 15 | .emit_mode(rustfmt_nightly::EmitMode::Stdout); 16 | rustfmt_config 17 | .set() 18 | .verbose(rustfmt_nightly::Verbosity::Quiet); 19 | 20 | let mut out = Vec::with_capacity(src.len() * 2); 21 | let input = rustfmt_nightly::Input::Text(src.into()); 22 | 23 | if rustfmt_nightly::Session::new(rustfmt_config, Some(&mut out)) 24 | .format(input) 25 | .is_ok() 26 | { 27 | String::from_utf8(out).ok().map(|s| s.replace("\r\n", "\n")) 28 | } else { 29 | None 30 | } 31 | } 32 | 33 | #[cfg(not(feature = "inner_rustfmt"))] 34 | pub fn format_src(src: &str) -> Option { 35 | use std::io::Write; 36 | use std::process; 37 | 38 | let mut command = process::Command::new("rustfmt") 39 | .stdin(process::Stdio::piped()) 40 | .stdout(process::Stdio::piped()) 41 | .stderr(process::Stdio::piped()) 42 | .spawn() 43 | .expect("Failed to spawn rustfmt process"); 44 | { 45 | let mut stdin = command.stdin.take()?; 46 | write!(stdin, "{}", src).unwrap(); 47 | } 48 | let out = command.wait_with_output().ok()?; 49 | 50 | if !out.status.success() { 51 | log::error!("rustfmt returns non-zero status"); 52 | log::error!("[stdout]\n{}", String::from_utf8_lossy(&out.stdout)); 53 | log::error!("[stderr]\n{}", String::from_utf8_lossy(&out.stderr)); 54 | return None; 55 | } 56 | 57 | let stdout = out.stdout; 58 | let out = String::from_utf8(stdout).ok()?; 59 | Some(out.replace("\r\n", "\n")) 60 | } 61 | 62 | pub fn write_neosnippet(snippets: &BTreeMap) { 63 | for (name, content) in snippets.iter() { 64 | if let Some(formatted) = format_src(content) { 65 | println!("snippet {}", name); 66 | for line in formatted.lines() { 67 | println!(" {}", line); 68 | } 69 | println!(); 70 | } 71 | } 72 | } 73 | 74 | pub fn write_vscode(snippets: &BTreeMap) { 75 | let vscode: BTreeMap = snippets 76 | .iter() 77 | .filter_map(|(name, content)| { 78 | format_src(content).map(|formatted| { 79 | ( 80 | name.to_owned(), 81 | VScode { 82 | prefix: name.to_owned(), 83 | body: formatted 84 | .lines() 85 | .map(|l| 86 | // Escape "$" to disable placeholder 87 | l.to_owned().replace("$", "\\$")) 88 | .collect(), 89 | }, 90 | ) 91 | }) 92 | }) 93 | .collect(); 94 | 95 | if let Ok(json) = serde_json::to_string_pretty(&vscode) { 96 | println!("{}", json); 97 | } 98 | } 99 | 100 | pub fn write_ultisnips(snippets: &BTreeMap) { 101 | for (name, content) in snippets.iter() { 102 | if let Some(formatted) = format_src(content) { 103 | println!("snippet {}", name); 104 | print!("{}", formatted); 105 | println!("endsnippet"); 106 | println!(); 107 | } 108 | } 109 | } 110 | 111 | #[test] 112 | fn test_format_src() { 113 | assert_eq!(format_src("fn foo(){}"), Some("fn foo() {}\n".into())); 114 | 115 | assert_eq!( 116 | format_src("/// doc comment\n pub fn foo(){}"), 117 | Some("/// doc comment\npub fn foo() {}\n".into()) 118 | ); 119 | } 120 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cargo-snippet 2 | 3 | [![crates.io](https://img.shields.io/crates/v/cargo-snippet.svg)](https://crates.io/crates/cargo-snippet) 4 | [![Build Status](https://travis-ci.org/hatoo/cargo-snippet.svg?branch=master)](https://travis-ci.org/hatoo/cargo-snippet) 5 | [![dependency status](https://deps.rs/repo/github/hatoo/cargo-snippet/status.svg)](https://deps.rs/repo/github/hatoo/cargo-snippet) 6 | 7 | A snippet extractor for competitive programmers. 8 | 9 | This allows you to manage your code snippets with tests and benchmarks available !! 10 | 11 | ## Installing 12 | 13 | You need to install `rustfmt` to run `cargo-snippet`. 14 | 15 | ```bash 16 | $ rustup component add rustfmt 17 | ``` 18 | 19 | Install `cargo-snippet` 20 | 21 | ```bash 22 | $ cargo install cargo-snippet --features="binaries" 23 | ``` 24 | 25 | ## Usage 26 | 27 | Create a project for snippet. 28 | 29 | ``` 30 | $ cargo new --lib mysnippet 31 | ``` 32 | 33 | Add dependencies to Cargo.toml. 34 | 35 | ```toml 36 | [dependencies] 37 | cargo-snippet = "0.6" 38 | ``` 39 | 40 | Note: `cargo-snippet` on dependencies is needed just for register `#[snippet]` attribute to prevent the error from the compiler. 41 | All logics that extract snippet is in the binary package which is installed by `Installing` section. 42 | 43 | Then write some snippet codes and tests. 44 | 45 | ```rust 46 | use cargo_snippet::snippet; 47 | 48 | // Annotate snippet name 49 | #[snippet("mymath")] 50 | #[snippet("gcd")] 51 | fn gcd(a: u64, b: u64) -> u64 { 52 | if b == 0 { 53 | a 54 | } else { 55 | gcd(b, a % b) 56 | } 57 | } 58 | 59 | // Also works 60 | #[snippet(name = "mymath")] 61 | // Equivalent to #[snippet("lcm")] 62 | #[snippet] 63 | fn lcm(a: u64, b: u64) -> u64 { 64 | a / gcd(a, b) * b 65 | } 66 | 67 | #[snippet] 68 | // Include snippet 69 | #[snippet(include = "gcd")] 70 | fn gcd_list(list: &[u64]) -> u64 { 71 | list.iter().fold(list[0], |a, &b| gcd(a, b)) 72 | } 73 | 74 | // You can set prefix string. 75 | // Note: All codes will be formatted by rustfmt on output 76 | #[snippet(prefix = "use std::io::{self,Read};")] 77 | #[snippet(prefix = "use std::str::FromStr;")] 78 | fn foo() {} 79 | 80 | // By default, doc comments associated with items will be output with the snippet. 81 | #[snippet] 82 | /// This is a document! 83 | fn documented() { 84 | //! Inner document also works. 85 | } 86 | 87 | // If you want doc comment to be hidden, append `doc_hidden` keyword. 88 | #[snippet(doc_hidden, prefix = "use std::collections::HashMap;")] 89 | /// This is a doc comment for `bar`. 90 | /// Since `doc_hidden` is specified, it won't be present in the snippet. 91 | fn bar() { 92 | //! And this is also a doc comment for `bar`, which will be removed. 93 | } 94 | 95 | #[test] 96 | fn test_gcd() { 97 | assert_eq!(gcd(57, 3), 3); 98 | } 99 | 100 | #[test] 101 | fn test_lcm() { 102 | assert_eq!(lcm(3, 19), 57); 103 | } 104 | ``` 105 | 106 | You can test as always: 107 | 108 | ``` 109 | $ cargo test 110 | ``` 111 | 112 | Extract snippet ! 113 | 114 | ``` 115 | $ cargo snippet 116 | snippet foo 117 | use std::io::{self, Read}; 118 | use std::str::FromStr; 119 | fn foo() {} 120 | 121 | snippet documented 122 | /// This is a document! 123 | fn documented() { 124 | //! Inner document also works. 125 | } 126 | 127 | snippet bar 128 | use std::collections::HashMap; 129 | fn bar() {} 130 | 131 | snippet gcd 132 | fn gcd(a: u64, b: u64) -> u64 { 133 | if b == 0 { 134 | a 135 | } else { 136 | gcd(b, a % b) 137 | } 138 | } 139 | 140 | snippet gcd_list 141 | fn gcd(a: u64, b: u64) -> u64 { 142 | if b == 0 { 143 | a 144 | } else { 145 | gcd(b, a % b) 146 | } 147 | } 148 | fn gcd_list(list: &[u64]) -> u64 { 149 | list.iter().fold(list[0], |a, &b| gcd(a, b)) 150 | } 151 | 152 | snippet lcm 153 | fn lcm(a: u64, b: u64) -> u64 { 154 | a / gcd(a, b) * b 155 | } 156 | 157 | snippet mymath 158 | fn gcd(a: u64, b: u64) -> u64 { 159 | if b == 0 { 160 | a 161 | } else { 162 | gcd(b, a % b) 163 | } 164 | } 165 | fn lcm(a: u64, b: u64) -> u64 { 166 | a / gcd(a, b) * b 167 | } 168 | 169 | ``` 170 | 171 | ## Example 172 | 173 | My snippets [here](https://github.com/hatoo/competitive-rust-snippets.git). 174 | 175 | ## Supported output format 176 | 177 | * Neosnippet 178 | * VScode 179 | * Ultisnips 180 | 181 | You can specify output format via `-t` option. 182 | See `cargo snippet -h`. 183 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | use lazy_static::lazy_static; 2 | use proc_macro2::{Delimiter, TokenStream, TokenTree}; 3 | use quote::ToTokens; 4 | use regex::{Captures, Regex}; 5 | use syn::{parse_file, Attribute, File, Item, Meta, MetaList, NestedMeta}; 6 | 7 | use crate::snippet::{Snippet, SnippetAttributes}; 8 | use std::collections::HashSet; 9 | use std::{char, u32}; 10 | 11 | fn is_snippet_path(path: &str) -> bool { 12 | match path { 13 | "snippet" | "cargo_snippet :: snippet" => true, 14 | _ => false, 15 | } 16 | } 17 | 18 | macro_rules! get_attrs_impl { 19 | ($arg: expr, $($v: path), *) => { 20 | { 21 | match $arg { 22 | $( 23 | $v(ref x) => Some(&x.attrs), 24 | )* 25 | _ => None 26 | } 27 | } 28 | } 29 | } 30 | 31 | fn get_attrs(item: &Item) -> Option<&Vec> { 32 | // All Item variants except Item::Verbatim 33 | get_attrs_impl!( 34 | item, 35 | Item::ExternCrate, 36 | Item::Use, 37 | Item::Static, 38 | Item::Const, 39 | Item::Fn, 40 | Item::Mod, 41 | Item::ForeignMod, 42 | Item::Type, 43 | Item::Struct, 44 | Item::Enum, 45 | Item::Union, 46 | Item::Trait, 47 | Item::Impl, 48 | Item::Macro, 49 | Item::Macro2 50 | ) 51 | } 52 | 53 | macro_rules! remove_snippet_attr_impl { 54 | ($arg: expr, $($v: path), *) => { 55 | { 56 | match $arg { 57 | $( 58 | $v(ref mut x) => { 59 | x.attrs.retain(|attr| { 60 | attr.parse_meta().map(|m| !is_snippet_path(m.path().to_token_stream().to_string().as_str())).unwrap_or(true) 61 | }); 62 | }, 63 | )* 64 | _ => () 65 | } 66 | } 67 | } 68 | } 69 | 70 | fn remove_snippet_attr(item: &mut Item) { 71 | remove_snippet_attr_impl!( 72 | item, 73 | Item::ExternCrate, 74 | Item::Use, 75 | Item::Static, 76 | Item::Const, 77 | Item::Fn, 78 | Item::Mod, 79 | Item::ForeignMod, 80 | Item::Type, 81 | Item::Struct, 82 | Item::Enum, 83 | Item::Union, 84 | Item::Trait, 85 | Item::Impl, 86 | Item::Macro, 87 | Item::Macro2 88 | ); 89 | 90 | if let Item::Mod(ref mut item_mod) = item { 91 | if let Some(&mut (_, ref mut items)) = item_mod.content.as_mut() { 92 | items.iter_mut().for_each(|item| remove_snippet_attr(item)); 93 | } 94 | } 95 | } 96 | 97 | fn unquote(s: &str) -> String { 98 | let chars: Vec = s.chars().collect(); 99 | 100 | if chars.len() >= 2 && chars.first() == Some(&'"') && chars.last() == Some(&'"') { 101 | chars[1..chars.len() - 1].iter().collect() 102 | } else { 103 | chars.iter().collect() 104 | } 105 | } 106 | 107 | macro_rules! get_default_snippet_name_impl { 108 | ($arg:expr, $($v: path), *) => { 109 | match $arg { 110 | $( 111 | $v(ref x) => { 112 | Some(x.ident.to_string()) 113 | }, 114 | )* 115 | Item::Fn(ref x) => { 116 | Some(x.sig.ident.to_string()) 117 | } 118 | _ => None 119 | } 120 | }; 121 | } 122 | 123 | fn get_default_snippet_name(item: &Item) -> Option { 124 | get_default_snippet_name_impl!( 125 | item, 126 | Item::Static, 127 | Item::Const, 128 | Item::Mod, 129 | Item::Struct, 130 | Item::Enum, 131 | Item::Union, 132 | Item::Trait 133 | ) 134 | } 135 | 136 | fn get_snippet_name(attr: &Attribute) -> Option { 137 | attr.parse_meta().ok().and_then(|metaitem| { 138 | if !is_snippet_path(metaitem.path().to_token_stream().to_string().as_str()) { 139 | return None; 140 | } 141 | 142 | match metaitem { 143 | // #[snippet(name="..")] 144 | Meta::List(list) => list 145 | .nested 146 | .iter() 147 | .filter_map(|item| match item { 148 | NestedMeta::Meta(Meta::NameValue(ref nv)) => { 149 | if nv.path.to_token_stream().to_string() == "name" { 150 | Some(unquote(&nv.lit.clone().into_token_stream().to_string())) 151 | } else { 152 | None 153 | } 154 | } 155 | NestedMeta::Lit(lit) => { 156 | Some(unquote(lit.to_token_stream().to_string().as_str())) 157 | } 158 | _ => None, 159 | }) 160 | .next(), 161 | // #[snippet=".."] 162 | Meta::NameValue(nv) => Some(unquote(&nv.lit.into_token_stream().to_string())), 163 | _ => None, 164 | } 165 | }) 166 | } 167 | 168 | fn get_snippet_uses(attr: &Attribute) -> Option> { 169 | attr.parse_meta().ok().and_then(|metaitem| { 170 | if !is_snippet_path(metaitem.path().to_token_stream().to_string().as_str()) { 171 | return None; 172 | } 173 | 174 | match metaitem { 175 | // #[snippet(include="..")] 176 | Meta::List(list) => list 177 | .nested 178 | .iter() 179 | .filter_map(|item| { 180 | if let NestedMeta::Meta(Meta::NameValue(ref nv)) = item { 181 | // It can't use "use" keyword here xD. 182 | // It is reserved. 183 | if nv.path.to_token_stream().to_string() == "include" { 184 | let uses = unquote(&nv.lit.clone().into_token_stream().to_string()); 185 | Some( 186 | uses.split(',') 187 | .map(|s| s.trim()) 188 | .filter(|s| !s.is_empty()) 189 | .map(|s| s.to_string()) 190 | .collect(), 191 | ) 192 | } else { 193 | None 194 | } 195 | } else { 196 | None 197 | } 198 | }) 199 | .next(), 200 | _ => None, 201 | } 202 | }) 203 | } 204 | 205 | fn get_simple_attr(attr: &Attribute, key: &str) -> Vec { 206 | attr.parse_meta() 207 | .ok() 208 | .and_then(|metaitem| { 209 | if !is_snippet_path(metaitem.path().to_token_stream().to_string().as_str()) { 210 | return None; 211 | } 212 | 213 | match metaitem { 214 | // #[snippet(`key`="..")] 215 | Meta::List(list) => list 216 | .nested 217 | .iter() 218 | .filter_map(|item| { 219 | if let NestedMeta::Meta(Meta::NameValue(ref nv)) = item { 220 | if nv.path.to_token_stream().to_string() == key { 221 | let value = if let syn::Lit::Str(s) = &nv.lit.clone() { 222 | s.value() 223 | } else { 224 | panic!("attribute must be string"); 225 | }; 226 | Some(value) 227 | } else { 228 | None 229 | } 230 | } else { 231 | None 232 | } 233 | }) 234 | .collect::>() 235 | .into(), 236 | _ => None, 237 | } 238 | }) 239 | .unwrap_or(Vec::new()) 240 | } 241 | 242 | fn parse_attrs( 243 | attrs: &[Attribute], 244 | default_snippet_name: Option, 245 | ) -> Option { 246 | let meta_parsed = attrs 247 | .iter() 248 | .filter_map(|a| a.parse_meta().ok()) 249 | .map(|m| { 250 | let is_snippet_path = is_snippet_path(m.path().to_token_stream().to_string().as_str()); 251 | (m, is_snippet_path) 252 | }) 253 | .collect::>(); 254 | 255 | if meta_parsed 256 | .iter() 257 | .all(|&(_, is_snippet_path)| !is_snippet_path) 258 | { 259 | return None; 260 | } 261 | 262 | let mut names = attrs 263 | .iter() 264 | .filter_map(get_snippet_name) 265 | .collect::>(); 266 | 267 | let attr_snippet_without_value = meta_parsed.iter().any(|(meta, is_snippet_path)| { 268 | if !is_snippet_path { 269 | return false; 270 | } 271 | matches!(meta, Meta::Path(_)) 272 | }); 273 | 274 | if attr_snippet_without_value { 275 | if let Some(ref default) = default_snippet_name { 276 | names.insert(default.clone()); 277 | } 278 | } 279 | 280 | if names.is_empty() { 281 | if let Some(default) = default_snippet_name { 282 | names.insert(default); 283 | } else { 284 | return None; 285 | } 286 | } 287 | 288 | let uses = attrs 289 | .iter() 290 | .filter_map(get_snippet_uses) 291 | .flat_map(|v| v.into_iter()) 292 | .collect::>(); 293 | 294 | let prefix = attrs 295 | .iter() 296 | .map(|attr| get_simple_attr(attr, "prefix").into_iter()) 297 | .flatten() 298 | .collect::>() 299 | .join("\n"); 300 | 301 | let doc_hidden = meta_parsed.iter().any(|(meta, is_snippet_path)| { 302 | if !is_snippet_path { 303 | return false; 304 | } 305 | match meta { 306 | Meta::List(MetaList { ref nested, .. }) => nested.iter().any(|n| 307 | matches!(n, NestedMeta::Meta(Meta::Path(ref p)) if p.to_token_stream().to_string() == "doc_hidden") 308 | ), 309 | _ => false, 310 | } 311 | }); 312 | 313 | Some(SnippetAttributes { 314 | names, 315 | uses, 316 | prefix, 317 | doc_hidden, 318 | }) 319 | } 320 | 321 | fn next_token_is_doc(token: &TokenTree) -> bool { 322 | match token { 323 | TokenTree::Group(ref g) => g.to_string().starts_with("[doc = "), 324 | _ => false, 325 | } 326 | } 327 | 328 | fn unescape(s: impl Into) -> String { 329 | lazy_static! { 330 | static ref ESCAPED_UNICODE: Regex = Regex::new(r"\\u\{([0-9a-fA-F]{1,6})\}").unwrap(); 331 | } 332 | let s = s.into(); 333 | let unicode_unescaped: Vec = ESCAPED_UNICODE 334 | .replace_all(&s, |caps: &Captures| { 335 | caps.get(1) 336 | .and_then(|cap| u32::from_str_radix(cap.as_str(), 16).ok()) 337 | .and_then(|u| char::from_u32(u)) 338 | .map(|ch| ch.to_string()) 339 | .unwrap_or(caps[0].to_string()) 340 | }) 341 | .chars() 342 | .collect(); 343 | 344 | let mut ret = String::with_capacity(s.len()); 345 | let mut iter = unicode_unescaped.iter().peekable(); 346 | while let Some(&ch) = iter.next() { 347 | if ch == '\\' { 348 | match iter.peek() { 349 | Some(&next_ch) if *next_ch == '\\' => { 350 | ret.push('\\'); 351 | iter.next(); 352 | } 353 | Some(&next_ch) if *next_ch == '"' => { 354 | ret.push('"'); 355 | iter.next(); 356 | } 357 | Some(&next_ch) if *next_ch == 't' => { 358 | ret.push('\t'); 359 | iter.next(); 360 | } 361 | Some(&next_ch) if *next_ch == 'n' => { 362 | ret.push('\n'); 363 | iter.next(); 364 | } 365 | Some(&next_ch) if *next_ch == 'r' => { 366 | ret.push('\r'); 367 | iter.next(); 368 | } 369 | _ => unreachable!(), 370 | } 371 | } else { 372 | ret.push(ch); 373 | } 374 | } 375 | ret 376 | } 377 | 378 | fn format_doc_comment(doc_tt: TokenTree, is_inner: bool, doc_hidden: bool) -> Option { 379 | lazy_static! { 380 | static ref DOC_RE: Regex = Regex::new(r#"^\[doc = "(?s)(.*)"\]$"#).unwrap(); 381 | } 382 | if doc_hidden { 383 | return None; 384 | } 385 | 386 | let doc = unescape(doc_tt.to_string()); 387 | DOC_RE 388 | .captures(doc.as_str()) 389 | .and_then(|caps| caps.get(1)) 390 | .map(|c| { 391 | c.as_str().lines().fold(String::new(), |mut acc, line| { 392 | let s = if is_inner { 393 | format!("//!{}\n", line) 394 | } else { 395 | format!("///{}\n", line) 396 | }; 397 | acc.push_str(&s); 398 | acc 399 | }) 400 | }) 401 | } 402 | 403 | fn stringify_tokens(tokens: TokenStream, doc_hidden: bool) -> String { 404 | let mut res = String::new(); 405 | let mut iter = tokens.into_iter().peekable(); 406 | while let Some(tok) = iter.next() { 407 | match tok { 408 | TokenTree::Punct(ref punct) => { 409 | if punct.as_char() == '!' && iter.peek().map(next_token_is_doc).unwrap_or(false) { 410 | // inner doc comment here. 411 | // `res` already has a `#` character at the last, which is unnecessary, so remove it by calling pop. 412 | if res.chars().last() == Some(' ') { 413 | res.pop(); 414 | } 415 | assert_eq!(res.pop(), Some('#')); 416 | if let Some(doc) = 417 | format_doc_comment(iter.next().unwrap(), true, doc_hidden).as_deref() 418 | { 419 | res.push_str(doc); 420 | } 421 | } else if punct.as_char() == '#' 422 | && iter.peek().map(next_token_is_doc).unwrap_or(false) 423 | { 424 | // outer doc comment here. 425 | if let Some(doc) = 426 | format_doc_comment(iter.next().unwrap(), false, doc_hidden).as_deref() 427 | { 428 | res.push_str(doc); 429 | } 430 | } else { 431 | res.push_str(tok.to_string().as_str()); 432 | if punct.spacing() == proc_macro2::Spacing::Alone { 433 | res.push(' '); 434 | } 435 | } 436 | } 437 | TokenTree::Group(ref g) => { 438 | match g.delimiter() { 439 | Delimiter::Parenthesis => res.push('('), 440 | Delimiter::Brace => res.push('{'), 441 | Delimiter::Bracket => res.push('['), 442 | Delimiter::None => (), 443 | } 444 | res.push_str(stringify_tokens(g.stream(), doc_hidden).as_str()); 445 | match g.delimiter() { 446 | Delimiter::Parenthesis => res.push(')'), 447 | Delimiter::Brace => res.push('}'), 448 | Delimiter::Bracket => res.push(']'), 449 | Delimiter::None => (), 450 | } 451 | res.push(' '); 452 | } 453 | _ => { 454 | res.push_str(tok.to_string().as_str()); 455 | res.push(' '); 456 | } 457 | } 458 | } 459 | res 460 | } 461 | 462 | // Get snippet names and snippet code (not formatted) 463 | fn get_snippet_from_item(mut item: Item) -> Option { 464 | let default_name = get_default_snippet_name(&item); 465 | let snip_attrs = get_attrs(&item).and_then(|attrs| parse_attrs(attrs.as_slice(), default_name)); 466 | 467 | snip_attrs.map(|attrs| { 468 | remove_snippet_attr(&mut item); 469 | let doc_hidden = attrs.doc_hidden; 470 | Snippet { 471 | attrs, 472 | content: stringify_tokens(item.into_token_stream(), doc_hidden), 473 | } 474 | }) 475 | } 476 | 477 | fn get_snippet_from_item_recursive(item: Item) -> Vec { 478 | let mut res = Vec::new(); 479 | 480 | if let Some(pair) = get_snippet_from_item(item.clone()) { 481 | res.push(pair); 482 | } 483 | 484 | if let Item::Mod(mod_item) = item { 485 | res.extend( 486 | mod_item 487 | .content 488 | .into_iter() 489 | .flat_map(|(_, items)| items.into_iter().flat_map(get_snippet_from_item_recursive)), 490 | ); 491 | } 492 | 493 | res 494 | } 495 | 496 | fn get_snippet_from_file(file: File) -> Vec { 497 | let mut res = Vec::new(); 498 | 499 | // whole code is snippet 500 | if let Some(attrs) = parse_attrs(&file.attrs, None) { 501 | let mut file = file.clone(); 502 | file.attrs.retain(|attr| { 503 | attr.parse_meta() 504 | .map(|m| !is_snippet_path(m.path().to_token_stream().to_string().as_str())) 505 | .unwrap_or(true) 506 | }); 507 | file.items.iter_mut().for_each(|item| { 508 | remove_snippet_attr(item); 509 | }); 510 | let doc_hidden = attrs.doc_hidden; 511 | res.push(Snippet { 512 | attrs, 513 | content: stringify_tokens(file.into_token_stream(), doc_hidden), 514 | }) 515 | } 516 | 517 | res.extend( 518 | file.items 519 | .into_iter() 520 | .flat_map(get_snippet_from_item_recursive), 521 | ); 522 | 523 | res 524 | } 525 | 526 | pub fn parse_snippet(src: &str) -> Result, syn::parse::Error> { 527 | parse_file(src).map(get_snippet_from_file) 528 | } 529 | 530 | #[cfg(test)] 531 | mod test { 532 | use super::{parse_snippet, unescape}; 533 | use crate::snippet::process_snippets; 534 | use crate::writer::format_src; 535 | use quote::quote; 536 | use std::collections::BTreeMap; 537 | 538 | fn snippets(src: &str) -> BTreeMap { 539 | let snips = parse_snippet(src).unwrap(); 540 | process_snippets(&snips) 541 | } 542 | 543 | #[test] 544 | fn test_no_snippet() { 545 | let src = r#" 546 | fn test() {} 547 | "#; 548 | 549 | let snip = snippets(&src); 550 | assert_eq!(snip.get("test"), None); 551 | } 552 | 553 | #[test] 554 | fn test_parse_simple_case() { 555 | let src = r#" 556 | #[snippet("test")] 557 | fn test() {} 558 | "#; 559 | 560 | let snip = snippets(&src); 561 | 562 | assert_eq!( 563 | snip.get("test").and_then(|s| format_src(s)), 564 | format_src( 565 | "e!( 566 | fn test() {} 567 | ) 568 | .to_string() 569 | ) 570 | ); 571 | } 572 | 573 | #[test] 574 | fn test_multiple_annotaton() { 575 | { 576 | let src = r#" 577 | #[snippet("test1")] 578 | #[snippet("test2")] 579 | fn test() {} 580 | "#; 581 | 582 | let snip = snippets(&src); 583 | 584 | assert_eq!( 585 | snip.get("test1").and_then(|s| format_src(s)), 586 | format_src( 587 | "e!( 588 | fn test() {} 589 | ) 590 | .to_string() 591 | ) 592 | ); 593 | assert_eq!( 594 | snip.get("test2").and_then(|s| format_src(s)), 595 | format_src( 596 | "e!( 597 | fn test() {} 598 | ) 599 | .to_string() 600 | ) 601 | ); 602 | } 603 | 604 | { 605 | let src = r#" 606 | #![snippet("test1")] 607 | #![snippet("test2")] 608 | 609 | fn test() {} 610 | "#; 611 | 612 | let snip = snippets(&src); 613 | 614 | assert_eq!( 615 | snip.get("test1").and_then(|s| format_src(s)), 616 | format_src( 617 | "e!( 618 | fn test() {} 619 | ) 620 | .to_string() 621 | ) 622 | ); 623 | assert_eq!( 624 | snip.get("test2").and_then(|s| format_src(s)), 625 | format_src( 626 | "e!( 627 | fn test() {} 628 | ) 629 | .to_string() 630 | ) 631 | ); 632 | } 633 | 634 | { 635 | let src = r#" 636 | #[snippet] 637 | #[snippet("bar2")] 638 | fn bar() {} 639 | "#; 640 | 641 | let snip = snippets(&src); 642 | assert_eq!( 643 | snip.get("bar").and_then(|s| format_src(s)), 644 | format_src( 645 | "e!( 646 | fn bar() {} 647 | ) 648 | .to_string() 649 | ) 650 | ); 651 | assert_eq!( 652 | snip.get("bar2").and_then(|s| format_src(s)), 653 | format_src( 654 | "e!( 655 | fn bar() {} 656 | ) 657 | .to_string() 658 | ) 659 | ); 660 | } 661 | } 662 | 663 | #[test] 664 | fn test_deep() { 665 | let src = r#" 666 | #[snippet("bar")] 667 | fn bar() {} 668 | 669 | #[snippet("foo")] 670 | mod foo { 671 | #[snippet("hoge")] 672 | fn hoge() {} 673 | } 674 | "#; 675 | 676 | let snip = snippets(&src); 677 | 678 | assert_eq!( 679 | snip.get("bar").and_then(|s| format_src(s)), 680 | format_src( 681 | "e!( 682 | fn bar() {} 683 | ) 684 | .to_string() 685 | ) 686 | ); 687 | assert_eq!( 688 | snip.get("foo").and_then(|s| format_src(s)), 689 | // #[snippet("hoge")] should be removed. 690 | format_src( 691 | "e!( 692 | mod foo { 693 | fn hoge() {} 694 | } 695 | ) 696 | .to_string() 697 | ) 698 | ); 699 | assert_eq!( 700 | snip.get("hoge").and_then(|s| format_src(s)), 701 | format_src( 702 | "e!( 703 | fn hoge() {} 704 | ) 705 | .to_string() 706 | ) 707 | ); 708 | } 709 | 710 | #[test] 711 | fn test_default_snippet_name() { 712 | let src = r#" 713 | #[snippet] 714 | fn bar() {} 715 | 716 | #[snippet] 717 | struct Baz(); 718 | "#; 719 | 720 | let snip = snippets(&src); 721 | assert_eq!( 722 | snip.get("bar").and_then(|s| format_src(s)), 723 | format_src( 724 | "e!( 725 | fn bar() {} 726 | ) 727 | .to_string() 728 | ) 729 | ); 730 | assert_eq!( 731 | snip.get("Baz").and_then(|s| format_src(s)), 732 | format_src( 733 | "e!( 734 | struct Baz(); 735 | ) 736 | .to_string() 737 | ) 738 | ); 739 | } 740 | 741 | #[test] 742 | fn test_snippet_dependency() { 743 | let src = r#" 744 | #[snippet("bar")] 745 | fn bar() {} 746 | 747 | #[snippet(name = "baz", include = "bar")] 748 | fn baz() {} 749 | "#; 750 | 751 | let snip = snippets(&src); 752 | assert_eq!( 753 | snip.get("bar").and_then(|s| format_src(s)), 754 | format_src( 755 | "e!( 756 | fn bar() {} 757 | ) 758 | .to_string() 759 | ) 760 | ); 761 | assert_eq!( 762 | format_src(snip["baz"].as_str()).unwrap(), 763 | format_src("fn bar() {} fn baz() {}").unwrap() 764 | ); 765 | 766 | let src = r#" 767 | #[snippet] 768 | fn foo() {} 769 | 770 | #[snippet] 771 | fn bar() {} 772 | 773 | #[snippet(name = "baz", include = "foo, bar")] 774 | fn baz() {} 775 | "#; 776 | 777 | let snip = snippets(&src); 778 | assert_eq!( 779 | snip.get("bar").and_then(|s| format_src(s)), 780 | format_src( 781 | "e!( 782 | fn bar() {} 783 | ) 784 | .to_string() 785 | ) 786 | ); 787 | // Original order of "uses" are not saved. 788 | assert_eq!( 789 | format_src(snip["baz"].as_str()).unwrap(), 790 | format_src("fn foo() {} fn bar() {} fn baz() {}").unwrap() 791 | ); 792 | } 793 | 794 | #[test] 795 | fn test_recursive_dependency() { 796 | let src = r#" 797 | #[snippet(include = "baz")] 798 | fn bar() {} 799 | 800 | #[snippet(include = "bar")] 801 | fn baz() {} 802 | "#; 803 | 804 | let snip = snippets(&src); 805 | assert_eq!( 806 | format_src(snip["bar"].as_str()).unwrap(), 807 | format_src("fn baz() {} fn bar() {}").unwrap() 808 | ); 809 | assert_eq!( 810 | format_src(snip["baz"].as_str()).unwrap(), 811 | format_src("fn bar() {} fn baz() {}").unwrap() 812 | ); 813 | } 814 | 815 | #[test] 816 | fn test_missing_dependency() { 817 | let src = r#" 818 | #[snippet(include = "foo")] 819 | fn bar() {} 820 | 821 | #[snippet(include = "foo")] 822 | fn baz() {} 823 | "#; 824 | 825 | let snip = snippets(&src); 826 | assert_eq!( 827 | format_src(snip["bar"].as_str()).unwrap(), 828 | format_src("fn bar() {}").unwrap() 829 | ); 830 | assert_eq!( 831 | format_src(snip["baz"].as_str()).unwrap(), 832 | format_src("fn baz() {}").unwrap() 833 | ); 834 | } 835 | 836 | #[test] 837 | fn test_attribute_full_path() { 838 | let src = r#" 839 | #[cargo_snippet::snippet] 840 | fn bar() {} 841 | "#; 842 | 843 | let snip = snippets(&src); 844 | assert_eq!( 845 | format_src(snip["bar"].as_str()).unwrap(), 846 | format_src("fn bar() {}").unwrap() 847 | ); 848 | } 849 | 850 | #[test] 851 | fn test_attribute_prefix() { 852 | let src = r#" 853 | #[snippet(prefix = "use std::io;")] 854 | fn bar() {} 855 | "#; 856 | 857 | let snip = snippets(&src); 858 | assert_eq!( 859 | format_src(snip["bar"].as_str()).unwrap(), 860 | format_src("use std::io;\nfn bar() {}").unwrap() 861 | ); 862 | 863 | let src = r#" 864 | #[snippet(prefix="use std::io::{self,Read};\nuse std::str::FromStr;")] 865 | fn bar() {} 866 | "#; 867 | 868 | let snip = snippets(&src); 869 | assert_eq!( 870 | format_src(snip["bar"].as_str()).unwrap(), 871 | format_src("use std::io::{self,Read};\nuse std::str::FromStr;\nfn bar() {}").unwrap() 872 | ); 873 | 874 | let src = r#" 875 | #[snippet(prefix=r"use std::io::{self,Read}; 876 | use std::str::FromStr;")] 877 | fn bar() {} 878 | "#; 879 | 880 | let snip = snippets(&src); 881 | assert_eq!( 882 | format_src(snip["bar"].as_str()).unwrap(), 883 | format_src("use std::io::{self,Read};\nuse std::str::FromStr;\nfn bar() {}").unwrap() 884 | ); 885 | } 886 | 887 | #[test] 888 | fn test_attribute_prefix_include() { 889 | let src = r#" 890 | #[snippet(prefix = "use std::sync;")] 891 | fn foo() {} 892 | #[snippet(prefix = "use std::io;", include = "foo")] 893 | fn bar() {} 894 | "#; 895 | 896 | let snip = snippets(&src); 897 | assert_eq!( 898 | format_src(snip["bar"].as_str()).unwrap(), 899 | format_src( 900 | "e!( 901 | use std::sync; 902 | use std::io; 903 | fn foo() {} 904 | fn bar() {} 905 | ) 906 | .to_string() 907 | ) 908 | .unwrap() 909 | ); 910 | } 911 | 912 | #[test] 913 | fn test_outer_line_doc() { 914 | let src = r#" 915 | /// This is outer doc comment. (exactly three slashes) 916 | // This is *NOT* doc comment. 917 | //// This is also *NOT* doc comment. 918 | #[snippet] 919 | fn foo() {} 920 | "#; 921 | 922 | let snip = snippets(&src); 923 | assert_eq!( 924 | format_src(snip["foo"].as_str()).unwrap(), 925 | format_src("/// This is outer doc comment. (exactly three slashes)\nfn foo() {}") 926 | .unwrap(), 927 | ); 928 | } 929 | 930 | #[test] 931 | fn test_outer_block_doc() { 932 | let src = r#" 933 | /** This is outer doc comment. 934 | doc comment1 935 | * doc comment2 936 | doc comment finishes here! */ 937 | /* 938 | NOT doc comment 939 | */ 940 | /*** NOT doc comment */ 941 | #[snippet] 942 | fn foo() {} 943 | "#; 944 | 945 | let snip = snippets(&src); 946 | assert_eq!( 947 | format_src(snip["foo"].as_str()).unwrap(), 948 | format_src( 949 | r#" 950 | /// This is outer doc comment. 951 | ///doc comment1 952 | ///* doc comment2 953 | /// doc comment finishes here! 954 | fn foo() {} 955 | "# 956 | ) 957 | .unwrap(), 958 | ); 959 | } 960 | 961 | #[test] 962 | fn test_inner_line_doc() { 963 | let src = r#" 964 | #[snippet] 965 | fn foo() { 966 | //! This is inner doc comment. 967 | } 968 | "#; 969 | 970 | let snip = snippets(&src); 971 | assert_eq!( 972 | format_src(snip["foo"].as_str()).unwrap(), 973 | format_src("fn foo() {\n//! This is inner doc comment.\n}").unwrap(), 974 | ); 975 | } 976 | 977 | #[test] 978 | fn test_inner_block_doc() { 979 | let src = r#" 980 | #[snippet] 981 | fn foo() { 982 | /*! This is inner doc comment. 983 | doc comment1 984 | * doc comment2 985 | doc comment finishes here! */ 986 | /* 987 | NOT doc comment 988 | */ 989 | /*** NOT doc comment */ 990 | } 991 | "#; 992 | 993 | let snip = snippets(&src); 994 | assert_eq!( 995 | format_src(snip["foo"].as_str()).unwrap(), 996 | format_src( 997 | r#" 998 | fn foo() { 999 | //! This is inner doc comment. 1000 | //!doc comment1 1001 | //!* doc comment2 1002 | //! doc comment finishes here! 1003 | } 1004 | "# 1005 | ) 1006 | .unwrap(), 1007 | ); 1008 | } 1009 | 1010 | #[test] 1011 | fn test_outer_line_doc_in_file() { 1012 | let src = r#" 1013 | #![snippet("file")] 1014 | /// This is outer doc comment. 1015 | fn foo() {} 1016 | "#; 1017 | 1018 | let snip = snippets(&src); 1019 | assert_eq!( 1020 | format_src(snip["file"].as_str()).unwrap(), 1021 | format_src("/// This is outer doc comment.\nfn foo() {}").unwrap(), 1022 | ); 1023 | } 1024 | 1025 | #[test] 1026 | fn test_outer_line_doc_in_file_escaped_chars() { 1027 | let src = r#" 1028 | #![snippet("file")] 1029 | /// ///\\\ 'This \t is \r outer " doc \n comment. 1030 | fn foo() {} 1031 | "#; 1032 | 1033 | let snip = snippets(&src); 1034 | assert_eq!( 1035 | format_src(snip["file"].as_str()).unwrap(), 1036 | format_src( 1037 | r#"/// ///\\\ 'This \t is \r outer " doc \n comment. 1038 | fn foo() {}"# 1039 | ) 1040 | .unwrap(), 1041 | ); 1042 | } 1043 | 1044 | #[test] 1045 | fn test_inner_line_doc_in_file() { 1046 | let src = r#" 1047 | #![snippet("file")] 1048 | //! This is inner doc comment. 1049 | fn foo() {} 1050 | "#; 1051 | 1052 | let snip = snippets(&src); 1053 | assert_eq!( 1054 | format_src(snip["file"].as_str()).unwrap(), 1055 | format_src("//! This is inner doc comment.\nfn foo() {}").unwrap(), 1056 | ); 1057 | } 1058 | 1059 | #[test] 1060 | fn test_inner_line_doc_in_file_backslash() { 1061 | let src = r#" 1062 | #![snippet("file")] 1063 | //! ///\\\ This is outer doc comment. 1064 | fn foo() {} 1065 | "#; 1066 | 1067 | let snip = snippets(&src); 1068 | assert_eq!( 1069 | format_src(snip["file"].as_str()).unwrap(), 1070 | format_src( 1071 | r#"//! ///\\\ This is outer doc comment. 1072 | fn foo() {}"# 1073 | ) 1074 | .unwrap(), 1075 | ); 1076 | } 1077 | 1078 | #[test] 1079 | fn test_inner_line_doc_in_file_tab() { 1080 | let src = r#" 1081 | #![snippet("file")] 1082 | //! /// <- tab character 1083 | fn foo() {} 1084 | "#; 1085 | 1086 | let snip = snippets(&src); 1087 | assert_eq!( 1088 | format_src(snip["file"].as_str()).unwrap(), 1089 | format_src( 1090 | r#"//! /// <- tab character 1091 | fn foo() {}"# 1092 | ) 1093 | .unwrap(), 1094 | ); 1095 | } 1096 | 1097 | #[test] 1098 | fn test_unicode_unescape() { 1099 | // cf. https://ja.wikipedia.org/wiki/%E3%82%B9%E3%83%9A%E3%83%BC%E3%82%B9 1100 | assert_eq!(unescape("foo\\u{2002}bar"), "foo bar"); // EN SPACE 1101 | assert_eq!(unescape("foo\\u{2003}bar"), "foo bar"); // EM SPACE 1102 | assert_eq!(unescape("foo\\u{2004}bar"), "foo bar"); // THREE-PER-EM SPACE 1103 | assert_eq!(unescape("foo\\u{2005}bar"), "foo bar"); // FOUR-PER-EM SPACE 1104 | assert_eq!(unescape("foo\\u{2006}bar"), "foo bar"); // SIX-PER-EM SPACE 1105 | assert_eq!(unescape("foo\\u{2007}bar"), "foo bar"); // FIGURE SPACE 1106 | assert_eq!(unescape("foo\\u{2008}bar"), "foo bar"); // PUNCTUATION SPACE 1107 | assert_eq!(unescape("foo\\u{2009}bar"), "foo bar"); // THIN SPACE 1108 | assert_eq!(unescape("foo\\u{200A}bar"), "foo bar"); // HAIR SPACE 1109 | assert_eq!(unescape("foo\\u{200B}bar"), "foo\u{200B}bar"); // ZERO WIDTH SPACE 1110 | assert_eq!(unescape("foo\\u{3000}bar"), "foo bar"); // IDEOGRAPHIC SPACE 1111 | } 1112 | 1113 | #[test] 1114 | fn test_full_width_space_in_outer_line_doc() { 1115 | let src = r#" 1116 | #[snippet] 1117 | /// [ ] <- full width space 1118 | fn foo() {} 1119 | "#; 1120 | 1121 | let snip = snippets(&src); 1122 | assert_eq!( 1123 | format_src(snip["foo"].as_str()).unwrap(), 1124 | format_src("/// [ ] <- full width space\nfn foo() {}").unwrap(), 1125 | ); 1126 | } 1127 | 1128 | #[test] 1129 | fn test_full_width_space_in_outer_block_doc() { 1130 | let src = r#" 1131 | #[snippet] 1132 | /** 1133 | [ ] <- full width space 1134 | */ 1135 | fn foo() {} 1136 | "#; 1137 | 1138 | let snip = snippets(&src); 1139 | assert_eq!( 1140 | format_src(snip["foo"].as_str()).unwrap(), 1141 | format_src("///\n///[ ] <- full width space\nfn foo() {}").unwrap(), 1142 | ); 1143 | } 1144 | 1145 | #[test] 1146 | fn test_full_width_space_in_inner_line_doc() { 1147 | let src = r#" 1148 | #[snippet] 1149 | fn foo() { 1150 | //! [ ] <- full width space 1151 | } 1152 | "#; 1153 | 1154 | let snip = snippets(&src); 1155 | assert_eq!( 1156 | format_src(snip["foo"].as_str()).unwrap(), 1157 | format_src("fn foo() {\n//! [ ] <- full width space\n}").unwrap(), 1158 | ); 1159 | } 1160 | 1161 | #[test] 1162 | fn test_full_width_space_in_inner_block_doc() { 1163 | let src = r#" 1164 | #[snippet] 1165 | fn foo() { 1166 | /*! 1167 | [ ] <- full width space 1168 | */ 1169 | } 1170 | "#; 1171 | 1172 | let snip = snippets(&src); 1173 | assert_eq!( 1174 | format_src(snip["foo"].as_str()).unwrap(), 1175 | format_src("fn foo() {\n//!\n//![ ] <- full width space\n}").unwrap(), 1176 | ); 1177 | } 1178 | 1179 | #[test] 1180 | fn test_divide_deref() { 1181 | let src = r#" 1182 | #[snippet] 1183 | fn foo(a: &i32, b: &i32) -> i32 { 1184 | *a / *b 1185 | } 1186 | "#; 1187 | let snip = snippets(&src); 1188 | assert_eq!( 1189 | format_src(snip["foo"].as_str()).unwrap(), 1190 | format_src("fn foo(a: &i32, b: &i32) -> i32 { *a / *b }").unwrap(), 1191 | ); 1192 | } 1193 | 1194 | #[test] 1195 | fn test_doc_hidden_outer_line() { 1196 | let src = r#" 1197 | /// comment 1198 | #[snippet(doc_hidden)] 1199 | fn foo() {} 1200 | "#; 1201 | let snip = snippets(&src); 1202 | assert_eq!( 1203 | format_src(snip["foo"].as_str()).unwrap(), 1204 | format_src("fn foo() {}").unwrap(), 1205 | ); 1206 | } 1207 | 1208 | #[test] 1209 | fn test_doc_hidden_inner_line() { 1210 | let src = r#" 1211 | #[snippet(doc_hidden)] 1212 | fn foo() { 1213 | //! comment 1214 | } 1215 | "#; 1216 | let snip = snippets(&src); 1217 | assert_eq!( 1218 | format_src(snip["foo"].as_str()).unwrap(), 1219 | format_src("fn foo() {}").unwrap(), 1220 | ); 1221 | } 1222 | 1223 | #[test] 1224 | fn test_doc_hidden_outer_block() { 1225 | let src = r#" 1226 | /** comment */ 1227 | #[snippet(doc_hidden)] 1228 | fn foo() {} 1229 | "#; 1230 | let snip = snippets(&src); 1231 | assert_eq!( 1232 | format_src(snip["foo"].as_str()).unwrap(), 1233 | format_src("fn foo() {}").unwrap(), 1234 | ); 1235 | } 1236 | 1237 | #[test] 1238 | fn test_doc_hidden_inner_block() { 1239 | let src = r#" 1240 | #[snippet(doc_hidden)] 1241 | fn foo() { 1242 | /*! comment */ 1243 | } 1244 | "#; 1245 | let snip = snippets(&src); 1246 | assert_eq!( 1247 | format_src(snip["foo"].as_str()).unwrap(), 1248 | format_src("fn foo() {}").unwrap(), 1249 | ); 1250 | } 1251 | 1252 | #[test] 1253 | fn test_doc_hidden_outer_line_in_file() { 1254 | let src = r#" 1255 | #![snippet("file", doc_hidden)] 1256 | /// comment 1257 | fn foo() {} 1258 | "#; 1259 | 1260 | let snip = snippets(&src); 1261 | assert_eq!( 1262 | format_src(snip["file"].as_str()).unwrap(), 1263 | format_src("fn foo() {}").unwrap(), 1264 | ); 1265 | } 1266 | 1267 | #[test] 1268 | fn test_doc_hidden_multiple_items_in_file() { 1269 | let src = r#" 1270 | #![snippet("file", doc_hidden)] 1271 | 1272 | /// foo comment 1273 | fn foo() {} 1274 | 1275 | fn bar() { 1276 | //! bar comment 1277 | } 1278 | 1279 | /// baz outer 1280 | fn baz() { 1281 | //! baz inner 1282 | } 1283 | "#; 1284 | 1285 | let snip = snippets(&src); 1286 | assert_eq!( 1287 | format_src(snip["file"].as_str()).unwrap(), 1288 | format_src("fn foo() {}\nfn bar() {}\nfn baz() {}").unwrap(), 1289 | ); 1290 | } 1291 | 1292 | #[test] 1293 | fn test_doc_hidden_outer_line_with_other_metas() { 1294 | let src = r#" 1295 | /// comment 1296 | #[snippet(name = "bar", doc_hidden, prefix = "use std::collections::HashMap;")] 1297 | fn foo() {} 1298 | "#; 1299 | let snip = snippets(&src); 1300 | assert_eq!( 1301 | format_src(snip["bar"].as_str()).unwrap(), 1302 | format_src("use std::collections::HashMap;\nfn foo() {}").unwrap(), 1303 | ); 1304 | } 1305 | } 1306 | --------------------------------------------------------------------------------