├── cli ├── README.md ├── CHANGELOG.md ├── tests │ ├── test_data │ │ ├── fake_sourcegen │ │ │ ├── src │ │ │ │ └── lib.rs │ │ │ └── Cargo.toml │ │ ├── 010-trait │ │ │ ├── input │ │ │ │ ├── src │ │ │ │ │ └── lib.rs │ │ │ │ └── Cargo.toml │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── 005-complete-file │ │ │ ├── input │ │ │ │ ├── src │ │ │ │ │ └── lib.rs │ │ │ │ └── Cargo.toml │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── 003-modules │ │ │ ├── input │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ │ ├── five_other.rs │ │ │ │ │ ├── one │ │ │ │ │ └── three.rs │ │ │ │ │ ├── two │ │ │ │ │ ├── four.rs │ │ │ │ │ └── mod.rs │ │ │ │ │ ├── six │ │ │ │ │ └── seven │ │ │ │ │ │ ├── eight.rs │ │ │ │ │ │ └── nine │ │ │ │ │ │ └── mod.rs │ │ │ │ │ ├── one.rs │ │ │ │ │ └── lib.rs │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ ├── two │ │ │ │ ├── four.rs │ │ │ │ └── mod.rs │ │ │ │ ├── five_other.rs │ │ │ │ ├── one │ │ │ │ └── three.rs │ │ │ │ ├── six │ │ │ │ └── seven │ │ │ │ │ ├── eight.rs │ │ │ │ │ └── nine │ │ │ │ │ └── mod.rs │ │ │ │ ├── one.rs │ │ │ │ └── lib.rs │ │ ├── 008-newline │ │ │ ├── input │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ │ └── lib.rs │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── 009-keywords │ │ │ ├── input │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ │ ├── await.rs │ │ │ │ │ └── lib.rs │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ ├── await.rs │ │ │ │ └── lib.rs │ │ ├── 002-remove-impls │ │ │ ├── input │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ │ └── lib.rs │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── 004-doc-comments │ │ │ ├── input │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ │ └── lib.rs │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── 006-complete-file-workaround │ │ │ ├── input │ │ │ │ ├── src │ │ │ │ │ └── lib.rs │ │ │ │ └── Cargo.toml │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ └── lib.rs │ │ ├── 001-strip-attributes │ │ │ ├── input │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ │ └── lib.rs │ │ │ └── expected │ │ │ │ ├── Cargo.toml │ │ │ │ └── src │ │ │ │ └── lib.rs │ │ └── 007-plain-comments │ │ │ ├── expected │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ │ └── lib.rs │ │ │ └── input │ │ │ ├── Cargo.toml │ │ │ └── src │ │ │ └── lib.rs │ ├── suite.rs │ ├── helpers │ │ └── mod.rs │ └── generators │ │ └── mod.rs ├── Cargo.toml └── src │ ├── tokens.rs │ ├── mods.rs │ ├── error.rs │ ├── rustfmt.rs │ ├── region.rs │ ├── normalize.rs │ ├── lib.rs │ └── generate.rs ├── derive ├── README.md ├── CHANGELOG.md ├── Cargo.toml └── src │ └── lib.rs ├── Cargo.toml ├── .gitignore ├── release.toml ├── LICENSE-MIT ├── .github └── workflows │ └── rust.yml ├── CHANGELOG.md ├── README.md ├── CODE_OF_CONDUCT.md └── LICENSE-APACHE /cli/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /derive/README.md: -------------------------------------------------------------------------------- 1 | ../README.md -------------------------------------------------------------------------------- /cli/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /derive/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ../CHANGELOG.md -------------------------------------------------------------------------------- /cli/tests/test_data/fake_sourcegen/src/lib.rs: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = ["derive", "cli"] 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /target 3 | **/*.rs.bk 4 | /Cargo.lock 5 | -------------------------------------------------------------------------------- /cli/tests/test_data/010-trait/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-trait")] 2 | trait Test {} 3 | -------------------------------------------------------------------------------- /cli/tests/test_data/005-complete-file/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![sourcegen::sourcegen(generator = "generate-file")] 2 | // will be discarded 3 | pub enum Test {} -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/008-newline/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/009-keywords/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/010-trait/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/010-trait/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/002-remove-impls/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/004-doc-comments/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/005-complete-file/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/006-complete-file-workaround/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-file", file = true)] 2 | struct __Unused; 3 | 4 | // will be discarded 5 | pub enum Test {} -------------------------------------------------------------------------------- /cli/tests/test_data/008-newline/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/009-keywords/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/001-strip-attributes/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/002-remove-impls/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/004-doc-comments/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/005-complete-file/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/007-plain-comments/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/007-plain-comments/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/001-strip-attributes/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/006-complete-file-workaround/expected/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/006-complete-file-workaround/input/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "test" 3 | version = "0.0.0" 4 | 5 | [dependencies] 6 | sourcegen = { path = "../../fake_sourcegen" } 7 | 8 | [workspace] 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/008-newline/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-newline")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | struct Test; 4 | -------------------------------------------------------------------------------- /cli/tests/test_data/009-keywords/input/src/await.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-simple")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | struct Test; 4 | -------------------------------------------------------------------------------- /cli/tests/test_data/004-doc-comments/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-doc-comments")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | struct Hello; 4 | -------------------------------------------------------------------------------- /cli/tests/test_data/007-plain-comments/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-plain-comments")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | struct Hello; 4 | -------------------------------------------------------------------------------- /cli/tests/test_data/008-newline/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-newline")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | struct Test; 4 | 5 | impl Test {} 6 | -------------------------------------------------------------------------------- /cli/tests/test_data/fake_sourcegen/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sourcegen" 3 | version = "0.0.0" 4 | description = """ 5 | Empty crate for test purposes -- sourcegen looks at Cargo.toml's `sourcegen` dependency; we use this crate as one. 6 | """ -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/src/five_other.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct; 6 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/src/one/three.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct; 6 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/src/two/four.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct; 6 | -------------------------------------------------------------------------------- /cli/tests/test_data/009-keywords/expected/src/await.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-simple")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | struct Test { 4 | pub hello: String, 5 | } 6 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/src/six/seven/eight.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct; 6 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/src/six/seven/nine/mod.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct; 6 | -------------------------------------------------------------------------------- /cli/tests/test_data/005-complete-file/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | #![sourcegen::sourcegen(generator = "generate-file")] 2 | // Generated. All manual edits below this line will be discarded. 3 | /// Some generated comment here 4 | struct Hello { 5 | pub hello: String, 6 | } 7 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/src/one.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | pub mod three; 4 | 5 | #[sourcegen(generator = "generate-simple")] 6 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 7 | pub struct TestStruct; 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/src/two/mod.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | pub mod four; 4 | 5 | #[sourcegen(generator = "generate-simple")] 6 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 7 | pub struct TestStruct; 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/009-keywords/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod r#async { 2 | #[sourcegen::sourcegen(generator = "generate-simple")] 3 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 4 | struct Test; 5 | } 6 | 7 | mod r#await; 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/010-trait/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-trait")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | /// Some generated comment here 4 | trait Test { 5 | fn hello(); 6 | } 7 | -------------------------------------------------------------------------------- /cli/tests/test_data/001-strip-attributes/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[another] 2 | #[sourcegen::sourcegen(generator = "write-back")] 3 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 4 | #[derive(Custom)] 5 | struct Hello { 6 | field: usize, 7 | } 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/001-strip-attributes/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[another] 2 | #[sourcegen::sourcegen(generator = "write-back")] 3 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 4 | #[derive(Custom)] 5 | struct Hello { 6 | field: usize, 7 | } 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/src/two/four.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct { 6 | pub hello: String, 7 | } 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/src/five_other.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct { 6 | pub hello: String, 7 | } 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/src/one/three.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct { 6 | pub hello: String, 7 | } 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/src/six/seven/eight.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct { 6 | pub hello: String, 7 | } 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/src/six/seven/nine/mod.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | #[sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct { 6 | pub hello: String, 7 | } 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/004-doc-comments/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-doc-comments")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | /// Some generated comment here 4 | struct Hello { 5 | pub hello: String, 6 | } 7 | -------------------------------------------------------------------------------- /cli/tests/test_data/006-complete-file-workaround/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-file", file = true)] 2 | struct __Unused; 3 | 4 | // Generated. All manual edits below this line will be discarded. 5 | /// Some generated comment here 6 | struct Hello { 7 | pub hello: String, 8 | } 9 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/src/one.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | pub mod three; 4 | 5 | #[sourcegen(generator = "generate-simple")] 6 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 7 | pub struct TestStruct { 8 | pub hello: String, 9 | } 10 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/src/two/mod.rs: -------------------------------------------------------------------------------- 1 | use sourcegen::sourcegen; 2 | 3 | pub mod four; 4 | 5 | #[sourcegen(generator = "generate-simple")] 6 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 7 | pub struct TestStruct { 8 | pub hello: String, 9 | } 10 | -------------------------------------------------------------------------------- /cli/tests/test_data/007-plain-comments/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-plain-comments")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | // This is some struct! 4 | struct Hello { 5 | // This is some field! 6 | pub hello: String, 7 | } 8 | -------------------------------------------------------------------------------- /cli/tests/test_data/009-keywords/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | mod r#async { 2 | #[sourcegen::sourcegen(generator = "generate-simple")] 3 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 4 | struct Test { 5 | pub hello: String, 6 | } 7 | } 8 | 9 | mod r#await; 10 | -------------------------------------------------------------------------------- /release.toml: -------------------------------------------------------------------------------- 1 | #pre-release-hook = ["./hook.sh"] 2 | pre-release-replacements = [ 3 | {file="README.md", search="Current release: [a-z0-9\\.-]+", replace="Current release: {{version}}"} , 4 | {file="CHANGELOG.md", search="Unreleased", replace="{{version}}"}, 5 | {file="CHANGELOG.md", search="ReleaseDate", replace="{{date}}"} 6 | ] 7 | -------------------------------------------------------------------------------- /derive/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sourcegen" 3 | version = "0.3.0" 4 | authors = ["Commure "] 5 | edition = "2018" 6 | repository = "https://github.com/commure/sourcegen" 7 | license = "MIT/Apache-2.0" 8 | readme = "README.md" 9 | description = """ 10 | This crate contains a procedural macro to work together with "sourcegen-cli" crate. 11 | """ 12 | 13 | [lib] 14 | proc-macro = true 15 | -------------------------------------------------------------------------------- /cli/tests/test_data/002-remove-impls/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-impls")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | struct Hello; 4 | #[sourcegen::generated] 5 | impl Hello {} 6 | 7 | struct Irrelevant; 8 | 9 | #[sourcegen::sourcegen(generator = "generate-impls")] 10 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 11 | struct Hello2; 12 | #[sourcegen::generated] 13 | impl Hello2 {} 14 | -------------------------------------------------------------------------------- /cli/tests/test_data/002-remove-impls/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | #[sourcegen::sourcegen(generator = "generate-impls")] 2 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 3 | struct Hello; 4 | #[sourcegen::generated] 5 | impl Hello {} 6 | 7 | struct Irrelevant; 8 | 9 | #[sourcegen::sourcegen(generator = "generate-impls")] 10 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 11 | struct Hello2; 12 | #[sourcegen::generated] 13 | impl Hello2 {} 14 | -------------------------------------------------------------------------------- /derive/src/lib.rs: -------------------------------------------------------------------------------- 1 | extern crate proc_macro; 2 | 3 | /// Does nothing (returns item as-is). Needed to remove the attribute that is handled by source generator. 4 | #[proc_macro_attribute] 5 | pub fn sourcegen( 6 | _attrs: proc_macro::TokenStream, 7 | item: proc_macro::TokenStream, 8 | ) -> proc_macro::TokenStream { 9 | item 10 | } 11 | 12 | #[proc_macro_attribute] 13 | pub fn generated( 14 | _attrs: proc_macro::TokenStream, 15 | item: proc_macro::TokenStream, 16 | ) -> proc_macro::TokenStream { 17 | item 18 | } 19 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/input/src/lib.rs: -------------------------------------------------------------------------------- 1 | use sourcegen; 2 | 3 | #[sourcegen::sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct; 6 | 7 | /// Nested modules 8 | pub mod nested { 9 | #[sourcegen::sourcegen(generator = "generate-simple")] 10 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 11 | pub struct TestStruct; 12 | } 13 | 14 | pub mod one; 15 | pub mod two; 16 | 17 | #[path = "five_other.rs"] 18 | pub mod five; 19 | 20 | mod six { 21 | mod seven { 22 | mod eight; 23 | mod nine; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /cli/tests/test_data/003-modules/expected/src/lib.rs: -------------------------------------------------------------------------------- 1 | use sourcegen; 2 | 3 | #[sourcegen::sourcegen(generator = "generate-simple")] 4 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 5 | pub struct TestStruct { 6 | pub hello: String, 7 | } 8 | 9 | /// Nested modules 10 | pub mod nested { 11 | #[sourcegen::sourcegen(generator = "generate-simple")] 12 | // Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded. 13 | pub struct TestStruct { 14 | pub hello: String, 15 | } 16 | } 17 | 18 | pub mod one; 19 | pub mod two; 20 | 21 | #[path = "five_other.rs"] 22 | pub mod five; 23 | 24 | mod six { 25 | mod seven { 26 | mod eight; 27 | mod nine; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /cli/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "sourcegen-cli" 3 | version = "0.4.1-alpha.0" 4 | authors = ["Commure "] 5 | edition = "2018" 6 | repository = "https://github.com/commure/sourcegen" 7 | license = "MIT/Apache-2.0" 8 | readme = "README.md" 9 | description = """ 10 | In-place Rust source generator CLI utility. 11 | """ 12 | 13 | [[test]] 14 | name = "suite" 15 | harness = false 16 | 17 | [dependencies] 18 | anyhow = "1.0.26" 19 | syn = { version = "1.0.0", features = ["full"] } 20 | proc-macro2 = { version = "1.0.0", features = ["span-locations"] } 21 | quote = "1.0.0" 22 | cargo_metadata = "0.8.1" 23 | thiserror = "1.0.9" 24 | tempfile = "3.0.8" 25 | dunce = "1.0.0" 26 | 27 | [dev-dependencies] 28 | quote = "1.0.0" 29 | copy_dir = "0.1.2" 30 | pretty_assertions = "0.6.1" 31 | 32 | [features] 33 | # Disable normalizing doc comments (`#[doc = r" hello"]`) into `///`. 34 | # On nightly, one can make `rustfmt` to do that via `normalize_doc_attributes` configuration parameter for `rustfmt`, 35 | # but on current stable this is not supported. So we support doing our own normalization by default. This feature 36 | # is to disable that normalization 37 | disable_normalize_doc_attributes = [] 38 | 39 | default = [] 40 | -------------------------------------------------------------------------------- /cli/src/tokens.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Ident, Span, TokenStream, TokenTree}; 2 | 3 | pub(crate) const MAGIC_COMMENT_IDENT: &str = "__SOURCEGEN_MAGIC_COMMENT__"; 4 | 5 | /// Token used to generate plain Rust comments in the output. Used as a marker in front of the 6 | /// string literal to generate a plain comment. Usage: 7 | /// 8 | /// ```rust 9 | /// use sourcegen_cli::tokens::PlainComment; 10 | /// let _output = quote::quote! { 11 | /// #PlainComment "GeneratedComment" 12 | /// struct Test; 13 | /// }; 14 | /// ``` 15 | /// 16 | /// Generated output will contain a plain comment: 17 | /// ``` 18 | /// // Generated comment 19 | /// struct Test; 20 | /// ``` 21 | pub struct PlainComment; 22 | 23 | impl quote::ToTokens for PlainComment { 24 | fn to_tokens(&self, tokens: &mut TokenStream) { 25 | tokens.extend(std::iter::once(TokenTree::Ident(Ident::new( 26 | MAGIC_COMMENT_IDENT, 27 | Span::call_site(), 28 | )))); 29 | } 30 | } 31 | 32 | pub(crate) const MAGIC_NEWLINE_IDENT: &str = "__SOURCEGEN_MAGIC_NEWLINE__"; 33 | 34 | /// Token used to generate a newline in the output. Used as a marker. Usage: 35 | /// 36 | /// ```rust 37 | /// use sourcegen_cli::tokens::NewLine; 38 | /// let _output = quote::quote! { 39 | /// struct Frist; 40 | /// #NewLine 41 | /// struct Second; 42 | /// }; 43 | /// ``` 44 | /// 45 | /// Generated output will contain a plain comment: 46 | /// ``` 47 | /// struct First; 48 | /// 49 | /// struct Second; 50 | /// ``` 51 | pub struct NewLine; 52 | 53 | impl quote::ToTokens for NewLine { 54 | fn to_tokens(&self, tokens: &mut TokenStream) { 55 | tokens.extend(std::iter::once(TokenTree::Ident(Ident::new( 56 | MAGIC_NEWLINE_IDENT, 57 | Span::call_site(), 58 | )))); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /cli/src/mods.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{SourcegenError, SourcegenErrorKind}; 2 | use std::path::PathBuf; 3 | use syn::{Attribute, ItemMod, Lit, Meta}; 4 | 5 | // FIXME: support cfg_attr, too? 6 | pub struct ModResolver { 7 | base: PathBuf, 8 | } 9 | 10 | impl ModResolver { 11 | pub fn new(base: impl Into) -> Self { 12 | ModResolver { base: base.into() } 13 | } 14 | 15 | /// Nested module -- append a new directory. 16 | pub fn push_module(&self, name: &str) -> Self { 17 | ModResolver { 18 | base: self.base.join(name), 19 | } 20 | } 21 | 22 | /// Resolve to a module file. 23 | pub fn resolve_module_file(&self, item: &ItemMod) -> Result { 24 | if let Some(path) = detect_mod_path(&item.attrs) { 25 | Ok(self.base.join(path)) 26 | } else { 27 | let name = item.ident.to_string(); 28 | let name = name.trim_start_matches("r#"); 29 | let path = self.base.join(&format!("{}.rs", name)); 30 | if path.is_file() { 31 | return Ok(path); 32 | } 33 | let path = self.base.join(&name).join("mod.rs"); 34 | if path.is_file() { 35 | return Ok(path); 36 | } 37 | Err(SourcegenErrorKind::CannotResolveModule( 38 | path.display().to_string(), 39 | name.to_owned(), 40 | ) 41 | .into()) 42 | } 43 | } 44 | } 45 | 46 | fn detect_mod_path(attrs: &[Attribute]) -> Option { 47 | let attr = attrs.iter().find(|attr| attr.path.is_ident("path"))?; 48 | let meta = attr.parse_meta().ok()?; 49 | if let Meta::NameValue(nv) = meta { 50 | if let Lit::Str(ref value) = nv.lit { 51 | return Some(value.value()); 52 | } 53 | } 54 | None 55 | } 56 | -------------------------------------------------------------------------------- /cli/tests/suite.rs: -------------------------------------------------------------------------------- 1 | use sourcegen_cli::SourcegenParameters; 2 | use std::path::Path; 3 | 4 | pub mod generators; 5 | pub mod helpers; 6 | 7 | fn main() -> Result<(), anyhow::Error> { 8 | let temp = tempfile::tempdir()?; 9 | let root = temp.path().join("root"); 10 | copy_dir::copy_dir("tests/test_data", &root)?; 11 | for entry in std::fs::read_dir(&root)? { 12 | let entry = entry?; 13 | let path = entry.path(); 14 | if path.is_dir() 15 | && path 16 | .file_name() 17 | .map_or(true, |name| name != "fake_sourcegen") 18 | { 19 | eprintln!("running test for '{}'", path.strip_prefix(&root)?.display()); 20 | helpers::install_rustfmt(&path)?; 21 | run_test_dir(&path)?; 22 | } 23 | } 24 | 25 | Ok(()) 26 | } 27 | 28 | fn parameters(manifest: &Path) -> SourcegenParameters { 29 | SourcegenParameters { 30 | manifest: Some(manifest), 31 | generators: &[ 32 | ("write-back", &self::generators::WriteBack), 33 | ("generate-impls", &self::generators::GenerateImpls), 34 | ("generate-simple", &self::generators::GenerateSimple), 35 | ( 36 | "generate-doc-comments", 37 | &self::generators::GenerateDocComments, 38 | ), 39 | ( 40 | "generate-plain-comments", 41 | &self::generators::GeneratePlainComments, 42 | ), 43 | ("generate-newline", &self::generators::GenerateNewLine), 44 | ("generate-file", &self::generators::GenerateFile), 45 | ("generate-trait", &self::generators::GenerateTrait), 46 | ], 47 | ..Default::default() 48 | } 49 | } 50 | 51 | fn run_test_dir(dir: &Path) -> Result<(), anyhow::Error> { 52 | let manifest = dir.join("input").join("Cargo.toml"); 53 | sourcegen_cli::run_sourcegen(¶meters(&manifest))?; 54 | 55 | self::helpers::assert_matches_expected(dir, &dir.join("input"), &dir.join("expected"))?; 56 | Ok(()) 57 | } 58 | -------------------------------------------------------------------------------- /cli/src/error.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{LineColumn, Span}; 2 | use std::fmt; 3 | use std::path::{Path, PathBuf}; 4 | use thiserror::Error; 5 | 6 | /// Pointer to the file with an error. 7 | #[derive(Debug, Clone, PartialEq, Eq)] 8 | pub struct Location { 9 | path: PathBuf, 10 | start: LineColumn, 11 | end: LineColumn, 12 | } 13 | 14 | impl Location { 15 | pub(crate) fn from_path_span(path: &Path, span: Span) -> Self { 16 | Location { 17 | path: path.to_owned(), 18 | start: span.start(), 19 | end: span.end(), 20 | } 21 | } 22 | } 23 | 24 | impl fmt::Display for Location { 25 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 26 | write!( 27 | f, 28 | "{} (line: {}, column: {})", 29 | self.path.display(), 30 | self.start.line, 31 | self.start.column 32 | ) 33 | } 34 | } 35 | 36 | pub type SourcegenError = anyhow::Error; 37 | 38 | #[derive(Debug, Error, Clone, PartialEq, Eq)] 39 | pub enum SourcegenErrorKind { 40 | // Tool errors 41 | #[error("Failed scan cargo metadata.")] 42 | MetadataError, 43 | #[error("Failed to process source file `{0}`.")] 44 | ProcessFile(String), 45 | #[error("{0}: generator '{1}' is not supported")] 46 | GeneratorNotFound(Location, String), 47 | #[error("{0}: Failed to generate source content.")] 48 | GeneratorError(Location), 49 | 50 | // Source parser errors 51 | #[error("{0}: multiple `generator` attributes are not allowed")] 52 | MultipleGeneratorAttributes(Location), 53 | #[error("{0}: `generator` attributes must be a string (for example, `generator = \"sample_generator\"`)")] 54 | GeneratorAttributeMustBeString(Location), 55 | #[error("{0}: missing `generator` attribute, must be a string (for example, `generator = \"sample_generator\"`)")] 56 | MissingGeneratorAttribute(Location), 57 | #[error("Failed to resolve module '{1}' with a parent module '{0}'")] 58 | CannotResolveModule(String, String), 59 | 60 | // Formatter errors 61 | #[error("`rustfmt` is not installed for the current toolchain. Run `rustup component add rustfmt` to install it.")] 62 | NoRustFmt, 63 | #[error("`rustup which rustfmt` failed.")] 64 | WhichRustFmtFailed, 65 | #[error("Failed to format chunk of code via `rustfmt `.")] 66 | RustFmtFailed, 67 | #[error("`rustfmt` returned an error: {0}")] 68 | RustFmtError(String), 69 | 70 | #[error("Invalid package names: {0}")] 71 | InvalidPackageNames(String), 72 | } 73 | -------------------------------------------------------------------------------- /cli/src/rustfmt.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{SourcegenError, SourcegenErrorKind}; 2 | use anyhow::Context; 3 | use std::io::Write; 4 | use std::path::Path; 5 | use std::process::{Command, Output, Stdio}; 6 | 7 | /// Rust code formatter. Uses an external `rustfmt` executable for formatting the code. 8 | pub struct Formatter { 9 | rustfmt: String, 10 | } 11 | 12 | impl Formatter { 13 | pub fn new(root: &Path) -> Result { 14 | let basedir = dunce::canonicalize(root).context(SourcegenErrorKind::WhichRustFmtFailed)?; 15 | let output = Command::new("rustup") 16 | .current_dir(basedir) 17 | .arg("which") 18 | .arg("rustfmt") 19 | .stderr(Stdio::null()) 20 | .output() 21 | .context(SourcegenErrorKind::WhichRustFmtFailed)?; 22 | if !output.status.success() { 23 | return Err(SourcegenErrorKind::NoRustFmt.into()); 24 | } 25 | let rustfmt = String::from_utf8(output.stdout) 26 | .context(SourcegenErrorKind::WhichRustFmtFailed)? 27 | .trim() 28 | .to_owned(); 29 | Ok(Self { rustfmt }) 30 | } 31 | 32 | /// Reformat generated block of code via rustfmt 33 | pub fn format( 34 | &self, 35 | basefile: &Path, 36 | content: impl std::fmt::Display, 37 | ) -> Result { 38 | let basedir = dunce::canonicalize(basefile.parent().unwrap()) 39 | .context(SourcegenErrorKind::RustFmtFailed)?; 40 | let mut rustfmt = Command::new(&self.rustfmt) 41 | .current_dir(basedir) 42 | .stdout(Stdio::piped()) 43 | .stderr(Stdio::piped()) 44 | .stdin(Stdio::piped()) 45 | .spawn() 46 | .context(SourcegenErrorKind::RustFmtFailed)?; 47 | 48 | let write = rustfmt.stdin.as_mut().unwrap(); 49 | write!(write, "{}", content).context(SourcegenErrorKind::RustFmtFailed)?; 50 | let output = rustfmt 51 | .wait_with_output() 52 | .context(SourcegenErrorKind::RustFmtFailed)?; 53 | rustfmt_output(output) 54 | } 55 | } 56 | 57 | fn rustfmt_output(output: Output) -> Result { 58 | if output.status.success() { 59 | let result = String::from_utf8(output.stdout).context(SourcegenErrorKind::RustFmtFailed)?; 60 | Ok(result) 61 | } else { 62 | let err = String::from_utf8(output.stderr).context(SourcegenErrorKind::RustFmtFailed)?; 63 | Err(SourcegenErrorKind::RustFmtError(err).into()) 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | # Note: `toolchain` doesn't work for clippy-check (adds `+stable`/`+nightly` to the wrong place), so instead we install 2 | # toolchains as default 3 | on: [pull_request] 4 | name: Rust 5 | jobs: 6 | rustfmt: 7 | name: Verify formatting 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout sources 11 | uses: actions/checkout@v1 12 | 13 | - name: Install stable toolchain 14 | uses: actions-rs/toolchain@v1 15 | with: 16 | profile: minimal 17 | toolchain: stable 18 | default: true 19 | components: rustfmt 20 | 21 | - name: Cargo fmt 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: fmt 25 | args: --all -- --check 26 | 27 | verify: 28 | name: Verify Clippy and Tests 29 | strategy: 30 | matrix: 31 | os: 32 | - ubuntu-latest 33 | - windows-latest 34 | - macos-latest 35 | runs-on: ${{ matrix.os }} 36 | steps: 37 | - name: Checkout sources 38 | uses: actions/checkout@v1 39 | 40 | - name: Cache cargo registry 41 | uses: actions/cache@v1 42 | with: 43 | path: ~/.cargo/registry 44 | key: ${{ runner.os }}-cargo-registry 45 | 46 | - name: Cache cargo index 47 | uses: actions/cache@v1 48 | with: 49 | path: ~/.cargo/git 50 | key: ${{ runner.os }}-cargo-index 51 | 52 | - name: Cache cargo build 53 | uses: actions/cache@v1 54 | with: 55 | path: target 56 | key: ${{ runner.os }}-cargo-build-target 57 | 58 | - name: Install stable toolchain 59 | uses: actions-rs/toolchain@v1 60 | with: 61 | profile: minimal 62 | toolchain: stable 63 | default: true 64 | components: clippy 65 | 66 | - name: Cargo clippy (stable) 67 | uses: actions-rs/clippy-check@v1 68 | with: 69 | token: ${{ secrets.GITHUB_TOKEN }} 70 | args: --all-features 71 | 72 | - name: Cargo test (stable) 73 | uses: actions-rs/cargo@v1 74 | with: 75 | command: test 76 | args: --all --all-targets 77 | 78 | - name: Install nightly toolchain 79 | uses: actions-rs/toolchain@v1 80 | with: 81 | profile: minimal 82 | toolchain: nightly 83 | default: true 84 | components: clippy 85 | 86 | - name: Cargo clippy (nightly) 87 | uses: actions-rs/clippy-check@v1 88 | with: 89 | token: ${{ secrets.GITHUB_TOKEN }} 90 | args: --all-features 91 | 92 | - name: Cargo test (nightly) 93 | uses: actions-rs/cargo@v1 94 | with: 95 | command: test 96 | args: --all --all-targets 97 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 2 | # Changelog 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [0.3.6] - 2019-12-06 9 | 10 | ### Changed 11 | 12 | - Allow generating traits 13 | 14 | [0.3.6]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.3.6 15 | 16 | ## [0.3.5] - 2019-12-06 17 | 18 | ### Changed 19 | 20 | - Allow filtering by package name 21 | 22 | [0.3.5]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.3.5 23 | 24 | ## [0.3.4] - 2019-11-19 25 | 26 | ### Changed 27 | 28 | - Fixed issue with modules named as keywords (for example, `async`) 29 | 30 | [0.3.4]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.3.4 31 | 32 | ## [0.3.3] - 2019-09-27 33 | 34 | ### Changed 35 | 36 | - Adding support for plain Rust comments via special `quote!`-able struct `sourcegen_cli::tokens::PlainComment`. 37 | - Adding support for newlines via special `quote!`-able struct `sourcegen_cli::tokens::Newline`. 38 | 39 | [0.3.3]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.3.3 40 | 41 | ## [0.3.2] - 2019-08-21 42 | 43 | ### Changed 44 | 45 | - Normalizing doc attributes into `///` without relying on a nightly rustfmt feature. 46 | 47 | [0.3.2]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.3.2 48 | 49 | ## [0.3.1] - 2019-08-15 50 | 51 | ### Changed 52 | 53 | - Adding support for nested included modules. 54 | 55 | [0.3.1]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.3.1 56 | 57 | ## [0.3.0] - 2019-08-14 58 | 59 | ### Changed 60 | 61 | - Upgraded `proc_macro2` and `syn` dependencies to `1.0` 62 | 63 | [0.3.0]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.3.0 64 | 65 | ## [0.2.2] - 2019-08-06 66 | 67 | ### Changed 68 | 69 | - Fixed issue with missing newline on the last line of generated file 70 | 71 | [0.2.2]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.2.2 72 | 73 | ## [0.2.1] - 2019-08-06 74 | 75 | ### Added 76 | 77 | - Support for generating full files both via `#![sourcegen::sourcegen(..)]` top-level macro and workaround via `file = true` attribute. 78 | 79 | [0.2.1]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.2.1 80 | 81 | ## [0.2.0] - 2019-08-04 82 | 83 | ### Added 84 | 85 | - Support for generated impls via `#[sourcegen::generated]` attribute 86 | - Tests 87 | 88 | ### Changed 89 | 90 | - Fixed issue with newlines on Windows 91 | 92 | [0.2.0]: https://github.com/commure/sourcegen/releases/tag/sourcegen-cli-v0.2.0 93 | 94 | ## [0.1.0] - 2019-07-30 95 | 96 | Initial version 97 | 98 | [0.1.0]: https://github.com/commure/sourcegen/releases/tag/sourcegen-v0.1.0 99 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sourcegen 2 | 3 | **Source gen**erator 4 | 5 | [![crates.io][Crate Logo]][Crate] 6 | [![Documentation][Doc Logo]][Doc] 7 | ![Build Status][CI Logo] 8 | 9 | Sourcegen is a toolkit for in-place source code generation in Rust. 10 | 11 | In-place source code generation is like a procedural macro in Rust, but with a difference that it is expanded before 12 | Rust code is compiled. For example, one use-case could be generating data types based on an external definition. 13 | 14 | You start with the following code: 15 | 16 | ```rust 17 | #[sourcegen::sourcegen(generator = "json-schema", schema = "widget.json")] 18 | struct Widget; 19 | ``` 20 | 21 | Then, you run a special tool that is built on top of the `sourcegen-cli` crate: 22 | 23 | ```sh 24 | cargo run --package json-schema-sourcegen 25 | ``` 26 | 27 | Which expands the code above into something like (this assumes that `widget.json` schema defines a data type with 28 | two fields, `name` and `weight`: 29 | 30 | ```rust 31 | #[sourcegen::sourcegen(generator = "json-schema", schema = "widget.json")] 32 | struct Widget { 33 | /// Name of the widget 34 | name: String, 35 | /// Weight of the widget 36 | weight: usize, 37 | } 38 | ``` 39 | 40 | Next time you run the tool, it would not change the code as it is already in it's correct form. 41 | 42 | ## Creating a Tool 43 | 44 | In the current form, you build your own tool on top of the `sourcegen_cli::run_tool` entry point. This function takes 45 | a number of input parameters and a list of source generators implementations. 46 | 47 | Source generators are similar to procedural macros, they take syntax as an input and return token stream as an output. 48 | Input to source generators use [`syn`](https://crates.io./crates/syn) crate for representing syntax trees. Returned tokens are 49 | rendered by generators into the source code and formatted via `rustfmt`. 50 | 51 | ## Rationale 52 | 53 | What are the benefits of generating source code this way compared to using procedural macros or generating code during 54 | the build via `build.rs`? 55 | 56 | Advantages over procedural macros: 57 | 58 | 1. Does not take compilation time. 59 | 2. Compilation does not depend on original metadata used for generation. 60 | 3. You have source code to look at. This is especially useful when generated code are data types of some sort. 61 | 4. The generator code can depend on the generated types (bootstrapping). 62 | 63 | Advantages over `build.rs` source generation: 64 | 65 | 1. Does not take compilation time. 66 | 2. Compilation does not depend on original metadata used for generation. 67 | 3. More flexible setup: can generate code piecemeal directly where it is used. 68 | 4. No need to include generated code via `include!` or other means (`build.rs` cannot write to sources). 69 | 70 | However, there are also some disadvantages: 71 | 72 | 1. Potential desynchronization between source of truth metadata and source code. 73 | 2. If source generators depend on their own output, harder to work on the source generators themselves (if they generate 74 | invalid code, recovering might require reverting the generated code via source control). 75 | 3. Too magical. 76 | 77 | 78 | 79 | [Crate]: https://crates.io/crates/sourcegen-cli 80 | [Crate Logo]: https://img.shields.io/crates/v/sourcegen-cli.svg 81 | 82 | [Doc]: https://docs.rs/sourcegen-cli 83 | [Doc Logo]: https://docs.rs/sourcegen-cli/badge.svg 84 | 85 | [CI Logo]: https://github.com/commure/sourcegen/workflows/Rust/badge.svg -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | 78 | -------------------------------------------------------------------------------- /cli/src/region.rs: -------------------------------------------------------------------------------- 1 | use crate::SourceGenerator; 2 | use proc_macro2::{Span, TokenStream}; 3 | use syn::spanned::Spanned; 4 | use syn::{Attribute, AttributeArgs, Item}; 5 | 6 | pub fn item_attributes(item: &mut syn::Item) -> Option<&mut Vec> { 7 | Some(match item { 8 | Item::ExternCrate(item) => &mut item.attrs, 9 | Item::Use(item) => &mut item.attrs, 10 | Item::Static(item) => &mut item.attrs, 11 | Item::Const(item) => &mut item.attrs, 12 | Item::Fn(item) => &mut item.attrs, 13 | Item::Mod(item) => &mut item.attrs, 14 | Item::ForeignMod(item) => &mut item.attrs, 15 | Item::Type(item) => &mut item.attrs, 16 | Item::Struct(item) => &mut item.attrs, 17 | Item::Enum(item) => &mut item.attrs, 18 | Item::Union(item) => &mut item.attrs, 19 | Item::Trait(item) => &mut item.attrs, 20 | Item::TraitAlias(item) => &mut item.attrs, 21 | Item::Impl(item) => &mut item.attrs, 22 | Item::Macro(item) => &mut item.attrs, 23 | Item::Macro2(item) => &mut item.attrs, 24 | _ => return None, 25 | }) 26 | } 27 | 28 | pub fn item_end_span(item: &Item) -> Span { 29 | match item { 30 | Item::ExternCrate(item) => item.semi_token.span, 31 | Item::Use(item) => item.semi_token.span, 32 | Item::Static(item) => item.semi_token.span, 33 | Item::Const(item) => item.semi_token.span, 34 | Item::Fn(item) => item.block.span(), 35 | Item::Mod(item) => { 36 | if let Some(semi) = item.semi { 37 | semi.span() 38 | } else if let Some(ref content) = item.content { 39 | content.0.span 40 | } else { 41 | item.ident.span() 42 | } 43 | } 44 | Item::ForeignMod(item) => item.brace_token.span, 45 | Item::Type(item) => item.semi_token.span(), 46 | Item::Struct(item) => { 47 | if let Some(semi) = item.semi_token { 48 | semi.span() 49 | } else { 50 | item.fields.span() 51 | } 52 | } 53 | Item::Enum(item) => item.brace_token.span, 54 | Item::Union(item) => item.fields.span(), 55 | Item::Trait(item) => item.brace_token.span, 56 | Item::TraitAlias(item) => item.semi_token.span, 57 | Item::Impl(item) => item.brace_token.span, 58 | Item::Macro(item) => { 59 | if let Some(semi) = item.semi_token { 60 | semi.span() 61 | } else { 62 | item.mac.span() 63 | } 64 | } 65 | Item::Macro2(item) => item.rules.span(), 66 | _ => unreachable!(), 67 | } 68 | } 69 | 70 | pub fn invoke_generator( 71 | item: &Item, 72 | args: AttributeArgs, 73 | generator: &dyn SourceGenerator, 74 | ) -> Result, anyhow::Error> { 75 | match item { 76 | // ExternCrate(ItemExternCrate), 77 | // Use(ItemUse), 78 | // Static(ItemStatic), 79 | // Const(ItemConst), 80 | // Fn(ItemFn), 81 | Item::Mod(item) => generator.generate_mod(args, item), 82 | // ForeignMod(ItemForeignMod), 83 | // Type(ItemType), 84 | Item::Struct(item) => generator.generate_struct(args, item), 85 | Item::Enum(item) => generator.generate_enum(args, item), 86 | // Union(ItemUnion), 87 | Item::Trait(item) => generator.generate_trait(args, item), 88 | // Impl(ItemImpl), 89 | // Macro(ItemMacro), 90 | // Macro2(ItemMacro2), 91 | // Verbatim(ItemVerbatim), 92 | // FIXME: support other? 93 | _ => return Ok(None), 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /cli/src/normalize.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::{Delimiter, Spacing, TokenStream, TokenTree}; 2 | use syn::Lit; 3 | 4 | /// Write tokens same way as `TokenStream::to_string` would do, but with normalization of doc 5 | /// attributes into `///`. 6 | pub fn write_tokens_normalized( 7 | f: &mut std::fmt::Formatter, 8 | tokens: TokenStream, 9 | ) -> std::fmt::Result { 10 | let mut tokens = tokens.into_iter().peekable(); 11 | let mut joint = false; 12 | let mut first = true; 13 | let mut plain_comment = false; 14 | while let Some(tt) = tokens.next() { 15 | if !first && !joint { 16 | write!(f, " ")?; 17 | } 18 | first = false; 19 | joint = false; 20 | 21 | // Handle plain comments 22 | if plain_comment { 23 | if let TokenTree::Literal(lit) = tt { 24 | if let Lit::Str(lit) = Lit::new(lit) { 25 | writeln!(f, "// {}", lit.value())?; 26 | } 27 | } 28 | plain_comment = false; 29 | continue; 30 | } 31 | if let Some(comment) = tokens 32 | .peek() 33 | .and_then(|lookahead| as_doc_comment(&tt, lookahead)) 34 | { 35 | let _ignore = tokens.next(); 36 | writeln!(f, "///{}", comment)?; 37 | continue; 38 | } 39 | match tt { 40 | TokenTree::Group(ref tt) => { 41 | let (start, end) = match tt.delimiter() { 42 | Delimiter::Parenthesis => ("(", ")"), 43 | Delimiter::Brace => ("{", "}"), 44 | Delimiter::Bracket => ("[", "]"), 45 | Delimiter::None => ("", ""), 46 | }; 47 | if tt.stream().into_iter().next().is_none() { 48 | write!(f, "{} {}", start, end)? 49 | } else { 50 | write!(f, "{} ", start)?; 51 | write_tokens_normalized(f, tt.stream())?; 52 | write!(f, " {}", end)? 53 | } 54 | } 55 | TokenTree::Ident(ref tt) if tt == crate::tokens::MAGIC_NEWLINE_IDENT => { 56 | writeln!(f)?; 57 | writeln!(f)?; 58 | } 59 | TokenTree::Ident(ref tt) if tt == crate::tokens::MAGIC_COMMENT_IDENT => { 60 | plain_comment = true; 61 | } 62 | TokenTree::Ident(ref tt) => write!(f, "{}", tt)?, 63 | TokenTree::Punct(ref tt) => { 64 | write!(f, "{}", tt.as_char())?; 65 | match tt.spacing() { 66 | Spacing::Alone => {} 67 | Spacing::Joint => joint = true, 68 | } 69 | } 70 | TokenTree::Literal(ref tt) => write!(f, "{}", tt)?, 71 | } 72 | } 73 | Ok(()) 74 | } 75 | 76 | fn as_doc_comment(first: &TokenTree, second: &TokenTree) -> Option { 77 | match (first, second) { 78 | (TokenTree::Punct(first), TokenTree::Group(group)) 79 | if first.as_char() == '#' && group.delimiter() == Delimiter::Bracket => 80 | { 81 | let mut it = group.stream().into_iter(); 82 | match (it.next(), it.next(), it.next()) { 83 | ( 84 | Some(TokenTree::Ident(ident)), 85 | Some(TokenTree::Punct(punct)), 86 | Some(TokenTree::Literal(lit)), 87 | ) => { 88 | if ident == "doc" && punct.as_char() == '=' { 89 | if let Lit::Str(lit) = Lit::new(lit) { 90 | return Some(lit.value()); 91 | } 92 | } 93 | } 94 | _ => {} 95 | } 96 | } 97 | _ => {} 98 | } 99 | None 100 | } 101 | -------------------------------------------------------------------------------- /cli/tests/helpers/mod.rs: -------------------------------------------------------------------------------- 1 | use pretty_assertions::assert_eq as pretty_assert_eq; 2 | use std::path::Path; 3 | use std::process::Command; 4 | 5 | /// Assert that all files at `expected` path exist at `actual` path and the contents of the files 6 | /// is the same. 7 | pub fn assert_matches_expected( 8 | base: &Path, 9 | actual: &Path, 10 | expected: &Path, 11 | ) -> Result<(), anyhow::Error> { 12 | assert_eq!( 13 | actual.is_file(), 14 | expected.is_file(), 15 | "actual '{}' is_file({}) != expected '{}' is_file({})", 16 | actual.display(), 17 | actual.is_file(), 18 | expected.display(), 19 | expected.is_file() 20 | ); 21 | 22 | if actual.is_file() { 23 | let actual_content = std::fs::read_to_string(actual)?; 24 | let expected_content = std::fs::read_to_string(expected)?; 25 | pretty_assert_eq!( 26 | PrettyString(&actual_content), 27 | PrettyString(&expected_content), 28 | "contents of actual file '{}' differs from the contents of expected file '{}'", 29 | actual.strip_prefix(base)?.display(), 30 | expected.strip_prefix(base)?.display() 31 | ); 32 | } else { 33 | let mut expected_it = std::fs::read_dir(expected)?; 34 | while let Some(expected_child) = expected_it.next() { 35 | let expected_child = expected_child?; 36 | let name = expected_child.file_name(); 37 | 38 | let actual_child = actual.join(&name); 39 | 40 | assert!( 41 | actual_child.exists(), 42 | "expected file '{}' does not exists in actual output '{}'", 43 | Path::new(&name).display(), 44 | actual.strip_prefix(base)?.display() 45 | ); 46 | 47 | assert_matches_expected(base, &actual_child, &expected_child.path())?; 48 | } 49 | 50 | // Make sure everything in `actual` also exists in `expected` 51 | let mut actual_it = std::fs::read_dir(actual)?; 52 | while let Some(actual_child) = actual_it.next() { 53 | let actual_child = actual_child?; 54 | let name = actual_child.file_name(); 55 | let expected_child = expected.join(&name); 56 | 57 | // We ignore Cargo.lock 58 | if name != "Cargo.lock" { 59 | assert!( 60 | expected_child.exists(), 61 | "actual file '{}' does not exists in expected output '{}'", 62 | Path::new(&name).display(), 63 | actual.strip_prefix(base)?.display() 64 | ); 65 | } 66 | } 67 | } 68 | 69 | Ok(()) 70 | } 71 | 72 | /// Wrapper around string slice that makes debug output `{:?}` to print string same way as `{}`. 73 | /// Used in different `assert*!` macros in combination with `pretty_assertions` crate to make 74 | /// test failures to show nice diffs. 75 | #[derive(PartialEq, Eq)] 76 | #[doc(hidden)] 77 | pub struct PrettyString<'a>(pub &'a str); 78 | 79 | /// Make diff to display string as multi-line string 80 | impl<'a> std::fmt::Debug for PrettyString<'a> { 81 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 82 | f.write_str(self.0) 83 | } 84 | } 85 | 86 | /// Install `rustfmt` component so our generator can use it. 87 | pub fn install_rustfmt(path: &Path) -> Result<(), anyhow::Error> { 88 | let output = Command::new("rustup") 89 | .arg("component") 90 | .arg("add") 91 | .arg("rustfmt") 92 | .current_dir(path.canonicalize()?) 93 | .output()?; 94 | 95 | // Ignore status, but print to the console 96 | if !output.status.success() { 97 | let err = String::from_utf8(output.stderr)?; 98 | eprintln!( 99 | "Warning: failed to install rust fmt (exit code {}): {}", 100 | output.status.code().unwrap_or(0), 101 | err 102 | ); 103 | } 104 | Ok(()) 105 | } 106 | -------------------------------------------------------------------------------- /cli/tests/generators/mod.rs: -------------------------------------------------------------------------------- 1 | use proc_macro2::TokenStream; 2 | use quote::quote; 3 | use sourcegen_cli::tokens::{NewLine, PlainComment}; 4 | use sourcegen_cli::SourceGenerator; 5 | 6 | /// Writes back the input without any changes 7 | pub struct WriteBack; 8 | 9 | impl SourceGenerator for WriteBack { 10 | fn generate_struct( 11 | &self, 12 | _args: syn::AttributeArgs, 13 | item: &syn::ItemStruct, 14 | ) -> Result, anyhow::Error> { 15 | Ok(Some(quote! { 16 | #item 17 | })) 18 | } 19 | 20 | fn generate_enum( 21 | &self, 22 | _args: syn::AttributeArgs, 23 | item: &syn::ItemEnum, 24 | ) -> Result, anyhow::Error> { 25 | Ok(Some(quote! { 26 | #item 27 | })) 28 | } 29 | 30 | fn generate_mod( 31 | &self, 32 | _args: syn::AttributeArgs, 33 | item: &syn::ItemMod, 34 | ) -> Result, anyhow::Error> { 35 | Ok(Some(quote! { 36 | #item 37 | })) 38 | } 39 | } 40 | 41 | /// Generate some impls along with the struct itself 42 | pub struct GenerateImpls; 43 | 44 | impl SourceGenerator for GenerateImpls { 45 | fn generate_struct( 46 | &self, 47 | _args: syn::AttributeArgs, 48 | item: &syn::ItemStruct, 49 | ) -> Result, anyhow::Error> { 50 | let vis = &item.vis; 51 | let ident = &item.ident; 52 | Ok(Some(quote! { 53 | #vis struct #ident; 54 | 55 | #[sourcegen::generated] 56 | impl #ident {} 57 | })) 58 | } 59 | } 60 | 61 | /// Generate one field in the struct 62 | pub struct GenerateSimple; 63 | 64 | impl SourceGenerator for GenerateSimple { 65 | fn generate_struct( 66 | &self, 67 | _args: syn::AttributeArgs, 68 | item: &syn::ItemStruct, 69 | ) -> Result, anyhow::Error> { 70 | let vis = &item.vis; 71 | let ident = &item.ident; 72 | Ok(Some(quote! { 73 | #vis struct #ident { 74 | pub hello: String, 75 | } 76 | })) 77 | } 78 | } 79 | 80 | /// Generates a struct with a doc comment 81 | pub struct GenerateDocComments; 82 | 83 | impl SourceGenerator for GenerateDocComments { 84 | fn generate_struct( 85 | &self, 86 | _args: syn::AttributeArgs, 87 | item: &syn::ItemStruct, 88 | ) -> Result, anyhow::Error> { 89 | let vis = &item.vis; 90 | let ident = &item.ident; 91 | Ok(Some(quote! { 92 | /// Some generated comment here 93 | #vis struct #ident { 94 | pub hello: String, 95 | } 96 | })) 97 | } 98 | } 99 | 100 | /// Generate full file 101 | pub struct GenerateFile; 102 | 103 | impl SourceGenerator for GenerateFile { 104 | fn generate_file( 105 | &self, 106 | _args: syn::AttributeArgs, 107 | _file: &syn::File, 108 | ) -> Result, anyhow::Error> { 109 | Ok(Some(quote! { 110 | #[doc = r" Some generated comment here"] 111 | struct Hello { 112 | pub hello: String, 113 | } 114 | })) 115 | } 116 | } 117 | 118 | /// Generates a struct with regular comments 119 | pub struct GeneratePlainComments; 120 | 121 | impl SourceGenerator for GeneratePlainComments { 122 | fn generate_struct( 123 | &self, 124 | _args: syn::AttributeArgs, 125 | item: &syn::ItemStruct, 126 | ) -> Result, anyhow::Error> { 127 | let vis = &item.vis; 128 | let ident = &item.ident; 129 | Ok(Some(quote! { 130 | #PlainComment "This is some struct!" 131 | #vis struct #ident { 132 | #PlainComment "This is some field!" 133 | pub hello: String, 134 | } 135 | })) 136 | } 137 | } 138 | 139 | /// Generates a struct with a newline between struct and impl 140 | pub struct GenerateNewLine; 141 | 142 | impl SourceGenerator for GenerateNewLine { 143 | fn generate_struct( 144 | &self, 145 | _args: syn::AttributeArgs, 146 | item: &syn::ItemStruct, 147 | ) -> Result, anyhow::Error> { 148 | let vis = &item.vis; 149 | let ident = &item.ident; 150 | Ok(Some(quote! { 151 | #vis struct #ident; 152 | #NewLine 153 | impl #ident {} 154 | })) 155 | } 156 | } 157 | 158 | /// Writes back the input without any changes 159 | pub struct GenerateTrait; 160 | 161 | impl SourceGenerator for GenerateTrait { 162 | fn generate_trait( 163 | &self, 164 | _args: syn::AttributeArgs, 165 | item: &syn::ItemTrait, 166 | ) -> Result, anyhow::Error> { 167 | let vis = &item.vis; 168 | let ident = &item.ident; 169 | Ok(Some(quote! { 170 | /// Some generated comment here 171 | #vis trait #ident { 172 | fn hello(); 173 | } 174 | })) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /cli/src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Source generator command-line utility. 2 | //! 3 | //! Utility to do in-place source generation for Rust code. Takes a list of source generators to 4 | //! run and applies them to all crates that have [`sourcegen`] dependency. 5 | //! 6 | //! [`sourcegen`]: http://crates.io/crates/sourcegen 7 | use crate::error::{SourcegenError, SourcegenErrorKind}; 8 | use proc_macro2::TokenStream; 9 | use std::collections::{BTreeSet, HashMap}; 10 | use std::path::Path; 11 | 12 | mod error; 13 | mod generate; 14 | mod mods; 15 | #[cfg(not(feature = "disable_normalize_doc_attributes"))] 16 | mod normalize; 17 | mod region; 18 | mod rustfmt; 19 | pub mod tokens; 20 | 21 | /// Trait to be implemented by source generators. 22 | pub trait SourceGenerator { 23 | /// Generate struct definition. Return `None` if no changes are necessary. 24 | fn generate_struct( 25 | &self, 26 | _args: syn::AttributeArgs, 27 | _item: &syn::ItemStruct, 28 | ) -> Result, anyhow::Error> { 29 | Ok(None) 30 | } 31 | 32 | /// Generate enum definition. Return `None` if no changes are necessary. 33 | fn generate_enum( 34 | &self, 35 | _args: syn::AttributeArgs, 36 | _item: &syn::ItemEnum, 37 | ) -> Result, anyhow::Error> { 38 | Ok(None) 39 | } 40 | 41 | /// Generate trait definition. Return `None` if no changes are necessary. 42 | fn generate_trait( 43 | &self, 44 | _args: syn::AttributeArgs, 45 | _item: &syn::ItemTrait, 46 | ) -> Result, anyhow::Error> { 47 | Ok(None) 48 | } 49 | 50 | /// Generate module. Return `None` if no changes are necessary. 51 | fn generate_mod( 52 | &self, 53 | _args: syn::AttributeArgs, 54 | _item: &syn::ItemMod, 55 | ) -> Result, anyhow::Error> { 56 | Ok(None) 57 | } 58 | 59 | /// Generate the whole file. Return `None` if no changes are necessary. 60 | fn generate_file( 61 | &self, 62 | _args: syn::AttributeArgs, 63 | _file: &syn::File, 64 | ) -> Result, anyhow::Error> { 65 | Ok(None) 66 | } 67 | } 68 | 69 | pub(crate) type GeneratorsMap<'a> = HashMap<&'a str, &'a dyn SourceGenerator>; 70 | 71 | /// Parameters for the source generation tool 72 | #[derive(Default, Clone)] 73 | pub struct SourcegenParameters<'a> { 74 | /// Root cargo manifest file to start from. If not given, the default is to use `Cargo.toml` in 75 | /// the current directory. 76 | pub manifest: Option<&'a Path>, 77 | /// List of generators to run. Each entry is a pair of generator name and trait object 78 | /// implementing the generator. 79 | pub generators: &'a [(&'a str, &'a dyn SourceGenerator)], 80 | /// List of packages to generate code for. If not given, the default is to generate code for 81 | /// all of the packages. 82 | pub packages: BTreeSet, 83 | 84 | #[doc(hidden)] 85 | pub __must_use_default: (), 86 | } 87 | 88 | /// Main entry point to the source generator toolkit. 89 | pub fn run_sourcegen(parameters: &SourcegenParameters) -> Result<(), SourcegenError> { 90 | let generators = parameters 91 | .generators 92 | .iter() 93 | .cloned() 94 | .collect::(); 95 | 96 | let mut cmd = cargo_metadata::MetadataCommand::new(); 97 | if let Some(manifest) = parameters.manifest { 98 | cmd.manifest_path(manifest); 99 | } else { 100 | let path = std::env::current_dir().context(SourcegenErrorKind::MetadataError)?; 101 | let manifest = path.join("Cargo.toml"); 102 | cmd.manifest_path(&manifest); 103 | } 104 | let metadata = cmd.exec().context(SourcegenErrorKind::MetadataError)?; 105 | 106 | // Make sure all package names are valid 107 | let mut invalid = parameters.packages.clone(); 108 | for p in &metadata.packages { 109 | invalid.remove(&p.name); 110 | } 111 | if !invalid.is_empty() { 112 | let names = invalid.into_iter().collect::>().join(", "); 113 | return Err(SourcegenErrorKind::InvalidPackageNames(names).into()); 114 | } 115 | 116 | let packages = metadata 117 | .packages 118 | .into_iter() 119 | .filter(|p| parameters.packages.is_empty() || parameters.packages.contains(&p.name)) 120 | // Only take local projects 121 | .filter(|p| p.source.is_none()) 122 | // FIXME: should we look at "rename", too? 123 | .filter(|p| p.dependencies.iter().any(|dep| dep.name == "sourcegen")); 124 | 125 | for package in packages { 126 | eprintln!("Generating source code in crate '{}'", package.name); 127 | for target in &package.targets { 128 | let parent_path = target 129 | .src_path 130 | .parent() 131 | .ok_or(SourcegenErrorKind::MetadataError)?; 132 | let mod_resolver = crate::mods::ModResolver::new(parent_path); 133 | self::generate::process_source_file(&target.src_path, &generators, &mod_resolver)?; 134 | } 135 | } 136 | Ok(()) 137 | } 138 | 139 | pub use crate::generate::process_single_file; 140 | use anyhow::Context; 141 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2019 Commure, Inc. 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /cli/src/generate.rs: -------------------------------------------------------------------------------- 1 | use crate::error::{Location, SourcegenError, SourcegenErrorKind}; 2 | use crate::mods::ModResolver; 3 | use crate::{GeneratorsMap, SourceGenerator}; 4 | use anyhow::Context; 5 | use proc_macro2::{LineColumn, TokenStream}; 6 | use std::collections::{BTreeMap, HashMap}; 7 | use std::path::Path; 8 | use syn::spanned::Spanned; 9 | use syn::{Attribute, AttributeArgs, File, Item, LitStr, Meta, NestedMeta}; 10 | 11 | static ITEM_COMMENT: &str = 12 | "// Generated. All manual edits to the block annotated with #[sourcegen...] will be discarded."; 13 | static FILE_COMMENT: &str = "// Generated. All manual edits below this line will be discarded."; 14 | 15 | #[derive(Debug, PartialOrd, Ord, PartialEq, Eq)] 16 | struct Region { 17 | from: usize, 18 | to: usize, 19 | indent: usize, 20 | } 21 | 22 | /// Replace a single file with the generated content 23 | pub fn process_single_file(path: &Path, tokens: TokenStream) -> Result<(), SourcegenError> { 24 | let formatter = crate::rustfmt::Formatter::new(path.parent().unwrap())?; 25 | 26 | let source = if path.exists() { 27 | std::fs::read_to_string(path) 28 | .with_context(|| SourcegenErrorKind::ProcessFile(path.display().to_string()))? 29 | } else { 30 | String::new() 31 | }; 32 | let replacement = Replacement { 33 | comment: FILE_COMMENT, 34 | is_cr_lf: is_cr_lf(&source), 35 | tokens: &tokens, 36 | }; 37 | let output = formatter.format(path, replacement)?; 38 | if source != output { 39 | std::fs::write(path, output) 40 | .with_context(|| SourcegenErrorKind::ProcessFile(path.display().to_string()))?; 41 | } 42 | Ok(()) 43 | } 44 | 45 | pub fn process_source_file( 46 | path: &Path, 47 | generators: &HashMap<&str, &dyn SourceGenerator>, 48 | mod_resolver: &ModResolver, 49 | ) -> Result<(), SourcegenError> { 50 | let source = std::fs::read_to_string(path) 51 | .with_context(|| SourcegenErrorKind::ProcessFile(path.display().to_string()))?; 52 | let mut file = syn::parse_file(&source) 53 | .with_context(|| SourcegenErrorKind::ProcessFile(path.display().to_string()))?; 54 | 55 | let output = if let Some(invoke) = detect_file_invocation(path, &mut file, generators)? { 56 | if !invoke.is_file { 57 | // Remove all attributes in front of the `#![sourcegen]` attribute 58 | file.attrs.drain(0..invoke.sourcegen_attr_index + 1); 59 | } 60 | 61 | // Handle full file generation 62 | let context_location = invoke.context_location; 63 | let result = invoke 64 | .generator 65 | .generate_file(invoke.args, &file) 66 | .with_context(|| SourcegenErrorKind::GeneratorError(context_location))?; 67 | if let Some(expansion) = result { 68 | let from_loc = if invoke.is_file { 69 | crate::region::item_end_span(&file.items[0]).end() 70 | } else { 71 | invoke.sourcegen_attr.bracket_token.span.end() 72 | }; 73 | let from = line_column_to_offset(&source, from_loc)?; 74 | let from = from + skip_whitespaces(&source[from..]); 75 | let region = Region { 76 | from, 77 | to: source.len(), 78 | indent: 0, 79 | }; 80 | 81 | // Replace the whole file 82 | let mut replacements = BTreeMap::new(); 83 | replacements.insert(region, expansion); 84 | render_expansions(path, &source, &replacements, FILE_COMMENT)? 85 | } else { 86 | // Nothing to replace 87 | return Ok(()); 88 | } 89 | } else { 90 | let mut replacements = BTreeMap::new(); 91 | handle_content( 92 | path, 93 | &source, 94 | &mut file.items, 95 | &generators, 96 | &mut replacements, 97 | &mod_resolver, 98 | )?; 99 | render_expansions(path, &source, &replacements, ITEM_COMMENT)? 100 | }; 101 | 102 | if source != output { 103 | std::fs::write(path, output) 104 | .with_context(|| SourcegenErrorKind::ProcessFile(path.display().to_string()))?; 105 | } 106 | Ok(()) 107 | } 108 | 109 | /// Render given list of replacements into the source file. `basefile` is used to determine base 110 | /// directory to run `rustfmt` in (so it can use local overrides for formatting rules). 111 | /// 112 | /// `comment` is the warning comment that will be added in front of each generated block. 113 | fn render_expansions( 114 | basefile: &Path, 115 | source: &str, 116 | expansions: &BTreeMap, 117 | comment: &str, 118 | ) -> Result { 119 | let mut output = String::with_capacity(source.len()); 120 | let formatter = crate::rustfmt::Formatter::new(basefile.parent().unwrap())?; 121 | 122 | let mut offset = 0; 123 | let is_cr_lf = is_cr_lf(source); 124 | for (region, tokens) in expansions { 125 | output += &source[offset..region.from]; 126 | offset = region.to; 127 | let indent = format!("{:indent$}", "", indent = region.indent); 128 | if !tokens.is_empty() { 129 | let replacement = Replacement { 130 | comment, 131 | is_cr_lf, 132 | tokens, 133 | }; 134 | let formatted = formatter.format(basefile, replacement)?; 135 | let mut first = true; 136 | for line in formatted.lines() { 137 | // We don't want newline on the last line (the captured region does not include the 138 | // one) and also we don't want an indent on the first line (we splice after it). 139 | if first { 140 | first = false 141 | } else { 142 | if is_cr_lf { 143 | output.push('\r'); 144 | } 145 | output.push('\n'); 146 | output += &indent; 147 | } 148 | output += line; 149 | } 150 | } 151 | } 152 | // Insert newline at the end of the file! 153 | if offset == source.len() { 154 | if is_cr_lf { 155 | output.push('\r'); 156 | } 157 | output.push('\n'); 158 | } 159 | output += &source[offset..]; 160 | Ok(output) 161 | } 162 | 163 | fn handle_content( 164 | path: &Path, 165 | source: &str, 166 | items: &mut [Item], 167 | generators: &GeneratorsMap, 168 | replacements: &mut BTreeMap, 169 | mod_resolver: &ModResolver, 170 | ) -> Result<(), SourcegenError> { 171 | let mut item_idx = 0; 172 | while item_idx < items.len() { 173 | item_idx += 1; 174 | let (head, tail) = items.split_at_mut(item_idx); 175 | let item = head.last_mut().unwrap(); 176 | 177 | let mut empty_attrs = Vec::new(); 178 | let attrs = crate::region::item_attributes(item).unwrap_or(&mut empty_attrs); 179 | if let Some(invoke) = detect_invocation(path, attrs, generators)? { 180 | // Remove all attributes in front of the `#[sourcegen]` attribute 181 | attrs.drain(0..invoke.sourcegen_attr_index + 1); 182 | let context_location = invoke.context_location; 183 | let result = crate::region::invoke_generator(item, invoke.args, invoke.generator) 184 | .with_context(|| SourcegenErrorKind::GeneratorError(context_location))?; 185 | if let Some(expansion) = result { 186 | let indent = invoke.sourcegen_attr.span().start().column; 187 | let from_loc = invoke.sourcegen_attr.bracket_token.span.end(); 188 | let from = line_column_to_offset(source, from_loc)?; 189 | let from = from + skip_whitespaces(&source[from..]); 190 | 191 | // Find the first item that is not marked as "generated" 192 | let skip_count = (0..tail.len()) 193 | .find(|pos| { 194 | !is_generated( 195 | crate::region::item_attributes(&mut tail[*pos]) 196 | .unwrap_or(&mut empty_attrs), 197 | ) 198 | }) 199 | .unwrap_or(tail.len()); 200 | let to_span = if skip_count == 0 { 201 | crate::region::item_end_span(item) 202 | } else { 203 | // Skip consecutive items marked via `#[sourcegen::generated]` 204 | item_idx += skip_count; 205 | crate::region::item_end_span(&tail[skip_count - 1]) 206 | }; 207 | let to = line_column_to_offset(source, to_span.end())?; 208 | 209 | let region = Region { from, to, indent }; 210 | replacements.insert(region, expansion); 211 | continue; 212 | } 213 | } 214 | 215 | if let Item::Mod(item) = item { 216 | let nested_mod_resolved = mod_resolver.push_module(&item.ident.to_string()); 217 | if item.content.is_some() { 218 | let items = &mut item.content.as_mut().unwrap().1; 219 | handle_content( 220 | path, 221 | source, 222 | items, 223 | generators, 224 | replacements, 225 | &nested_mod_resolved, 226 | )?; 227 | } else { 228 | let mod_file = mod_resolver.resolve_module_file(item)?; 229 | process_source_file(&mod_file, generators, &nested_mod_resolved)?; 230 | } 231 | } 232 | } 233 | Ok(()) 234 | } 235 | 236 | fn is_generated(attrs: &[Attribute]) -> bool { 237 | let sourcegen_attr = attrs.iter().find(|attr| { 238 | attr.path 239 | .segments 240 | .first() 241 | .map_or(false, |segment| segment.ident == "sourcegen") 242 | }); 243 | if let Some(sourcegen) = sourcegen_attr { 244 | sourcegen 245 | .path 246 | .segments 247 | .iter() 248 | .skip(1) 249 | .next() 250 | .map_or(false, |segment| segment.ident == "generated") 251 | } else { 252 | false 253 | } 254 | } 255 | 256 | fn detect_file_invocation<'a>( 257 | path: &Path, 258 | file: &mut File, 259 | generators: &'a GeneratorsMap, 260 | ) -> Result>, SourcegenError> { 261 | if let Some(mut invoke) = detect_invocation(path, &mut file.attrs, generators)? { 262 | // This flag should only be set when we are processing a special workaround 263 | invoke.is_file = false; 264 | return Ok(Some(invoke)); 265 | } 266 | 267 | if let Some(item) = file.items.iter_mut().next() { 268 | // Special case: if first item in the file has `sourcegen::sourcegen` attribute with `file` set 269 | // to `true`, we treat it as file sourcegen. 270 | let mut empty_attrs = Vec::new(); 271 | let attrs = crate::region::item_attributes(item).unwrap_or(&mut empty_attrs); 272 | if let Some(invoke) = detect_invocation(path, &mut attrs.clone(), generators)? { 273 | if invoke.is_file { 274 | return Ok(Some(invoke)); 275 | } 276 | } 277 | } 278 | Ok(None) 279 | } 280 | 281 | /// Collect parameters from `#[sourcegen]` attribute. 282 | fn detect_invocation<'a>( 283 | path: &Path, 284 | attrs: &[Attribute], 285 | generators: &'a GeneratorsMap, 286 | ) -> Result>, SourcegenError> { 287 | let sourcegen_attr = attrs.iter().position(|attr| { 288 | attr.path 289 | .segments 290 | .first() 291 | .map_or(false, |segment| segment.ident == "sourcegen") 292 | }); 293 | if let Some(attr_pos) = sourcegen_attr { 294 | let invoke = detect_generator(path, attrs, attr_pos, generators)?; 295 | Ok(Some(invoke)) 296 | } else { 297 | Ok(None) 298 | } 299 | } 300 | 301 | /// Map from the line number and column back to the offset. 302 | fn line_column_to_offset(text: &str, lc: LineColumn) -> Result { 303 | let mut line = lc.line as usize; 304 | 305 | assert_ne!(line, 0, "line number must be 1-indexed"); 306 | 307 | let mut offset = 0; 308 | for (idx, ch) in text.char_indices() { 309 | offset = idx; 310 | if line == 1 { 311 | break; 312 | } 313 | if ch == '\n' { 314 | line -= 1; 315 | } 316 | } 317 | offset += lc.column; 318 | Ok(offset.min(text.len())) 319 | } 320 | 321 | fn skip_whitespaces(text: &str) -> usize { 322 | let end = text.trim_start().as_ptr() as usize; 323 | let start = text.as_ptr() as usize; 324 | end - start 325 | } 326 | 327 | struct GeneratorInfo<'a> { 328 | /// Source generator to run 329 | generator: &'a dyn SourceGenerator, 330 | args: AttributeArgs, 331 | /// `#[sourcegen]` attribute itself 332 | sourcegen_attr: Attribute, 333 | /// Index of `#[sourcegen]` attribute 334 | sourcegen_attr_index: usize, 335 | /// Location for error reporting 336 | context_location: Location, 337 | /// If this invocation should regenerate the whole block up to the end. 338 | /// (this is used as a workaround for attributes not allowed on modules) 339 | is_file: bool, 340 | } 341 | 342 | fn detect_generator<'a>( 343 | path: &Path, 344 | attrs: &[Attribute], 345 | sourcegen_attr_index: usize, 346 | generators: &'a GeneratorsMap, 347 | ) -> Result, SourcegenError> { 348 | let sourcegen_attr = attrs[sourcegen_attr_index].clone(); 349 | 350 | let loc = Location::from_path_span(path, sourcegen_attr.span()); 351 | let meta = sourcegen_attr 352 | .parse_meta() 353 | .with_context(|| SourcegenErrorKind::GeneratorError(loc.clone()))?; 354 | 355 | let meta_span = meta.span(); 356 | if let Meta::List(list) = meta { 357 | let mut name: Option<&LitStr> = None; 358 | let mut is_file = false; 359 | for item in &list.nested { 360 | match item { 361 | NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("generator") => { 362 | if let syn::Lit::Str(ref value) = nv.lit { 363 | if name.is_some() { 364 | let loc = Location::from_path_span(path, item.span()); 365 | return Err(SourcegenErrorKind::MultipleGeneratorAttributes(loc).into()); 366 | } 367 | name = Some(value); 368 | } else { 369 | let loc = Location::from_path_span(path, item.span()); 370 | return Err(SourcegenErrorKind::GeneratorAttributeMustBeString(loc).into()); 371 | } 372 | } 373 | NestedMeta::Meta(Meta::NameValue(nv)) if nv.path.is_ident("file") => { 374 | if let syn::Lit::Bool(ref value) = nv.lit { 375 | is_file = value.value; 376 | } 377 | } 378 | _ => {} 379 | } 380 | } 381 | if let Some(name) = name { 382 | let name_span = name.span(); 383 | let name = name.value(); 384 | let args = list.nested.into_iter().collect::>(); 385 | let context_location = Location::from_path_span(path, meta_span); 386 | let generator = *generators.get(name.as_str()).ok_or_else(|| { 387 | SourcegenErrorKind::GeneratorNotFound( 388 | Location::from_path_span(path, name_span), 389 | name, 390 | ) 391 | })?; 392 | return Ok(GeneratorInfo { 393 | generator, 394 | args, 395 | sourcegen_attr_index, 396 | sourcegen_attr, 397 | context_location, 398 | is_file, 399 | }); 400 | } 401 | } 402 | 403 | let loc = Location::from_path_span(path, meta_span); 404 | Err(SourcegenErrorKind::MissingGeneratorAttribute(loc).into()) 405 | } 406 | 407 | /// Look at the first newline and decide if we should use `\r\n` (Windows). 408 | fn is_cr_lf(source: &str) -> bool { 409 | if let Some(pos) = source.find('\n') { 410 | source[..pos].ends_with('\r') 411 | } else { 412 | false 413 | } 414 | } 415 | 416 | /// Struct used to generate replacement code directly into stdin of `rustfmt`. 417 | struct Replacement<'a> { 418 | comment: &'a str, 419 | is_cr_lf: bool, 420 | tokens: &'a TokenStream, 421 | } 422 | 423 | impl std::fmt::Display for Replacement<'_> { 424 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 425 | use std::fmt::Write; 426 | 427 | f.write_str(self.comment)?; 428 | if self.is_cr_lf { 429 | f.write_char('\r')?; 430 | } 431 | f.write_char('\n')?; 432 | 433 | #[cfg(feature = "disable_normalize_doc_attributes")] 434 | write!(f, "{}", self.tokens)?; 435 | 436 | #[cfg(not(feature = "disable_normalize_doc_attributes"))] 437 | crate::normalize::write_tokens_normalized(f, self.tokens.clone())?; 438 | 439 | Ok(()) 440 | } 441 | } 442 | --------------------------------------------------------------------------------