├── _config.yml ├── src ├── ctags │ ├── mod.rs │ ├── ctags_opt.rs │ ├── ctags_cmd.rs │ └── ctags_parser.rs ├── parse_option.rs ├── segment │ ├── camelcase_tok.rs │ ├── mod.rs │ └── stop_words.rs ├── coco_struct.rs ├── bin │ ├── diffing.rs │ ├── visualing.rs │ ├── concepting.rs │ └── modeling.rs ├── render │ ├── plantuml_render.rs │ ├── mermaid_render.rs │ ├── graphviz_render.rs │ └── mod.rs ├── file_filter.rs └── lib.rs ├── _fixtures ├── ctags │ ├── main.go │ ├── source │ │ ├── animal.ts │ │ ├── datastore.go │ │ ├── TypeName.java │ │ └── field.cpp │ ├── ts_tags │ ├── coco_tags │ ├── java_tags │ ├── go_tags │ ├── coco_class_tags │ └── cpp_tags └── rust │ └── render_tags ├── .gitignore ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── workflows │ ├── build.yml │ └── release.yml └── CONTRIBUTING.md ├── static ├── index.html ├── stylesheets │ └── main.css └── js │ └── chart.js ├── justfile ├── LICENSE ├── Cargo.toml ├── cliff.toml ├── README.md └── CHANGELOG.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-slate -------------------------------------------------------------------------------- /src/ctags/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod ctags_cmd; 2 | pub mod ctags_opt; 3 | pub mod ctags_parser; 4 | -------------------------------------------------------------------------------- /_fixtures/ctags/main.go: -------------------------------------------------------------------------------- 1 | func main() { 2 | 3 | } 4 | 5 | type person struct { 6 | name string 7 | age int 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | Cargo.lock 3 | .idea 4 | demo.puml 5 | tags 6 | *.puml 7 | temps/ 8 | output.csv 9 | demo.log 10 | output_text.csv 11 | output_word.csv 12 | modeling.svg 13 | modeling.dot 14 | output.json 15 | debug.json -------------------------------------------------------------------------------- /src/parse_option.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 4 | pub struct ParseOption { 5 | pub merge_method_name: bool, 6 | pub field_only: bool, 7 | pub inline_id_suffix: bool, 8 | pub without_parent: bool, 9 | pub without_impl_suffix: bool, 10 | pub without_suffix: String, 11 | } 12 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐞 Bug report 3 | about: Create a report about something that is not working 4 | --- 5 | 6 | 7 | ### Describe the bug 8 | A clear and concise description of what the bug is. 9 | 10 | ### Steps to reproduce (please include code) 11 | 12 | 13 | ### Environment 14 | - coco version 15 | - Rust version 16 | - OS: [e.g. OSX 10.13.4, Windows 10] 17 | -------------------------------------------------------------------------------- /src/segment/camelcase_tok.rs: -------------------------------------------------------------------------------- 1 | use tokenizers::{PreTokenizedString, PreTokenizer, SplitDelimiterBehavior}; 2 | 3 | #[derive(Clone, Debug)] 4 | pub struct CamelCaseTok; 5 | 6 | impl Default for CamelCaseTok { 7 | fn default() -> Self { 8 | Self 9 | } 10 | } 11 | 12 | impl PreTokenizer for CamelCaseTok { 13 | fn pre_tokenize(&self, pre_tokenized: &mut PreTokenizedString) -> tokenizers::Result<()> { 14 | pre_tokenized.split(|_, normalized| { 15 | normalized.split(char::is_uppercase, SplitDelimiterBehavior::MergedWithNext) 16 | }) 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Modeling simple Visualization 6 | 7 | 8 | 9 | 10 |
11 | Order: 12 | 17 |
18 |
19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /_fixtures/ctags/source/animal.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/microsoft/TypeScriptSamples/blob/master/simple/animals.ts 2 | 3 | class Animal { 4 | name : string 5 | constructor(name : string) { 6 | this.name = name; 7 | } 8 | move(meters) { 9 | console.log(this.name + " moved " + meters + "m."); 10 | } 11 | } 12 | 13 | class Snake extends Animal { 14 | move() { 15 | console.log("Slithering..."); 16 | super.move(5); 17 | } 18 | } 19 | 20 | class Horse extends Animal { 21 | move() { 22 | console.log("Galloping..."); 23 | super.move(45); 24 | } 25 | } 26 | 27 | var sam = new Snake("Sammy the Python") 28 | var tom: Animal = new Horse("Tommy the Palomino") 29 | 30 | sam.move() 31 | tom.move(34) 32 | -------------------------------------------------------------------------------- /static/stylesheets/main.css: -------------------------------------------------------------------------------- 1 | .hover path { 2 | stroke: #ccc; 3 | } 4 | 5 | .hover text { 6 | fill: #ccc; 7 | } 8 | 9 | .hover g.primary text { 10 | fill: black; 11 | font-weight: bold; 12 | } 13 | 14 | .hover g.secondary text { 15 | fill: #333; 16 | } 17 | 18 | .hover path.primary { 19 | stroke: #333; 20 | stroke-opacity: 1; 21 | } 22 | 23 | div.tooltip { 24 | position: absolute; 25 | text-align: left; 26 | width: auto; 27 | height: auto; 28 | padding: 12px; 29 | font: 14px sans-serif; 30 | background: lightsteelblue; 31 | border: 0; 32 | border-radius: 8px; 33 | /*pointer-events: none;*/ 34 | } 35 | 36 | #packing, 37 | #circle { 38 | width: 1200px; 39 | height: 1200px; 40 | } 41 | 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: "\U0001F680 Feature Request" 3 | about: "I have a suggestion (and may want to implement it \U0001F642)!" 4 | title: '' 5 | labels: 'i: enhancement, i: needs triage' 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Feature Request 11 | 12 | **Is your feature request related to a problem? Please describe.** 13 | A clear and concise description of what the problem is. Ex. I have an issue when [...] 14 | 15 | **Describe the solution you'd like** 16 | A clear and concise description of what you want to happen. Add any considered drawbacks. 17 | 18 | **Describe alternatives you've considered** 19 | A clear and concise description of any alternative solutions or features you've considered. 20 | 21 | **Teachability, Documentation, Adoption, Migration Strategy** 22 | If you can, explain how users will be able to use this and possibly write out a version the docs. 23 | Maybe a screenshot or design? 24 | -------------------------------------------------------------------------------- /justfile: -------------------------------------------------------------------------------- 1 | setup: 2 | cargo install git-cliff 3 | rustup component add llvm-tools-preview --toolchain nightly 4 | cargo install cargo-llvm-cov 5 | 6 | install: 7 | cargo install --path . 8 | 9 | build: 10 | cargo build --all 11 | 12 | test: 13 | cargo test --all 14 | 15 | release: 16 | cargo build --verbose --release --all 17 | 18 | coverage: 19 | cargo llvm-cov --all-features --workspace --html 20 | 21 | @bench: 22 | cargo bench 23 | 24 | @lint: 25 | rustup component add clippy 26 | rustup component add rustfmt 27 | cargo clippy -- -D warnings 28 | cargo clippy --tests 29 | cargo fmt -- --check 30 | 31 | @fix: 32 | cargo fmt --all 33 | 34 | # cargo install cargo-bloat 35 | @analysis: 36 | cargo bloat --release -n 50 37 | 38 | clean: 39 | cargo clean 40 | find . -type f -name "*.orig" -exec rm {} \; 41 | find . -type f -name "*.bk" -exec rm {} \; 42 | find . -type f -name ".*~" -exec rm {} \; 43 | 44 | changelog: 45 | git cliff --output CHANGELOG.md 46 | -------------------------------------------------------------------------------- /src/segment/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod camelcase_tok; 2 | pub mod stop_words; 3 | 4 | use crate::segment::camelcase_tok::CamelCaseTok; 5 | use tokenizers::{OffsetReferential, OffsetType, PreTokenizedString, PreTokenizer}; 6 | 7 | pub fn segment(str: &str) -> Vec { 8 | segment_camelcase(str) 9 | } 10 | 11 | pub fn segment_camelcase(str: &str) -> Vec { 12 | let pretok = CamelCaseTok::default(); 13 | 14 | let mut pretokenized = PreTokenizedString::from(str); 15 | pretok.pre_tokenize(&mut pretokenized).unwrap(); 16 | 17 | let vec = pretokenized 18 | .get_splits(OffsetReferential::Original, OffsetType::Byte) 19 | .into_iter() 20 | .map(|(s, _o, _)| (s.to_string())) 21 | .collect::>(); 22 | 23 | vec 24 | } 25 | 26 | #[cfg(test)] 27 | mod tests { 28 | use crate::segment::segment; 29 | 30 | #[test] 31 | fn should_segmentation() { 32 | assert_eq!( 33 | vec!["Hierarchy".to_string(), "Id".to_string()], 34 | segment("HierarchyId") 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /_fixtures/ctags/source/datastore.go: -------------------------------------------------------------------------------- 1 | // tags2uml 2 | // Copyright 2014 ruben2020 https://github.com/ruben2020/ 3 | // 4 | // Licensed under the Apache License, Version 2.0 (the "License"); 5 | // you may not use this file except in compliance with the License. 6 | // You may obtain a copy of the License at 7 | // 8 | // http://www.apache.org/licenses/LICENSE-2.0 9 | // 10 | // Unless required by applicable law or agreed to in writing, software 11 | // distributed under the License is distributed on an "AS IS" BASIS, 12 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | // See the License for the specific language governing permissions and 14 | // limitations under the License. 15 | 16 | package pkg 17 | 18 | type memberinfo_st struct { 19 | name, access, datatype string 20 | } 21 | 22 | type methodinfo_st struct { 23 | name, access, returntype string 24 | } 25 | 26 | type classinfo_st struct { 27 | name string 28 | id int 29 | parents []string 30 | members []memberinfo_st 31 | methods []methodinfo_st 32 | } 33 | 34 | var classmap map[string]classinfo_st 35 | var idcounter int = 1 36 | 37 | func InitDatastore() { 38 | classmap = make(map[string]classinfo_st) 39 | } 40 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Coco Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build: 7 | strategy: 8 | matrix: 9 | os: [macos-latest, ubuntu-latest, windows-latest] 10 | runs-on: ${{ matrix.os }} 11 | steps: 12 | - uses: actions/checkout@v2 13 | - name: Checkout submodules 14 | shell: bash 15 | run: | 16 | git fetch --tags 17 | auth_header="$(git config --local --get http.https://github.com/.extraheader)" 18 | git submodule sync --recursive 19 | git -c "http.extraheader=$auth_header" -c protocol.version=2 submodule update --init --force --recursive --depth=1 20 | 21 | - name: Install ctags on Linux 22 | if: matrix.os == 'ubuntu-latest' 23 | run: | 24 | sudo snap install universal-ctags 25 | 26 | - name: Install ctags on macOS 27 | if: matrix.os == 'macOS-latest' 28 | run: | 29 | brew update 30 | brew install --HEAD universal-ctags/universal-ctags/universal-ctags 31 | 32 | - name: Install ctags on Windows 33 | if: matrix.os == 'windows-latest' 34 | run: | 35 | choco install universal-ctags 36 | 37 | - name: Run unit tests 38 | run: ${{matrix.ENV_VARS}} cargo test 39 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | ctags analysis based on [https://github.com/dalance/ptags](https://github.com/dalance/ptags) with MIT, see in [src](plugins/coco_struct_analysis/src) 4 | 5 | ctags parser rewrite from Golang's [https://github.com/ruben2020/tags2uml](https://github.com/ruben2020/tags2uml) with Apache License. 6 | 7 | Copyright (c) 2021 Inherd Group 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in all 17 | copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 25 | SOFTWARE. 26 | -------------------------------------------------------------------------------- /_fixtures/ctags/ts_tags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ 2 | !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/ 4 | !_TAG_OUTPUT_FILESEP slash /slash or backslash/ 5 | !_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ 6 | !_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/ 7 | !_TAG_PROC_CWD /Users/chalme/CLionProjects/coco/_fixtures/ctags/source/ // 8 | !_TAG_PROGRAM_AUTHOR Universal Ctags Team // 9 | !_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ 10 | !_TAG_PROGRAM_URL https://ctags.io/ /official site/ 11 | !_TAG_PROGRAM_VERSION 5.9.0 /98ff77d/ 12 | Animal animal.ts /^class Animal {$/;" class line:3 language:TypeScript 13 | Horse animal.ts /^class Horse extends Animal {$/;" class line:20 language:TypeScript inherits:Animal 14 | Snake animal.ts /^class Snake extends Animal {$/;" class line:13 language:TypeScript inherits:Animal 15 | constructor animal.ts /^ constructor(name : string) {$/;" method line:5 language:TypeScript class:Animal access:public 16 | move animal.ts /^ move() {$/;" method line:14 language:TypeScript class:Snake access:public 17 | move animal.ts /^ move() {$/;" method line:21 language:TypeScript class:Horse access:public 18 | move animal.ts /^ move(meters) {$/;" method line:8 language:TypeScript class:Animal access:public 19 | name animal.ts /^ name : string$/;" property line:4 language:TypeScript class:Animal access:public 20 | sam animal.ts /^var sam = new Snake("Sammy the Python")$/;" variable line:27 language:TypeScript 21 | tom animal.ts /^var tom: Animal = new Horse("Tommy the Palomino")$/;" variable line:28 language:TypeScript 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "modeling" 3 | version = "0.6.2" 4 | authors = ["Inherd Group ", "Phodal Huang "] 5 | edition = "2018" 6 | license = "MIT" 7 | readme = "README.md" 8 | repository = "https://github.com/inherd/modeling" 9 | documentation = "https://github.com/inherd/modeling" 10 | homepage = "https://github.com/inherd/modeling" 11 | description = """ 12 | Modeling is a tools to analysis different languages by Ctags 13 | """ 14 | categories = ["text-processing", "command-line-interface", "development-tools"] 15 | exclude = [ 16 | ".github/*", 17 | ".gitattributes", 18 | ".adr.json", 19 | ] 20 | 21 | [dependencies] 22 | nix = "0.19" 23 | tempfile = "3" 24 | failure = "0.1" 25 | 26 | # serialize 27 | serde = "1" 28 | serde_derive = "1" 29 | serde_json = "1" 30 | 31 | lazy_static = "1.4.0" 32 | 33 | regex = "1" 34 | 35 | # gitignore 36 | # docs: https://github.com/BurntSushi/ripgrep/tree/master/crates/ignore 37 | ignore = "0.4" 38 | 39 | clap = "3.0.0-beta.2" 40 | 41 | # command args to struct 42 | structopt = "0.3" 43 | structopt-toml = "0.4" 44 | 45 | # https://crates.io/crates/grep-regex 46 | grep-regex = "0.1.9" 47 | grep-searcher = "0.1.8" 48 | 49 | similar = { version = "2.1.0", features = ["text", "inline", "bytes"] } 50 | console = "0.15.0" 51 | 52 | prettytable-rs = "^0.8" 53 | 54 | env_logger = "0.8.4" 55 | log = "0.4" 56 | 57 | tokenizers = "0.11.0" 58 | 59 | # visual for embed files 60 | rust-embed = "6.3.0" 61 | 62 | # visual for web 63 | actix-web = { version = "3", default-features = false } 64 | actix-rt = "2.6.0" 65 | 66 | mime_guess = "2.0.3" 67 | 68 | # open URLs in browsers 69 | webbrowser = "0.5.5" 70 | -------------------------------------------------------------------------------- /_fixtures/rust/render_tags: -------------------------------------------------------------------------------- 1 | DData graphviz_render.rs /^pub struct DData {$/;" struct line:14 language:Rust 2 | DLink graphviz_render.rs /^pub struct DLink {$/;" struct line:27 language:Rust 3 | DNode graphviz_render.rs /^pub struct DNode {$/;" struct line:20 language:Rust 4 | GraphvizRender graphviz_render.rs /^impl GraphvizRender {$/;" implementation line:34 language:Rust 5 | GraphvizRender graphviz_render.rs /^pub struct GraphvizRender;$/;" struct line:11 language:Rust 6 | group graphviz_render.rs /^ group: usize$/;" field line:23 language:Rust struct:DNode 7 | id graphviz_render.rs /^ id: String,$/;" field line:21 language:Rust struct:DNode 8 | links graphviz_render.rs /^ links: Vec$/;" field line:16 language:Rust struct:DData 9 | nodes graphviz_render.rs /^ nodes: Vec,$/;" field line:15 language:Rust struct:DData 10 | package graphviz_render.rs /^ package: String,$/;" field line:22 language:Rust struct:DNode 11 | package graphviz_render.rs /^ pub package: String,$/;" field line:31 language:Rust struct:DLink 12 | render graphviz_render.rs /^ pub fn render(classes: &Vec, parse_option: &ParseOption) -> String {$/;" method line:35 language:Rust implementation:GraphvizRender 13 | should_render_graphviz graphviz_render.rs /^ fn should_render_graphviz() {$/;" function line:136 language:Rust module:tests 14 | source graphviz_render.rs /^ source: String,$/;" field line:28 language:Rust struct:DLink 15 | target graphviz_render.rs /^ target: String,$/;" field line:29 language:Rust struct:DLink 16 | tests graphviz_render.rs /^mod tests {$/;" module line:131 language:Rust 17 | value graphviz_render.rs /^ value: usize,$/;" field line:30 language:Rust struct:DLink -------------------------------------------------------------------------------- /_fixtures/ctags/coco_tags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ 2 | !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/ 4 | !_TAG_OUTPUT_FILESEP slash /slash or backslash/ 5 | !_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ 6 | !_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/ 7 | !_TAG_PROC_CWD /Users/fdhuang/consultant/devops/coco/ // 8 | !_TAG_PROGRAM_AUTHOR Universal Ctags Team // 9 | !_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ 10 | !_TAG_PROGRAM_URL https://ctags.io/ /official site/ 11 | !_TAG_PROGRAM_VERSION 5.9.0 /d532b5c/ 12 | CocoSwagger coco_swagger/src/lib.rs /^impl Default for CocoSwagger {$/;" implementation line:20 language:Rust 13 | CocoSwagger coco_swagger/src/lib.rs /^impl PluginInterface for CocoSwagger {$/;" implementation line:6 language:Rust 14 | CocoSwagger coco_swagger/src/lib.rs /^pub struct CocoSwagger {}$/;" struct line:4 language:Rust 15 | default coco_swagger/src/lib.rs /^ fn default() -> Self {$/;" method line:21 language:Rust implementation:CocoSwagger 16 | execute coco_swagger/src/lib.rs /^ fn execute(&self, config: CocoConfig) {$/;" method line:15 language:Rust implementation:CocoSwagger 17 | name coco_swagger/src/lib.rs /^ fn name(&self) -> &'static str {$/;" method line:7 language:Rust implementation:CocoSwagger 18 | on_plugin_load coco_swagger/src/lib.rs /^ fn on_plugin_load(&self) {}$/;" method line:11 language:Rust implementation:CocoSwagger 19 | on_plugin_unload coco_swagger/src/lib.rs /^ fn on_plugin_unload(&self) {}$/;" method line:13 language:Rust implementation:CocoSwagger 20 | plugin coco_swagger/src/lib.rs /^pub fn plugin() -> Box {$/;" function line:27 language:Rust 21 | -------------------------------------------------------------------------------- /cliff.toml: -------------------------------------------------------------------------------- 1 | # configuration file for git-cliff (0.1.0) 2 | 3 | [changelog] 4 | # changelog header 5 | header = """ 6 | # Changelog 7 | All notable changes to this project will be documented in this file.\n 8 | """ 9 | # template for the changelog body 10 | # https://tera.netlify.app/docs/#introduction 11 | body = """ 12 | {% if version %}\ 13 | ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} 14 | {% else %}\ 15 | ## [unreleased] 16 | {% endif %}\ 17 | {% for group, commits in commits | group_by(attribute="group") %} 18 | ### {{ group | upper_first }} 19 | {% for commit in commits %} 20 | - {{ commit.message | upper_first }}\ 21 | {% endfor %} 22 | {% endfor %}\n 23 | """ 24 | # remove the leading and trailing whitespaces from the template 25 | trim = true 26 | # changelog footer 27 | footer = """ 28 | 29 | """ 30 | 31 | [git] 32 | # allow only conventional commits 33 | # https://www.conventionalcommits.org 34 | conventional_commits = true 35 | # regex for parsing and grouping commits 36 | commit_parsers = [ 37 | { message = "^feat", group = "Features"}, 38 | { message = "^fix", group = "Bug Fixes"}, 39 | { message = "^doc", group = "Documentation"}, 40 | { message = "^perf", group = "Performance"}, 41 | { message = "^refactor", group = "Refactor"}, 42 | { message = "^style", group = "Styling"}, 43 | { message = "^test", group = "Testing"}, 44 | { message = "^chore\\(release\\): prepare for", skip = true}, 45 | { message = "^chore", group = "Miscellaneous Tasks"}, 46 | { body = ".*security", group = "Security"}, 47 | ] 48 | # filter out the commits that are not matched by commit parsers 49 | filter_commits = false 50 | # glob pattern for matching git tags 51 | tag_pattern = "v[0-9]*" 52 | # regex for skipping tags 53 | skip_tags = "v0.1.0-beta.1" 54 | -------------------------------------------------------------------------------- /src/coco_struct.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[derive(Serialize, Deserialize, Debug, Clone)] 4 | pub struct MemberInfo { 5 | pub name: String, 6 | pub access: String, 7 | pub data_type: String, 8 | pub pure_data_type: String, 9 | pub line_no: i32, 10 | } 11 | 12 | impl MemberInfo { 13 | pub fn new(name: &str, access: String, data_type: String) -> Self { 14 | MemberInfo { 15 | name: name.to_string(), 16 | access, 17 | data_type, 18 | pure_data_type: "".to_string(), 19 | line_no: 0, 20 | } 21 | } 22 | } 23 | 24 | #[derive(Serialize, Deserialize, Debug, Clone)] 25 | pub struct MethodInfo { 26 | pub name: String, 27 | pub access: String, 28 | pub parameters: Vec, 29 | pub return_type: String, 30 | pub pure_return_type: String, 31 | pub line_no: i32, 32 | } 33 | 34 | impl MethodInfo { 35 | pub fn new(name: &str, access: String, parameters: Vec, return_type: String) -> Self { 36 | MethodInfo { 37 | name: name.to_string(), 38 | access, 39 | parameters, 40 | return_type, 41 | pure_return_type: "".to_string(), 42 | line_no: 0, 43 | } 44 | } 45 | } 46 | 47 | #[derive(Serialize, Deserialize, Debug, Clone)] 48 | pub struct ClassInfo { 49 | pub id: i32, 50 | pub name: String, 51 | pub package: String, 52 | pub file: String, 53 | pub lang: String, 54 | pub parents: Vec, 55 | pub members: Vec, 56 | pub methods: Vec, 57 | } 58 | 59 | impl ClassInfo { 60 | pub fn new(class_name: &str) -> Self { 61 | ClassInfo { 62 | id: 0, 63 | name: class_name.to_string(), 64 | package: "".to_string(), 65 | file: "".to_string(), 66 | lang: "".to_string(), 67 | parents: vec![], 68 | members: vec![], 69 | methods: vec![], 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/bin/diffing.rs: -------------------------------------------------------------------------------- 1 | use std::fmt; 2 | use std::fs::read; 3 | use std::process::exit; 4 | 5 | use console::{style, Style}; 6 | use similar::{ChangeTag, TextDiff}; 7 | 8 | struct Line(Option); 9 | 10 | impl fmt::Display for Line { 11 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 12 | match self.0 { 13 | None => write!(f, " "), 14 | Some(idx) => write!(f, "{:<4}", idx + 1), 15 | } 16 | } 17 | } 18 | 19 | fn main() { 20 | let args: Vec<_> = std::env::args_os().collect(); 21 | if args.len() < 3 { 22 | eprintln!("usage: diffing [old] [new]"); 23 | exit(1); 24 | } 25 | 26 | let old = read(&args[1]).unwrap(); 27 | let new = read(&args[2]).unwrap(); 28 | 29 | let diff = TextDiff::from_lines(&old, &new); 30 | 31 | for (idx, group) in diff.grouped_ops(3).iter().enumerate() { 32 | if idx > 0 { 33 | println!("{:-^1$}", "-", 80); 34 | } 35 | for op in group { 36 | for change in diff.iter_inline_changes(op) { 37 | let (sign, s) = match change.tag() { 38 | ChangeTag::Delete => ("-", Style::new().red()), 39 | ChangeTag::Insert => ("+", Style::new().green()), 40 | ChangeTag::Equal => (" ", Style::new().dim()), 41 | }; 42 | print!( 43 | "{}{} |{}", 44 | style(Line(change.old_index())).dim(), 45 | style(Line(change.new_index())).dim(), 46 | s.apply_to(sign).bold(), 47 | ); 48 | for (emphasized, value) in change.iter_strings_lossy() { 49 | if emphasized { 50 | print!("{}", s.apply_to(value).underlined().on_black()); 51 | } else { 52 | print!("{}", s.apply_to(value)); 53 | } 54 | } 55 | if change.missing_newline() { 56 | println!(); 57 | } 58 | } 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/render/plantuml_render.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use crate::coco_struct::ClassInfo; 4 | use crate::render::{process_name, render_member, render_method}; 5 | use crate::ParseOption; 6 | 7 | /// Render classes info to string 8 | pub struct PlantUmlRender; 9 | 10 | impl PlantUmlRender { 11 | pub fn render(classes: &Vec, parse_option: &ParseOption) -> String { 12 | let mut rendered: Vec = vec![]; 13 | let mut deps: Vec = vec![]; 14 | 15 | let mut class_map: HashMap = HashMap::default(); 16 | for clazz in classes { 17 | class_map.insert(process_name(&parse_option, &clazz.name), true); 18 | } 19 | 20 | for clazz in classes { 21 | let mut dep_map: HashMap = HashMap::default(); 22 | 23 | let members = render_member(&clazz, &mut dep_map, "", parse_option, &mut class_map); 24 | let mut methods = vec![]; 25 | if !parse_option.field_only { 26 | methods = render_method(&clazz, &mut dep_map, "", parse_option); 27 | } 28 | 29 | let content = format!("{}{}", members.join(""), methods.join("")); 30 | let clazz_name = process_name(&parse_option, &clazz.name); 31 | if clazz.parents.len() > 0 && !parse_option.without_parent { 32 | for parent in &clazz.parents { 33 | rendered.push(format!("{} <|-- {}", parent, clazz.name)); 34 | } 35 | } 36 | 37 | rendered.push(format!("class {} {{\n{}}}", clazz_name, content)); 38 | 39 | for (callee, current_clz) in dep_map { 40 | if callee == current_clz { 41 | continue; 42 | } 43 | 44 | if class_map.get(&callee).is_none() { 45 | continue; 46 | } 47 | 48 | deps.push(format!("{} -- {}\n", current_clz, callee)); 49 | } 50 | } 51 | 52 | format!( 53 | "@startuml\n\n{}\n{}\n@enduml", 54 | rendered.join("\n\n"), 55 | deps.join("") 56 | ) 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /_fixtures/ctags/java_tags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ 2 | !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/ 4 | !_TAG_OUTPUT_FILESEP slash /slash or backslash/ 5 | !_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ 6 | !_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/ 7 | !_TAG_PROC_CWD /Users/fdhuang/clone/lombok/src/core/lombok/core/configuration/ // 8 | !_TAG_PROGRAM_AUTHOR Universal Ctags Team // 9 | !_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ 10 | !_TAG_PROGRAM_URL https://ctags.io/ /official site/ 11 | !_TAG_PROGRAM_VERSION 5.9.0 /d532b5c/ 12 | TypeName TypeName.java /^ private TypeName(String name) {$/;" method line:29 language:Java class:TypeName file: access:private 13 | TypeName TypeName.java /^public final class TypeName implements ConfigurationValueType {$/;" class line:26 language:Java inherits:ConfigurationValueType 14 | description TypeName.java /^ public static String description() {$/;" method line:43 language:Java class:TypeName access:public 15 | equals TypeName.java /^ @Override public boolean equals(Object obj) {$/;" method line:51 language:Java class:TypeName access:public 16 | exampleValue TypeName.java /^ public static String exampleValue() {$/;" method line:47 language:Java class:TypeName access:public 17 | getCharArray TypeName.java /^ public char[] getCharArray() {$/;" method line:68 language:Java class:TypeName access:public 18 | getName TypeName.java /^ public String getName() {$/;" method line:64 language:Java class:TypeName access:public 19 | hashCode TypeName.java /^ @Override public int hashCode() {$/;" method line:56 language:Java class:TypeName access:public 20 | lombok.core.configuration TypeName.java /^package lombok.core.configuration;$/;" package line:22 language:Java 21 | name TypeName.java /^ private final String name;$/;" field line:27 language:Java class:TypeName file: access:private 22 | toString TypeName.java /^ @Override public String toString() {$/;" method line:60 language:Java class:TypeName access:public 23 | valueOf TypeName.java /^ public static TypeName valueOf(String name) {$/;" method line:33 language:Java class:TypeName access:public 24 | -------------------------------------------------------------------------------- /src/render/mermaid_render.rs: -------------------------------------------------------------------------------- 1 | use crate::coco_struct::ClassInfo; 2 | use crate::render::{process_name, render_member, render_method}; 3 | use crate::ParseOption; 4 | use std::collections::HashMap; 5 | 6 | /// Render classes info to string 7 | pub struct MermaidRender; 8 | 9 | impl MermaidRender { 10 | pub fn render(classes: &[ClassInfo], parse_option: &ParseOption) -> String { 11 | let space = " "; 12 | let mut rendered: Vec = vec![]; 13 | let mut deps: Vec = vec![]; 14 | 15 | let mut class_map: HashMap = HashMap::default(); 16 | for clazz in classes { 17 | class_map.insert(process_name(&parse_option, &clazz.name), true); 18 | } 19 | 20 | for clazz in classes { 21 | let mut dep_map: HashMap = HashMap::default(); 22 | 23 | let members = render_member(&clazz, &mut dep_map, space, parse_option, &mut class_map); 24 | let mut methods = vec![]; 25 | if !parse_option.field_only { 26 | methods = render_method(&clazz, &mut dep_map, space, parse_option); 27 | } 28 | 29 | let content = format!("{}{}", members.join(""), methods.join("")); 30 | let class_name = process_name(&parse_option, &clazz.name); 31 | if clazz.parents.len() > 0 && !parse_option.without_parent { 32 | rendered.push(format!( 33 | "{}{} <|-- {}", 34 | space, 35 | clazz.parents.join(","), 36 | class_name 37 | )); 38 | } 39 | 40 | rendered.push(format!( 41 | "{}class {} {{\n{}{}}}", 42 | space, class_name, content, space 43 | )); 44 | 45 | for (callee, current_clz) in dep_map { 46 | if callee == current_clz { 47 | continue; 48 | } 49 | 50 | if class_map.get(&callee).is_none() { 51 | continue; 52 | } 53 | 54 | deps.push(format!("{}{} -- {}\n", space, current_clz, callee)); 55 | } 56 | } 57 | 58 | format!("{}\n{}", rendered.join("\n\n"), deps.join("")) 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/bin/visualing.rs: -------------------------------------------------------------------------------- 1 | use std::borrow::Cow; 2 | use std::fs; 3 | 4 | use actix_web::body::Body; 5 | use actix_web::{get, web, App, HttpResponse, HttpServer}; 6 | use mime_guess::from_path; 7 | use rust_embed::RustEmbed; 8 | 9 | #[actix_web::main] 10 | async fn main() -> std::io::Result<()> { 11 | let args: Vec<_> = std::env::args_os().collect(); 12 | 13 | let mut port = "9000"; 14 | if args.len() > 1 { 15 | port = args[1].to_str().unwrap(); 16 | } 17 | 18 | return start_local_server(port).await; 19 | } 20 | 21 | async fn start_local_server(port: &str) -> std::io::Result<()> { 22 | let url = format!("http://127.0.0.1:{}", port); 23 | println!("start server: {}", url); 24 | 25 | open_url(&url); 26 | return start(port).await; 27 | } 28 | 29 | pub async fn start(port: &str) -> std::io::Result<()> { 30 | return HttpServer::new(move || { 31 | App::new() 32 | .service(web::resource("/").route(web::get().to(index))) 33 | .service(data) 34 | .service(web::resource("/{_:.*}").route(web::get().to(dist))) 35 | }) 36 | .bind(format!("127.0.0.1:{}", port))? 37 | .run() 38 | .await; 39 | } 40 | 41 | #[get("/output.json")] 42 | pub fn data() -> HttpResponse { 43 | let content = fs::read_to_string("output.json").unwrap(); 44 | 45 | return HttpResponse::Ok() 46 | .content_type("application/json") 47 | .body(content.into_bytes()); 48 | } 49 | 50 | pub fn open_url(url: &str) { 51 | if let Err(err) = webbrowser::open(url) { 52 | println!("failure to open in browser: {}", err); 53 | } 54 | } 55 | 56 | #[derive(RustEmbed)] 57 | #[folder = "static/"] 58 | struct Asset; 59 | 60 | fn handle_embedded_file(path: &str) -> HttpResponse { 61 | match Asset::get(path) { 62 | Some(content) => { 63 | let body: Body = match content.data { 64 | Cow::Borrowed(bytes) => bytes.into(), 65 | Cow::Owned(bytes) => bytes.into(), 66 | }; 67 | HttpResponse::Ok() 68 | .content_type(from_path(path).first_or_octet_stream().as_ref()) 69 | .body(body) 70 | } 71 | None => HttpResponse::NotFound().body("404 Not Found"), 72 | } 73 | } 74 | 75 | pub fn index() -> HttpResponse { 76 | handle_embedded_file("index.html") 77 | } 78 | 79 | pub fn dist(path: web::Path) -> HttpResponse { 80 | handle_embedded_file(&path.0) 81 | } 82 | -------------------------------------------------------------------------------- /_fixtures/ctags/source/TypeName.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2013-2019 The Project Lombok Authors. 3 | * 4 | * Permission is hereby granted, free of charge, to any person obtaining a copy 5 | * of this software and associated documentation files (the "Software"), to deal 6 | * in the Software without restriction, including without limitation the rights 7 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | * copies of the Software, and to permit persons to whom the Software is 9 | * furnished to do so, subject to the following conditions: 10 | * 11 | * The above copyright notice and this permission notice shall be included in 12 | * all copies or substantial portions of the Software. 13 | * 14 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 19 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 20 | * THE SOFTWARE. 21 | */ 22 | package lombok.core.configuration; 23 | 24 | import lombok.core.JavaIdentifiers; 25 | 26 | public final class TypeName implements ConfigurationValueType { 27 | private final String name; 28 | 29 | private TypeName(String name) { 30 | this.name = name; 31 | } 32 | 33 | public static TypeName valueOf(String name) { 34 | if (name == null || name.trim().isEmpty()) return null; 35 | 36 | String trimmedName = name.trim(); 37 | for (String identifier : trimmedName.split("\\.")) { 38 | if (!JavaIdentifiers.isValidJavaIdentifier(identifier)) throw new IllegalArgumentException("Invalid type name " + trimmedName + " (part " + identifier + ")"); 39 | } 40 | return new TypeName(trimmedName); 41 | } 42 | 43 | public static String description() { 44 | return "type-name"; 45 | } 46 | 47 | public static String exampleValue() { 48 | return ""; 49 | } 50 | 51 | @Override public boolean equals(Object obj) { 52 | if (!(obj instanceof TypeName)) return false; 53 | return name.equals(((TypeName) obj).name); 54 | } 55 | 56 | @Override public int hashCode() { 57 | return name.hashCode(); 58 | } 59 | 60 | @Override public String toString() { 61 | return name; 62 | } 63 | 64 | public String getName() { 65 | return name; 66 | } 67 | 68 | public char[] getCharArray() { 69 | return name.toCharArray(); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /_fixtures/ctags/go_tags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ 2 | !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/ 4 | !_TAG_OUTPUT_FILESEP slash /slash or backslash/ 5 | !_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ 6 | !_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/ 7 | !_TAG_PROC_CWD /Users/fdhuang/consultant/devops/coco/ // 8 | !_TAG_PROGRAM_AUTHOR Universal Ctags Team // 9 | !_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ 10 | !_TAG_PROGRAM_URL https://ctags.io/ /official site/ 11 | !_TAG_PROGRAM_VERSION 5.9.0 /d532b5c/ 12 | InitDatastore pkg/datastore.go /^func InitDatastore() {$/;" func line:37 language:Go package:pkg 13 | access pkg/datastore.go /^ name, access, datatype string$/;" member line:19 language:Go struct:pkg.memberinfo_st typeref:typename:string 14 | access pkg/datastore.go /^ name, access, returntype string$/;" member line:23 language:Go struct:pkg.methodinfo_st typeref:typename:string 15 | classinfo_st pkg/datastore.go /^type classinfo_st struct {$/;" struct line:26 language:Go package:pkg 16 | classmap pkg/datastore.go /^var classmap map[string]classinfo_st$/;" var line:34 language:Go package:pkg typeref:typename:map[string]classinfo_st 17 | datatype pkg/datastore.go /^ name, access, datatype string$/;" member line:19 language:Go struct:pkg.memberinfo_st typeref:typename:string 18 | id pkg/datastore.go /^ id int$/;" member line:28 language:Go struct:pkg.classinfo_st typeref:typename:int 19 | idcounter pkg/datastore.go /^var idcounter int = 1$/;" var line:35 language:Go package:pkg typeref:typename:int 20 | memberinfo_st pkg/datastore.go /^type memberinfo_st struct {$/;" struct line:18 language:Go package:pkg 21 | members pkg/datastore.go /^ members []memberinfo_st$/;" member line:30 language:Go struct:pkg.classinfo_st typeref:typename:[]memberinfo_st 22 | methodinfo_st pkg/datastore.go /^type methodinfo_st struct {$/;" struct line:22 language:Go package:pkg 23 | methods pkg/datastore.go /^ methods []methodinfo_st$/;" member line:31 language:Go struct:pkg.classinfo_st typeref:typename:[]methodinfo_st 24 | name pkg/datastore.go /^ name string$/;" member line:27 language:Go struct:pkg.classinfo_st typeref:typename:string 25 | name pkg/datastore.go /^ name, access, datatype string$/;" member line:19 language:Go struct:pkg.memberinfo_st typeref:typename:string 26 | name pkg/datastore.go /^ name, access, returntype string$/;" member line:23 language:Go struct:pkg.methodinfo_st typeref:typename:string 27 | parents pkg/datastore.go /^ parents []string$/;" member line:29 language:Go struct:pkg.classinfo_st typeref:typename:[]string 28 | pkg pkg/datastore.go /^package pkg$/;" package line:16 language:Go 29 | returntype pkg/datastore.go /^ name, access, returntype string$/;" member line:23 language:Go struct:pkg.methodinfo_st typeref:typename:string 30 | -------------------------------------------------------------------------------- /_fixtures/ctags/coco_class_tags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ 2 | !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/ 4 | !_TAG_OUTPUT_FILESEP slash /slash or backslash/ 5 | !_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ 6 | !_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/ 7 | !_TAG_PROC_CWD coco/ // 8 | !_TAG_PROGRAM_AUTHOR Universal Ctags Team // 9 | !_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ 10 | !_TAG_PROGRAM_URL https://ctags.io/ /official site/ 11 | !_TAG_PROGRAM_VERSION 5.9.0 /d532b5c/ 12 | ClassInfo src/coco_struct.rs /^impl ClassInfo {$/;" implementation line:48 language:Rust 13 | ClassInfo src/coco_struct.rs /^pub struct ClassInfo {$/;" struct line:38 language:Rust 14 | MemberInfo src/coco_struct.rs /^impl MemberInfo {$/;" implementation line:10 language:Rust 15 | MemberInfo src/coco_struct.rs /^pub struct MemberInfo {$/;" struct line:4 language:Rust 16 | MethodInfo src/coco_struct.rs /^impl MethodInfo {$/;" implementation line:27 language:Rust 17 | MethodInfo src/coco_struct.rs /^pub struct MethodInfo {$/;" struct line:21 language:Rust 18 | access src/coco_struct.rs /^ pub access: String,$/;" field line:23 language:Rust struct:MethodInfo 19 | access src/coco_struct.rs /^ pub access: String,$/;" field line:6 language:Rust struct:MemberInfo 20 | data_type src/coco_struct.rs /^ pub data_type: String,$/;" field line:7 language:Rust struct:MemberInfo 21 | file src/coco_struct.rs /^ pub file: String,$/;" field line:41 language:Rust struct:ClassInfo 22 | id src/coco_struct.rs /^ pub id: i32,$/;" field line:40 language:Rust struct:ClassInfo 23 | lang src/coco_struct.rs /^ pub lang: String,$/;" field line:42 language:Rust struct:ClassInfo 24 | members src/coco_struct.rs /^ pub members: Vec,$/;" field line:44 language:Rust struct:ClassInfo 25 | methods src/coco_struct.rs /^ pub methods: Vec,$/;" field line:45 language:Rust struct:ClassInfo 26 | name src/coco_struct.rs /^ pub name: String,$/;" field line:22 language:Rust struct:MethodInfo 27 | name src/coco_struct.rs /^ pub name: String,$/;" field line:39 language:Rust struct:ClassInfo 28 | name src/coco_struct.rs /^ pub name: String,$/;" field line:5 language:Rust struct:MemberInfo 29 | new src/coco_struct.rs /^ pub fn new(class_name: &str) -> Self {$/;" method line:49 language:Rust implementation:ClassInfo 30 | new src/coco_struct.rs /^ pub fn new(name: &str, access: &str, data_type: String) -> Self {$/;" method line:11 language:Rust implementation:MemberInfo 31 | new src/coco_struct.rs /^ pub fn new(name: &str, access: &str, data_type: String) -> Self {$/;" method line:28 language:Rust implementation:MethodInfo 32 | parents src/coco_struct.rs /^ pub parents: Vec,$/;" field line:43 language:Rust struct:ClassInfo 33 | return_type src/coco_struct.rs /^ pub return_type: String,$/;" field line:24 language:Rust struct:MethodInfo 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - '*' 7 | 8 | jobs: 9 | build_windows: 10 | name: Build Windows 11 | runs-on: windows-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | 21 | - name: Build 22 | run: cargo build --release 23 | 24 | - name: Upload modeling to release 25 | uses: svenstaro/upload-release-action@v1-release 26 | with: 27 | repo_token: ${{ secrets.GITHUB_TOKEN }} 28 | file: target/release/modeling.exe 29 | asset_name: modeling-windows.exe 30 | tag: ${{ github.ref }} 31 | 32 | - name: Upload visualing to release 33 | uses: svenstaro/upload-release-action@v1-release 34 | with: 35 | repo_token: ${{ secrets.GITHUB_TOKEN }} 36 | file: target/release/visualing.exe 37 | asset_name: visualing-windows.exe 38 | tag: ${{ github.ref }} 39 | 40 | build_ubuntu: 41 | name: Build Ubuntu 42 | runs-on: ubuntu-latest 43 | 44 | steps: 45 | - uses: actions/checkout@v2 46 | 47 | - uses: actions-rs/toolchain@v1 48 | with: 49 | profile: minimal 50 | toolchain: stable 51 | 52 | - name: Build 53 | run: cargo build --release 54 | 55 | - name: Upload modeling to release 56 | uses: svenstaro/upload-release-action@v1-release 57 | with: 58 | repo_token: ${{ secrets.GITHUB_TOKEN }} 59 | file: target/release/modeling 60 | asset_name: modeling_linux 61 | tag: ${{ github.ref }} 62 | 63 | - name: Upload visualing to release 64 | uses: svenstaro/upload-release-action@v1-release 65 | with: 66 | repo_token: ${{ secrets.GITHUB_TOKEN }} 67 | file: target/release/visualing 68 | asset_name: visualing_linux 69 | tag: ${{ github.ref }} 70 | 71 | build_macos: 72 | name: Build macOS 73 | runs-on: macos-latest 74 | 75 | steps: 76 | - uses: actions/checkout@v2 77 | 78 | - uses: actions-rs/toolchain@v1 79 | with: 80 | profile: minimal 81 | toolchain: stable 82 | 83 | - name: Build 84 | run: cargo build --release 85 | 86 | - name: Upload modeling to release 87 | uses: svenstaro/upload-release-action@v1-release 88 | with: 89 | repo_token: ${{ secrets.GITHUB_TOKEN }} 90 | file: target/release/modeling 91 | asset_name: modeling_macos 92 | tag: ${{ github.ref }} 93 | 94 | - name: Upload visualing to release 95 | uses: svenstaro/upload-release-action@v1-release 96 | with: 97 | repo_token: ${{ secrets.GITHUB_TOKEN }} 98 | file: target/release/visualing 99 | asset_name: visualing_macos 100 | tag: ${{ github.ref }} 101 | -------------------------------------------------------------------------------- /_fixtures/ctags/source/field.cpp: -------------------------------------------------------------------------------- 1 | // based on https://stackoverflow.com/questions/38217459/accessing-list-of-fields-and-types-in-a-class-in-c 2 | #include 3 | #include 4 | 5 | using namespace std; 6 | 7 | //the "Field" interface 8 | class IFieldOrm 9 | { 10 | public: 11 | virtual ~IFieldOrm() {} 12 | 13 | virtual void save() = 0; 14 | virtual void migrate() = 0; 15 | }; 16 | 17 | //your base class 18 | class BaseOrm 19 | { 20 | public: 21 | virtual ~BaseOrm(); 22 | 23 | virtual void save(); 24 | virtual void migrate(); 25 | 26 | protected: 27 | map m_fields; //prefer a smart pointer if you don't want to mess with raw pointer 28 | }; 29 | 30 | //base class implementation 31 | void BaseOrm::save() 32 | { 33 | for(auto& f : m_fields) 34 | f.second->save(); 35 | } 36 | 37 | void BaseOrm::migrate() 38 | { 39 | for(auto& f : m_fields) 40 | f.second->migrate(); 41 | } 42 | 43 | //don't forget to free your "fields" pointers if you have raw pointers 44 | BaseOrm::~BaseOrm() 45 | { 46 | for(auto& f : m_fields) 47 | delete f.second; 48 | } 49 | 50 | 51 | //then implement your basic types 52 | //(like string, int, ..., whatever type you want to store in your database) 53 | class StringFieldOrm : public IFieldOrm 54 | { 55 | public: 56 | StringFieldOrm(const string& value) : m_value(value) {} 57 | 58 | virtual void save(); 59 | virtual void migrate(); 60 | 61 | private: 62 | string m_value; 63 | }; 64 | 65 | void StringFieldOrm::save() 66 | { 67 | cout << "Save value " << m_value << endl; 68 | //save stuff... 69 | } 70 | 71 | void StringFieldOrm::migrate() 72 | { 73 | cout << "Migrate value " << m_value << endl; 74 | //migrate stuff... 75 | } 76 | 77 | class IntFieldOrm : public IFieldOrm 78 | { 79 | public: 80 | IntFieldOrm(int& value) : m_value(value) {} 81 | 82 | virtual void save(); 83 | virtual void migrate(); 84 | 85 | private: 86 | int m_value; 87 | }; 88 | 89 | void IntFieldOrm::save() 90 | { 91 | cout << "Save value " << m_value << endl; 92 | //save stuff... 93 | } 94 | 95 | void IntFieldOrm::migrate() 96 | { 97 | cout << "Migrate value " << m_value << endl; 98 | //migrate stuff 99 | } 100 | 101 | 102 | 103 | //and finally implement your final class 104 | //note that this object can be "dynamically extended" by inserting new fields, 105 | //you may want to prevent that and I can think of a solution if you want to 106 | class UserProfile: public BaseOrm 107 | { 108 | public: 109 | UserProfile(const string& username, const string& email, int age); 110 | }; 111 | 112 | UserProfile::UserProfile(const string& username, const string& email, int age) 113 | { 114 | m_fields["username"] = new StringFieldOrm(username); 115 | m_fields["email"] = new StringFieldOrm(email); 116 | m_fields["age"] = new IntFieldOrm(age); 117 | } 118 | 119 | int main(int argc, char* argv[]) 120 | { 121 | UserProfile user = UserProfile("Batman", "bw@batmail.com", 30); 122 | 123 | user.save(); 124 | 125 | return 0; 126 | } 127 | -------------------------------------------------------------------------------- /_fixtures/ctags/cpp_tags: -------------------------------------------------------------------------------- 1 | !_TAG_FILE_FORMAT 2 /extended format; --format=1 will not append ;" to lines/ 2 | !_TAG_FILE_SORTED 1 /0=unsorted, 1=sorted, 2=foldcase/ 3 | !_TAG_OUTPUT_EXCMD mixed /number, pattern, mixed, or combineV2/ 4 | !_TAG_OUTPUT_FILESEP slash /slash or backslash/ 5 | !_TAG_OUTPUT_MODE u-ctags /u-ctags or e-ctags/ 6 | !_TAG_PATTERN_LENGTH_LIMIT 96 /0 for no limit/ 7 | !_TAG_PROC_CWD /Users/fdhuang/consultant/devops/coco/_fixtures/ctags/source/ // 8 | !_TAG_PROGRAM_AUTHOR Universal Ctags Team // 9 | !_TAG_PROGRAM_NAME Universal Ctags /Derived from Exuberant Ctags/ 10 | !_TAG_PROGRAM_URL https://ctags.io/ /official site/ 11 | !_TAG_PROGRAM_VERSION 5.9.0 /d532b5c/ 12 | BaseOrm field.cpp /^class BaseOrm$/;" class line:18 language:C++ file: 13 | IFieldOrm field.cpp /^class IFieldOrm$/;" class line:8 language:C++ file: 14 | IntFieldOrm field.cpp /^ IntFieldOrm(int& value) : m_value(value) {}$/;" function line:80 language:C++ class:IntFieldOrm file: access:public 15 | IntFieldOrm field.cpp /^class IntFieldOrm : public IFieldOrm$/;" class line:77 language:C++ file: inherits:IFieldOrm 16 | StringFieldOrm field.cpp /^ StringFieldOrm(const string& value) : m_value(value) {}$/;" function line:56 language:C++ class:StringFieldOrm file: access:public 17 | StringFieldOrm field.cpp /^class StringFieldOrm : public IFieldOrm$/;" class line:53 language:C++ file: inherits:IFieldOrm 18 | UserProfile field.cpp /^UserProfile::UserProfile(const string& username, const string& email, int age)$/;" function line:112 language:C++ class:UserProfile 19 | UserProfile field.cpp /^class UserProfile: public BaseOrm$/;" class line:106 language:C++ file: inherits:BaseOrm 20 | m_fields field.cpp /^ map m_fields; \/\/prefer a smart pointer if you don't want to mess with /;" member line:27 language:C++ class:BaseOrm typeref:typename:map file: access:protected 21 | m_value field.cpp /^ int m_value;$/;" member line:86 language:C++ class:IntFieldOrm typeref:typename:int file: access:private 22 | m_value field.cpp /^ string m_value;$/;" member line:62 language:C++ class:StringFieldOrm typeref:typename:string file: access:private 23 | main field.cpp /^int main(int argc, char* argv[])$/;" function line:119 language:C++ typeref:typename:int 24 | migrate field.cpp /^void BaseOrm::migrate()$/;" function line:37 language:C++ class:BaseOrm typeref:typename:void 25 | migrate field.cpp /^void IntFieldOrm::migrate()$/;" function line:95 language:C++ class:IntFieldOrm typeref:typename:void 26 | migrate field.cpp /^void StringFieldOrm::migrate()$/;" function line:71 language:C++ class:StringFieldOrm typeref:typename:void 27 | save field.cpp /^void BaseOrm::save()$/;" function line:31 language:C++ class:BaseOrm typeref:typename:void 28 | save field.cpp /^void IntFieldOrm::save()$/;" function line:89 language:C++ class:IntFieldOrm typeref:typename:void 29 | save field.cpp /^void StringFieldOrm::save()$/;" function line:65 language:C++ class:StringFieldOrm typeref:typename:void 30 | ~BaseOrm field.cpp /^BaseOrm::~BaseOrm()$/;" function line:44 language:C++ class:BaseOrm 31 | ~IFieldOrm field.cpp /^ virtual ~IFieldOrm() {}$/;" function line:11 language:C++ class:IFieldOrm file: access:public 32 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Modeling 2 | 3 | [![crates.io](https://img.shields.io/crates/v/modeling.svg)](https://crates.io/crates/modeling) 4 | [![docs.rs](https://docs.rs/modeling/badge.svg)](https://docs.rs/modeling/) 5 | [![license](https://img.shields.io/crates/l/modeling)](https://github.com/inherd/modeling/blob/master/LICENSE) 6 | 7 | > Modeling is a tools to analysis different languages by Ctags 8 | 9 | process: 10 | 11 | 1. analysis `ctags` 12 | - generate to opt 13 | - call `ctags` with opt 14 | - analysis `ctags` logs by regex 15 | 2. generate results 16 | 3. visual result with `visualing` (optional) 17 | 18 | language support: 19 | 20 | - [x] Java 21 | - [x] C# 22 | - [x] Cpp 23 | - [x] TypeScript 24 | - [x] Golang 25 | - [x] Rust 26 | - ... others by ctags 27 | 28 | ## Usage 29 | 30 | - modeling, generate model from source code. 31 | - concepting, generate concepts from source code. 32 | - visualing, visualization the uml. 33 | 34 | ```bash 35 | Modeling 0.6.2 36 | 37 | USAGE: 38 | modeling [FLAGS] [OPTIONS] 39 | 40 | FLAGS: 41 | -b, --by-modules multiple modules 42 | -d, --debug output debug information 43 | -f, --field-only only load field in methods 44 | -h, --help Prints help information 45 | --inline-id-suffix if class's prop end with Id and class in list, will replace `int` type to `xxClass` 46 | -m, --merge merge for same method name 47 | -V, --version Prints version information 48 | --without-impl-suffix if class's prop start with `IRepository` will become `Repository` 49 | --without-parent without class inheritance 50 | 51 | OPTIONS: 52 | -g, --grep by grep regex rules: for example: `.*Service` [default: ] 53 | -i, --input input dir [default: .] 54 | -o, --output-type support: puml, mermaid, graphviz with json [default: puml] 55 | -p, --packages ... filter by packages, like: `com.phodal.modeling` 56 | -s, --suffixes ... filter by suffixes, like: `java` for .java file 57 | --without-suffix remove specify suffix by text, for example `DemoDto` with be `Demo` [default: ] 58 | ``` 59 | 60 | ### sample: Grep with MVC 61 | 62 | ```bash 63 | modeling --input=/youpath/ --field-only --without-parent --grep ".*Service|.*Controller|.*Repository" 64 | ``` 65 | 66 | ### sample: with Graphviz and Visualization 67 | 68 | with `--output-type=graphviz` 69 | 70 | ```bash 71 | modeling --input=/youpath --field-only -o graphviz --without-impl-suffix 72 | ``` 73 | 74 | ### sample: puml to Image 75 | 76 | convert to image: `plantuml modeling.puml modeling.svg -tsvg` 77 | 78 | ### with Visualization 79 | 80 | PS: need to set `--output-type graphviz`, in order to generate `output.json` file 81 | 82 | ```bash 83 | modeling -i youpath -o graphviz 84 | visualing 85 | ``` 86 | 87 | ## Library 88 | 89 | ``` 90 | cargo install modeling 91 | modeling . 92 | ``` 93 | 94 | #### Library 95 | 96 | ```rust 97 | use modeling::{by_dir}; 98 | use modeling::render::PlantUmlRender; 99 | 100 | let classes = by_dir("src/"); 101 | let puml = PlantUmlRender::render(&classes); 102 | ``` 103 | 104 | output sample: 105 | 106 | ```puml 107 | @startuml 108 | 109 | class Animal { 110 | + string name 111 | + string constructor() 112 | +move() 113 | } 114 | 115 | class Horse extends Animal { 116 | +move() 117 | } 118 | 119 | class Snake extends Animal { 120 | +move() 121 | } 122 | 123 | @enduml 124 | ``` 125 | 126 | License 127 | --- 128 | 129 | ctags analysis based on [https://github.com/dalance/ptags](https://github.com/dalance/ptags) with MIT, see in [src](plugins/coco_struct_analysis/src) 130 | 131 | ctags parser rewrite from Golang's [https://github.com/ruben2020/tags2uml](https://github.com/ruben2020/tags2uml) with Apache License. 132 | 133 | @ 2020~2021 This code is distributed under the MIT license. See `LICENSE` in this directory. 134 | -------------------------------------------------------------------------------- /src/bin/concepting.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate prettytable; 3 | 4 | use log::info; 5 | use std::collections::HashMap; 6 | use std::fs::File; 7 | use std::path::{Path, PathBuf}; 8 | 9 | use prettytable::{format, row, Table}; 10 | use structopt::StructOpt; 11 | 12 | use modeling::file_filter::FileFilter; 13 | use modeling::segment::segment; 14 | use modeling::segment::stop_words::{STOP_WORDS, TECH_STOP_WORDS}; 15 | use modeling::{by_dir, ClassInfo, ParseOption}; 16 | 17 | #[derive(StructOpt, Debug, PartialEq, Clone)] 18 | #[structopt(name = "basic")] 19 | struct ConceptOpts { 20 | /// input dir 21 | #[structopt(short, long, default_value = ".")] 22 | input: String, 23 | /// filter by packages, like: `com.phodal.modeling` 24 | #[structopt(long, short, use_delimiter = true)] 25 | packages: Vec, 26 | /// filter by suffixes, like: `java` for .java file 27 | #[structopt(long, short, use_delimiter = true)] 28 | suffixes: Vec, 29 | /// by grep regex rules: for example: `.*Service` 30 | #[structopt(short, long, default_value = "")] 31 | grep: String, 32 | } 33 | 34 | impl ConceptOpts { 35 | pub fn to_parse_option(&self) -> ParseOption { 36 | ParseOption { 37 | merge_method_name: false, 38 | field_only: false, 39 | without_parent: false, 40 | without_impl_suffix: false, 41 | inline_id_suffix: false, 42 | without_suffix: "".to_string() 43 | } 44 | } 45 | } 46 | 47 | fn main() { 48 | env_logger::init(); 49 | let opts: ConceptOpts = ConceptOpts::from_args(); 50 | 51 | info!("parse input {:?} with {:?}", &opts.input, &opts); 52 | 53 | let parse_option = opts.to_parse_option(); 54 | let filter = FileFilter::new( 55 | opts.packages.clone(), 56 | opts.suffixes.clone(), 57 | opts.grep.clone(), 58 | ); 59 | 60 | output_by_dir(&parse_option, &filter, &PathBuf::from(&opts.input)); 61 | } 62 | 63 | fn output_by_dir(parse_option: &ParseOption, filter: &FileFilter, dir: &Path) { 64 | let classes = by_dir(dir, filter.clone(), parse_option); 65 | let (word, text) = class_to_identify_map(&classes); 66 | 67 | map_to_csv(word, "output_word.csv"); 68 | map_to_csv(text, "output_text.csv"); 69 | } 70 | 71 | fn map_to_csv(word: HashMap, path: &str) { 72 | let mut hash_vec: Vec<(&String, &u32)> = word.iter().collect(); 73 | hash_vec.sort_by(|a, b| b.1.cmp(a.1)); 74 | 75 | let mut table = Table::new(); 76 | table.set_format(*format::consts::FORMAT_NO_LINESEP_WITH_TITLE); 77 | 78 | for (key, value) in hash_vec { 79 | table.add_row(row![key, value.to_string()]); 80 | } 81 | 82 | let out = File::create(path).unwrap(); 83 | table.to_csv(out).unwrap(); 84 | } 85 | 86 | fn class_to_identify_map(classes: &Vec) -> (HashMap, HashMap) { 87 | let mut by_word: HashMap = HashMap::default(); 88 | let mut by_text: HashMap = HashMap::default(); 89 | info!("class counts: {:?}", &classes.len()); 90 | 91 | let mut methods_counts = 0; 92 | for class in classes { 93 | count_words(&mut by_word, &class.name); 94 | count_text(&mut by_text, &class.name); 95 | 96 | methods_counts = methods_counts + class.methods.len(); 97 | for method in &class.methods { 98 | count_words(&mut by_word, &method.name); 99 | count_text(&mut by_text, &method.name); 100 | } 101 | 102 | for member in &class.members { 103 | count_words(&mut by_word, &member.name); 104 | count_text(&mut by_text, &member.name); 105 | } 106 | } 107 | 108 | info!("methods counts: {:?}", methods_counts); 109 | (by_word, by_text) 110 | } 111 | 112 | fn count_text(map: &mut HashMap, var: &str) { 113 | let counter = map.entry(var.to_string()).or_insert(0); 114 | *counter += 1; 115 | } 116 | 117 | fn count_words(map: &mut HashMap, var: &str) { 118 | for word in segment(var) { 119 | if STOP_WORDS.contains(&&**&word.to_lowercase()) { 120 | continue; 121 | } 122 | 123 | if TECH_STOP_WORDS.contains(&&**&word.to_lowercase()) { 124 | continue; 125 | } 126 | 127 | let counter = map.entry(word).or_insert(0); 128 | *counter += 1; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/ctags/ctags_opt.rs: -------------------------------------------------------------------------------- 1 | /// MIT License 2 | // 3 | // Copyright (c) 2018 dalance 4 | // 5 | // Permission is hereby granted, free of charge, to any person obtaining a copy 6 | // of this software and associated documentation files (the "Software"), to deal 7 | // in the Software without restriction, including without limitation the rights 8 | // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | // copies of the Software, and to permit persons to whom the Software is 10 | // furnished to do so, subject to the following conditions: 11 | // 12 | // The above copyright notice and this permission notice shall be included in all 13 | // copies or substantial portions of the Software. 14 | // 15 | // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | // SOFTWARE. 22 | // https://github.com/dalance/ptags 23 | use serde_derive::{Deserialize, Serialize}; 24 | use std::path::PathBuf; 25 | use structopt::{clap, StructOpt}; 26 | use structopt_toml::StructOptToml; 27 | 28 | #[derive(Debug, Deserialize, Serialize, StructOpt, StructOptToml)] 29 | #[serde(default)] 30 | #[structopt(name = "ptags")] 31 | #[structopt(long_version = option_env!("LONG_VERSION").unwrap_or(env!("CARGO_PKG_VERSION")))] 32 | #[structopt(setting = clap::AppSettings::AllowLeadingHyphen)] 33 | #[structopt(setting = clap::AppSettings::ColoredHelp)] 34 | pub struct Opt { 35 | /// Number of threads 36 | #[structopt(short = "t", long = "thread", default_value = "8")] 37 | pub thread: usize, 38 | 39 | /// Output filename ( filename '-' means output to stdout ) 40 | #[structopt(short = "f", long = "file", default_value = "tags", parse(from_os_str))] 41 | pub output: PathBuf, 42 | 43 | /// Search directory 44 | #[structopt(name = "DIR", default_value = ".", parse(from_os_str))] 45 | pub dir: PathBuf, 46 | 47 | /// Show statistics 48 | #[structopt(short = "s", long = "stat")] 49 | pub stat: bool, 50 | 51 | /// Filename of input file list 52 | #[structopt(short = "L", long = "list")] 53 | pub list: Option, 54 | 55 | /// Path to ctags binary 56 | #[structopt(long = "bin-ctags", default_value = "ctags", parse(from_os_str))] 57 | pub bin_ctags: PathBuf, 58 | 59 | /// Path to git binary 60 | #[structopt(long = "bin-git", default_value = "git", parse(from_os_str))] 61 | pub bin_git: PathBuf, 62 | 63 | /// Options passed to ctags 64 | #[structopt(short = "c", long = "opt-ctags", number_of_values = 1)] 65 | pub opt_ctags: Vec, 66 | 67 | /// Options passed to git 68 | #[structopt(short = "g", long = "opt-git", number_of_values = 1)] 69 | pub opt_git: Vec, 70 | 71 | /// Options passed to git-lfs 72 | #[structopt(long = "opt-git-lfs", number_of_values = 1)] 73 | pub opt_git_lfs: Vec, 74 | 75 | /// Verbose mode 76 | #[structopt(short = "v", long = "verbose")] 77 | pub verbose: bool, 78 | 79 | /// Exclude git-lfs tracked files 80 | #[structopt(long = "exclude-lfs")] 81 | pub exclude_lfs: bool, 82 | 83 | /// Include untracked files 84 | #[structopt(long = "include-untracked")] 85 | pub include_untracked: bool, 86 | 87 | /// Include ignored files 88 | #[structopt(long = "include-ignored")] 89 | pub include_ignored: bool, 90 | 91 | /// Include submodule files 92 | #[structopt(long = "include-submodule")] 93 | pub include_submodule: bool, 94 | 95 | /// Validate UTF8 sequence of tag file 96 | #[structopt(long = "validate-utf8")] 97 | pub validate_utf8: bool, 98 | 99 | /// Disable tags sort 100 | #[structopt(long = "unsorted")] 101 | pub unsorted: bool, 102 | 103 | /// Disable tags sort 104 | #[structopt(long = "fields")] 105 | pub fields: Option, 106 | 107 | /// Languages 108 | #[structopt(long = "languages")] 109 | pub languages: Option, 110 | 111 | /// Glob pattern of exclude file ( ex. --exclude '*.rs' ) 112 | #[structopt(short = "e", long = "exclude", number_of_values = 1)] 113 | pub exclude: Vec, 114 | 115 | /// Generate shell completion file 116 | #[structopt( 117 | long = "completion", 118 | possible_values = &["bash", "fish", "zsh", "powershell"] 119 | )] 120 | pub completion: Option, 121 | 122 | /// Generate configuration sample file 123 | #[structopt(long = "config")] 124 | pub config: bool, 125 | } 126 | -------------------------------------------------------------------------------- /src/bin/modeling.rs: -------------------------------------------------------------------------------- 1 | use std::fs; 2 | 3 | use ignore::{DirEntry, WalkBuilder}; 4 | use structopt::StructOpt; 5 | 6 | use modeling::file_filter::FileFilter; 7 | use modeling::render::graphviz_render::GraphvizRender; 8 | use modeling::render::{MermaidRender, PlantUmlRender}; 9 | use modeling::{by_dir, ClassInfo, ParseOption}; 10 | use std::ffi::OsStr; 11 | 12 | #[derive(StructOpt, Debug, PartialEq, Clone)] 13 | #[structopt(name = "Modeling")] 14 | struct Opts { 15 | /// output debug information 16 | #[structopt(short, long)] 17 | debug: bool, 18 | /// merge for same method name 19 | #[structopt(short, long)] 20 | merge: bool, 21 | /// multiple modules 22 | #[structopt(short, long)] 23 | by_modules: bool, 24 | /// input dir 25 | #[structopt(short, long, default_value = ".")] 26 | input: String, 27 | /// support: puml, mermaid, graphviz with json 28 | #[structopt(short, long, default_value = "puml")] 29 | output_type: String, 30 | /// filter by packages, like: `com.phodal.modeling` 31 | #[structopt(long, short, use_delimiter = true)] 32 | packages: Vec, 33 | /// filter by suffixes, like: `java` for .java file 34 | #[structopt(long, short, use_delimiter = true)] 35 | suffixes: Vec, 36 | /// only load field in methods 37 | #[structopt(short, long)] 38 | field_only: bool, 39 | /// by grep regex rules: for example: `.*Service` 40 | #[structopt(short, long, default_value = "")] 41 | grep: String, 42 | /// without class inheritance 43 | #[structopt(long)] 44 | without_parent: bool, 45 | /// if class's prop start with `IRepository` will become `Repository` 46 | #[structopt(long)] 47 | without_impl_suffix: bool, 48 | /// if class's prop end with Id and class in list, will replace `int` type to `xxClass` 49 | #[structopt(long)] 50 | inline_id_suffix: bool, 51 | /// remove specify suffix by text, for example `DemoDto` with be `Demo` 52 | #[structopt(long, default_value = "")] 53 | without_suffix: String 54 | } 55 | 56 | impl Opts { 57 | pub fn to_parse_option(&self) -> ParseOption { 58 | ParseOption { 59 | merge_method_name: self.merge, 60 | field_only: self.field_only, 61 | without_parent: self.without_parent, 62 | without_impl_suffix: self.without_impl_suffix, 63 | inline_id_suffix: self.inline_id_suffix, 64 | without_suffix: self.without_suffix.clone(), 65 | } 66 | } 67 | } 68 | 69 | fn main() { 70 | let opts: Opts = Opts::from_args(); 71 | 72 | let parse_option = opts.to_parse_option(); 73 | let filter = FileFilter::new( 74 | opts.packages.clone(), 75 | opts.suffixes.clone(), 76 | opts.grep.clone(), 77 | ); 78 | 79 | if !opts.by_modules { 80 | output_all_in_one(opts, &parse_option, filter); 81 | return; 82 | } 83 | 84 | for result in WalkBuilder::new(&opts.input).max_depth(Some(1)).build() { 85 | if let Ok(dir) = result { 86 | let path = dir.path(); 87 | if path.is_dir() { 88 | if let Some(x) = path.file_name() { 89 | output_by_dir(&opts, &parse_option, &filter, &dir, x) 90 | }; 91 | } 92 | } 93 | } 94 | } 95 | 96 | fn output_by_dir( 97 | opts: &Opts, 98 | parse_option: &ParseOption, 99 | filter: &FileFilter, 100 | dir: &DirEntry, 101 | x: &OsStr, 102 | ) { 103 | let dir_name = x.to_str().unwrap(); 104 | let classes = by_dir(dir.path(), filter.clone(), parse_option); 105 | if classes.len() > 0 { 106 | output_file(&opts, &classes, dir_name) 107 | } 108 | } 109 | 110 | fn output_all_in_one(opts: Opts, parse_option: &ParseOption, filter: FileFilter) { 111 | let classes = by_dir(&opts.input, filter, parse_option); 112 | 113 | if opts.debug { 114 | let _ = fs::write("debug.json", serde_json::to_string(&classes).unwrap()); 115 | } 116 | 117 | output_file(&opts, &classes, "modeling"); 118 | } 119 | 120 | fn output_file(opts: &Opts, classes: &Vec, name: &str) { 121 | let parse_option = opts.to_parse_option(); 122 | match opts.output_type.as_str() { 123 | "mermaid" => { 124 | let uml = MermaidRender::render(&classes, &parse_option); 125 | let file_name = format!("{}.mermaid", name); 126 | let _ = fs::write(file_name, uml); 127 | } 128 | "graphviz" => { 129 | let graph = GraphvizRender::render(&classes, &parse_option); 130 | let file_name = format!("{}.dot", name); 131 | let _ = fs::write(file_name, graph); 132 | } 133 | &_ => { 134 | let uml = PlantUmlRender::render(&classes, &parse_option); 135 | let file_name = format!("{}.puml", name); 136 | let _ = fs::write(file_name, uml); 137 | } 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/file_filter.rs: -------------------------------------------------------------------------------- 1 | use grep_regex::RegexMatcher; 2 | use grep_searcher::sinks::UTF8; 3 | use grep_searcher::Searcher; 4 | use std::path::PathBuf; 5 | 6 | use serde::{Deserialize, Serialize}; 7 | 8 | #[derive(Serialize, Deserialize, Debug, Clone)] 9 | pub struct FileFilter { 10 | grep: String, 11 | packages: Vec, 12 | suffixes: Vec, 13 | } 14 | 15 | impl Default for FileFilter { 16 | fn default() -> Self { 17 | FileFilter { 18 | grep: "".to_string(), 19 | packages: vec![], 20 | suffixes: vec![], 21 | } 22 | } 23 | } 24 | 25 | impl FileFilter { 26 | pub fn new(packages: Vec, suffixes: Vec, string: String) -> FileFilter { 27 | FileFilter { 28 | grep: string, 29 | packages, 30 | suffixes, 31 | } 32 | } 33 | 34 | pub fn allow(&self, path: PathBuf) -> bool { 35 | if self.grep.len() > 0 { 36 | return match RegexMatcher::new(&self.grep) { 37 | Ok(matcher) => grep_by_text(&matcher, &format!("{:}", path.display())), 38 | Err(err) => { 39 | println!("error: {:?}", err); 40 | false 41 | } 42 | }; 43 | } 44 | 45 | if self.packages.len() == 0 && self.suffixes.len() == 0 { 46 | return true; 47 | } 48 | 49 | if self.packages.len() > 0 { 50 | return filter_by_packages(path, &self.packages); 51 | } 52 | 53 | if self.suffixes.len() > 0 { 54 | return filter_by_suffix(path, &self.suffixes); 55 | } 56 | 57 | return false; 58 | } 59 | } 60 | 61 | pub fn grep_by_text(matcher: &RegexMatcher, text: &str) -> bool { 62 | let from = text.as_bytes(); 63 | let mut searcher = Searcher::new(); 64 | 65 | let mut has_match = false; 66 | let _ = searcher.search_reader( 67 | matcher, 68 | from, 69 | UTF8(|_, _| { 70 | has_match = true; 71 | Ok(true) 72 | }), 73 | ); 74 | 75 | has_match 76 | } 77 | 78 | pub fn filter_by_packages(path: PathBuf, packages: &Vec) -> bool { 79 | if packages.len() == 0 { 80 | return true; 81 | } 82 | 83 | let mut include_package = false; 84 | for child in path.iter() { 85 | if let Some(sub) = child.to_str() { 86 | if packages.contains(&sub.to_string()) { 87 | include_package = true; 88 | } 89 | } 90 | } 91 | 92 | return include_package; 93 | } 94 | 95 | pub fn filter_by_suffix(path: PathBuf, suffixes: &Vec) -> bool { 96 | if suffixes.len() == 0 { 97 | return true; 98 | } 99 | 100 | if let None = path.file_name() { 101 | return false; 102 | } 103 | 104 | let file_name = path.file_name().unwrap().to_str().unwrap(); 105 | 106 | for suffix in suffixes.iter() { 107 | if file_name.contains(suffix) { 108 | return true; 109 | } 110 | } 111 | 112 | return false; 113 | } 114 | 115 | #[cfg(test)] 116 | mod tests { 117 | use crate::file_filter::{filter_by_packages, filter_by_suffix}; 118 | use std::path::PathBuf; 119 | 120 | #[test] 121 | fn should_filter_by_file_name_suffix() { 122 | let buf = PathBuf::new().join("model").join("CustomModel.java"); 123 | let suffixes = vec!["Model".to_string()]; 124 | 125 | assert!(filter_by_suffix(buf, &suffixes)); 126 | } 127 | 128 | #[test] 129 | fn should_return_false_when_not_correct_name() { 130 | let buf = PathBuf::new() 131 | .join("controller") 132 | .join("CustomController.java"); 133 | let suffixes = vec!["Model".to_string()]; 134 | 135 | assert_eq!(false, filter_by_suffix(buf, &suffixes)); 136 | } 137 | 138 | #[test] 139 | fn should_no_filter_for_empty_suffix() { 140 | let buf = PathBuf::new() 141 | .join("controller") 142 | .join("CustomController.java"); 143 | let suffixes: Vec = vec![]; 144 | 145 | assert_eq!(true, filter_by_suffix(buf, &suffixes)); 146 | } 147 | 148 | #[test] 149 | fn should_filter_by_package() { 150 | let buf = PathBuf::new().join("model").join("CustomModel.java"); 151 | 152 | let suffixes = vec!["model".to_string()]; 153 | 154 | assert!(filter_by_packages(buf, &suffixes)); 155 | } 156 | 157 | #[test] 158 | fn should_return_no_when_no_in_dir() { 159 | let buf = PathBuf::new().join("model").join("CustomModel.java"); 160 | 161 | let suffixes = vec!["controller".to_string()]; 162 | 163 | assert_eq!(false, filter_by_packages(buf, &suffixes)); 164 | } 165 | 166 | #[test] 167 | fn should_no_filter_for_empty_package() { 168 | let buf = PathBuf::new().join("model").join("CustomModel.java"); 169 | 170 | let suffixes: Vec = vec![]; 171 | 172 | assert!(filter_by_packages(buf, &suffixes)); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /src/render/graphviz_render.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::fs; 3 | 4 | use serde::{Deserialize, Serialize}; 5 | 6 | use crate::coco_struct::ClassInfo; 7 | use crate::render::{process_name, render_member, render_method}; 8 | use crate::ParseOption; 9 | 10 | /// Render classes info to string 11 | pub struct GraphvizRender; 12 | 13 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 14 | pub struct DData { 15 | nodes: Vec, 16 | links: Vec, 17 | } 18 | 19 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 20 | pub struct DNode { 21 | id: String, 22 | package: String, 23 | group: usize, 24 | } 25 | 26 | #[derive(Serialize, Deserialize, Debug, Clone, Default)] 27 | pub struct DLink { 28 | source: String, 29 | target: String, 30 | value: usize, 31 | pub package: String, 32 | } 33 | 34 | impl GraphvizRender { 35 | pub fn render(classes: &Vec, parse_option: &ParseOption) -> String { 36 | let mut sub_graphs_map: HashMap> = HashMap::default(); 37 | let mut deps: Vec = vec![]; 38 | let mut data = DData::default(); 39 | 40 | let mut class_map: HashMap = HashMap::default(); 41 | for clazz in classes { 42 | class_map.insert(process_name(&parse_option, &clazz.name), true); 43 | } 44 | 45 | let class_catalog = Self::catalog_mvc_to_index(); 46 | let layer_cluster = Self::index_to_mvc_cluster(); 47 | 48 | for clazz in classes { 49 | let mut dep_map: HashMap = HashMap::default(); 50 | 51 | Self::create_data_nodes( 52 | &mut sub_graphs_map, 53 | &mut data, 54 | &class_catalog, 55 | &layer_cluster, 56 | &clazz, 57 | &parse_option 58 | ); 59 | 60 | let _ = render_member(&clazz, &mut dep_map, "", parse_option, &mut class_map); 61 | if !parse_option.field_only { 62 | let _ = render_method(&clazz, &mut dep_map, "", parse_option); 63 | } 64 | 65 | for (callee, current_clz) in dep_map { 66 | if callee == current_clz { 67 | continue; 68 | } 69 | 70 | if class_map.get(&callee).is_none() { 71 | continue; 72 | } 73 | 74 | // for service -> repository 75 | if current_clz.ends_with("Service") && callee.ends_with("Repository") { 76 | deps.push(format!("{} -> {} [color=\"red\"] \n", current_clz, callee)); 77 | } else { 78 | deps.push(format!("{} -> {}\n", current_clz, callee)); 79 | } 80 | 81 | data.links.push(DLink { 82 | source: current_clz, 83 | target: callee, 84 | package: clazz.package.clone(), 85 | value: 1, 86 | }) 87 | } 88 | } 89 | 90 | let mut sub_graphs = vec![]; 91 | for (key, items) in sub_graphs_map { 92 | sub_graphs.push(format!( 93 | "\n subgraph cluster_{}{{\n {}\n }}", 94 | key.to_lowercase(), 95 | items.join("\n ") 96 | )); 97 | } 98 | 99 | let _ = fs::write("output.json", serde_json::to_string(&data).unwrap()); 100 | 101 | format!( 102 | "digraph G {{ 103 | compound=true; 104 | ranksep=1 105 | node[shape=record] 106 | {}\n{}\n}}", 107 | sub_graphs.join("\n"), 108 | deps.join("") 109 | ) 110 | } 111 | 112 | fn create_data_nodes( 113 | sub_graphs_map: &mut HashMap>, 114 | data: &mut DData, 115 | class_catalog: &HashMap<&str, usize>, 116 | layer_cluster: &HashMap, 117 | clazz: &ClassInfo, 118 | parse_option: &ParseOption, 119 | ) { 120 | let mut has_catalog = false; 121 | let class_name = process_name(&parse_option, &clazz.name); 122 | for (key, value) in class_catalog { 123 | if class_name.ends_with(key) { 124 | has_catalog = true; 125 | let layer_name = layer_cluster.get(value).unwrap().to_string(); 126 | let graph = sub_graphs_map.entry(layer_name).or_insert(vec![]); 127 | graph.push(class_name.to_string()); 128 | 129 | data.nodes.push(DNode { 130 | id: class_name.to_string(), 131 | package: clazz.package.to_string(), 132 | group: *value, 133 | }) 134 | } 135 | } 136 | 137 | if !has_catalog { 138 | data.nodes.push(DNode { 139 | id: class_name.to_string(), 140 | package: clazz.package.to_string(), 141 | group: 4, 142 | }) 143 | } 144 | } 145 | 146 | fn catalog_mvc_to_index() -> HashMap<&'static str, usize> { 147 | let mut class_catalog: HashMap<&str, usize> = HashMap::new(); 148 | class_catalog.insert("Repository", 1); 149 | class_catalog.insert("Controller", 2); 150 | class_catalog.insert("Ctrl", 2); 151 | class_catalog.insert("Service", 3); 152 | class_catalog.insert("ServiceImpl", 3); 153 | class_catalog 154 | } 155 | 156 | fn index_to_mvc_cluster() -> HashMap { 157 | let mut map_names: HashMap = HashMap::new(); 158 | map_names.insert(1, "Repository"); 159 | map_names.insert(2, "Controller"); 160 | map_names.insert(3, "Service"); 161 | map_names 162 | } 163 | } 164 | 165 | #[cfg(test)] 166 | mod tests { 167 | use crate::render::graphviz_render::GraphvizRender; 168 | use crate::{ClassInfo, ParseOption}; 169 | 170 | #[test] 171 | fn should_render_graphviz() { 172 | let info = ClassInfo::new("WorldServiceImpl"); 173 | let clzs = vec![info]; 174 | let string = GraphvizRender::render(&clzs, &ParseOption::default()); 175 | assert_eq!("digraph G {\n compound=true;\n ranksep=1\n node[shape=record]\n\n subgraph cluster_service{\n WorldServiceImpl\n }\n\n}", string); 176 | } 177 | } 178 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | ## [unreleased] 5 | 6 | ### Documentation 7 | 8 | - Update README 9 | 10 | ## [0.6.2] - 2022-03-15 11 | 12 | ### Bug Fixes 13 | 14 | - Fix some lint for ctags parser for clippy 15 | - Fix lint for falttern items 16 | 17 | ### Documentation 18 | 19 | - Add changelog 20 | - Update modeling readme 21 | - Update badge name 22 | - Init doxygen config 23 | - Update doxygen config 24 | - Update process with usage 25 | 26 | ### Features 27 | 28 | - Add remove custom suffix support 29 | 30 | ### Miscellaneous Tasks 31 | 32 | - Release 0.6.2 33 | 34 | ### Refactor 35 | 36 | - Inline code 37 | 38 | ### Styling 39 | 40 | - Fix import order 41 | 42 | ### Lint 43 | 44 | - Auto fix 45 | - Fix some lints 46 | 47 | ## [0.6.1] - 2022-01-31 48 | 49 | ### Bug Fixes 50 | 51 | - Fix tests 52 | - Fix typos for rust 53 | - Fix structs issue 54 | - Fix typos 55 | 56 | ### Documentation 57 | 58 | - Add sample for graphviz 59 | - Try to debug for rust 60 | - Add docs for modeling 61 | - Add some alias 62 | - Update README 63 | 64 | ### Features 65 | 66 | - Add inline suffix id support 67 | - Fix package for cycle 68 | - Add acess support 69 | - Add type for rust fields 70 | - Add line no support 71 | - Add debug for output josn 72 | - Add match for datatype 73 | - Add visual sever 74 | 75 | ### Refactor 76 | 77 | - Rename parameter from remove to without for impl suffix 78 | - Fix parameter issue 79 | - Extract class name 80 | - Rename parameter 81 | - Change mvc naming 82 | - Extract mapping method 83 | - Extract create nodes method 84 | 85 | ### Testing 86 | 87 | - Add test for graphvviz render service 88 | - Fix test 89 | 90 | ### Build 91 | 92 | - Test articfacts 93 | - Update release package 94 | - Revert job of upload 95 | - Update release config 96 | 97 | ## [0.6.0] - 2022-01-17 98 | 99 | ### Bug Fixes 100 | 101 | - Fix tests 102 | - Fix render issue 103 | 104 | ### Documentation 105 | 106 | - Add sample for repository 107 | - Add concept docu 108 | - Add readme for content 109 | - Update usage 110 | - Update usage 111 | 112 | ### Features 113 | 114 | - Add without parent parameter 115 | - Add remove impl support 116 | - Init graphviz render 117 | - Add first version visual 118 | 119 | ### Miscellaneous Tasks 120 | 121 | - Release 0.6.0 122 | 123 | ### Refactor 124 | 125 | - Add package to clazz 126 | - Move code to data 127 | - Fix impl issues 128 | 129 | ## [0.5.0] - 2022-01-15 130 | 131 | ### Bug Fixes 132 | 133 | - Fix impl issue 134 | - Fix parameter issue again for option 135 | - Fix by modeling issue 136 | 137 | ### Documentation 138 | 139 | - Refactor: remove counts 140 | - Try to add log 141 | 142 | ### Features 143 | 144 | - Add grep support 145 | - Make concets works 146 | - Make concepting in beauty 147 | - Init camelcase tok 148 | - Make camelcael tok work 149 | - Init stopwords 150 | 151 | ### Miscellaneous Tasks 152 | 153 | - Release 0.5.0 154 | 155 | ### Refactor 156 | 157 | - Move code from test to production 158 | - Extract semgnet method 159 | - Remove unused projects 160 | - Extract counter method 161 | - Split words 162 | - Extract more maps 163 | 164 | ## [0.4.0] - 2022-01-13 165 | 166 | ### Documentation 167 | 168 | - Update todo 169 | - Add README 170 | 171 | ### Features 172 | 173 | - Init sqling 174 | - Init diffing cmd 175 | 176 | ### Miscellaneous Tasks 177 | 178 | - Release 0.4.0 179 | 180 | ### Refactor 181 | 182 | - Testing query 183 | - Fix only issue for texts 184 | - Fix parse option clone issue 185 | - Add field only for parse 186 | 187 | ### Build 188 | 189 | - Init sqling 190 | - Init domaining 191 | - Init jieba 192 | - Update ignore 193 | 194 | ## [0.3.0] - 2021-06-10 195 | 196 | ### Documentation 197 | 198 | - Update todos 199 | 200 | ### Features 201 | 202 | - Make by modules support 203 | 204 | ### Miscellaneous Tasks 205 | 206 | - Release 0.3.0 207 | 208 | ### Refactor 209 | 210 | - Extract all in one 211 | 212 | ## [0.2.1] - 2021-06-10 213 | 214 | ### Documentation 215 | 216 | - Thinking in merge options 217 | 218 | ### Features 219 | 220 | - Try to use method options 221 | 222 | ### Miscellaneous Tasks 223 | 224 | - Release 0.2.1 225 | 226 | ### Refactor 227 | 228 | - Change to structopt 229 | - Add for parse option 230 | 231 | ### Testing 232 | 233 | - Fix option issue 234 | 235 | ## [0.2.0] - 2021-06-10 236 | 237 | ### Bug Fixes 238 | 239 | - Fix url typos 240 | 241 | ### Features 242 | 243 | - Add filter by suffix func 244 | - Add ignore cases 245 | - Add filter for empty 246 | - Add filter for emptys 247 | - Add parser for delimiter 248 | - Enable filter to cli 249 | - Make include works 250 | 251 | ### Miscellaneous Tasks 252 | 253 | - Release 0.2.0 254 | 255 | ### Refactor 256 | 257 | - Use clap to replace origin node 258 | 259 | ### Build 260 | 261 | - Create file filter design 262 | 263 | ## [0.1.7] - 2021-05-07 264 | 265 | ### Bug Fixes 266 | 267 | - Fix typo 268 | 269 | ### Miscellaneous Tasks 270 | 271 | - Release 0.1.7 272 | 273 | ### Ci 274 | 275 | - Fix fix assets duplicate issue 276 | 277 | ## [0.1.6] - 2021-05-07 278 | 279 | ### Bug Fixes 280 | 281 | - Remove unused lock 282 | - Fix name typo 283 | 284 | ### Miscellaneous Tasks 285 | 286 | - Release 0.1.6 287 | 288 | ## [0.1.5] - 2021-05-06 289 | 290 | ### Features 291 | 292 | - Create api for mermaid 293 | - Add parameter for render modling 294 | - Support for space 295 | 296 | ### Miscellaneous Tasks 297 | 298 | - Release 0.1.5 299 | 300 | ### Refactor 301 | 302 | - Use same model for mermaid & puml 303 | 304 | ### Testing 305 | 306 | - Fix test issue 307 | 308 | ### Build 309 | 310 | - Update workflow 311 | - Fix no ctags issue 312 | - Fix typos 313 | 314 | ## [0.1.4] - 2021-05-02 315 | 316 | ### Documentation 317 | 318 | - Fix docs issues 319 | 320 | ### Features 321 | 322 | - Use args for input 323 | 324 | ### Miscellaneous Tasks 325 | 326 | - Release 0.1.4 327 | 328 | ## [0.1.3] - 2021-05-02 329 | 330 | ### Features 331 | 332 | - Create mermaid render 333 | - Make mermaid works 334 | - Create cargo bin 335 | 336 | ### Miscellaneous Tasks 337 | 338 | - Release 0.1.3 339 | 340 | ### Refactor 341 | 342 | - Move render method to mod 343 | 344 | ## [0.1.2] - 2021-05-02 345 | 346 | ### Bug Fixes 347 | 348 | - Fix project link error issue 349 | 350 | ### Documentation 351 | 352 | - Fix docs error issue 353 | 354 | ### Miscellaneous Tasks 355 | 356 | - Release 0.1.2 357 | 358 | ## [0.1.1] - 2021-05-02 359 | 360 | ### Documentation 361 | 362 | - Update README 363 | - Add sample for puml 364 | - Update samples for code 365 | - Update ctags version 366 | 367 | ### Features 368 | 369 | - Make it works 370 | - Expose by files api 371 | 372 | ### Refactor 373 | 374 | - Inline code 375 | 376 | ### Build 377 | 378 | - Create modeling package 379 | - Import code from coco 380 | - Config of projects 381 | 382 | 383 | -------------------------------------------------------------------------------- /src/segment/stop_words.rs: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2005, Jacques Savoy. 2 | // Use of this source code is governed by the BSD license 3 | // license that can be found in the LICENSE file. 4 | // from : https://github.com/bbalet/stopwords 5 | 6 | pub const TECH_STOP_WORDS: &[&str] = &[ 7 | "get", 8 | "create", 9 | "update", 10 | "delete", 11 | "save", 12 | "post", 13 | "add", 14 | "remove", 15 | "insert", 16 | "select", 17 | "exist", 18 | "find", 19 | "new", 20 | "parse", 21 | "set", 22 | "get", 23 | "first", 24 | "last", 25 | "type", 26 | "key", 27 | "value", 28 | "equal", 29 | "greater", 30 | "greater", 31 | "all", 32 | "by", 33 | "id", 34 | "is", 35 | "of", 36 | "not", 37 | "with", 38 | "main", 39 | "status", 40 | "count", 41 | "equals", 42 | "start", 43 | "config", 44 | "sort", 45 | "handle", 46 | "handler", 47 | "internal", 48 | "cache", 49 | "request", 50 | "process", 51 | "parameter", 52 | "method", 53 | "class", 54 | "default", 55 | "object", 56 | "annotation", 57 | "read", 58 | "write", 59 | "bean", 60 | "message", 61 | "factory", 62 | "error", 63 | "errors", 64 | "exception", 65 | "null", 66 | "string", 67 | "init", 68 | "data", 69 | "hash", 70 | "convert", 71 | "size", 72 | "build", 73 | "return", 74 | ]; 75 | 76 | pub const STOP_WORDS: &[&str] = &[ 77 | "a", 78 | "about", 79 | "above", 80 | "across", 81 | "after", 82 | "afterwards", 83 | "again", 84 | "against", 85 | "all", 86 | "almost", 87 | "alone", 88 | "along", 89 | "already", 90 | "also", 91 | "although", 92 | "always", 93 | "am", 94 | "among", 95 | "amongst", 96 | "amoungst", 97 | "amount", 98 | "an", 99 | "and", 100 | "another", 101 | "any", 102 | "anyhow", 103 | "anyone", 104 | "anything", 105 | "anyway", 106 | "anywhere", 107 | "are", 108 | "around", 109 | "as", 110 | "at", 111 | "back", 112 | "be", 113 | "became", 114 | "because", 115 | "become", 116 | "becomes", 117 | "becoming", 118 | "been", 119 | "before", 120 | "beforehand", 121 | "behind", 122 | "being", 123 | "below", 124 | "beside", 125 | "besides", 126 | "between", 127 | "beyond", 128 | "bill", 129 | "both", 130 | "bottom", 131 | "but", 132 | "by", 133 | "call", 134 | "can", 135 | "cannot", 136 | "cant", 137 | "co", 138 | "con", 139 | "could", 140 | "couldnt", 141 | "cry", 142 | "de", 143 | "describe", 144 | "detail", 145 | "do", 146 | "done", 147 | "down", 148 | "due", 149 | "during", 150 | "each", 151 | "eg", 152 | "eight", 153 | "either", 154 | "eleven", 155 | "else", 156 | "elsewhere", 157 | "empty", 158 | "enough", 159 | "etc", 160 | "even", 161 | "ever", 162 | "every", 163 | "everyone", 164 | "everything", 165 | "everywhere", 166 | "except", 167 | "few", 168 | "fifteen", 169 | "fify", 170 | "fill", 171 | "find", 172 | "fire", 173 | "first", 174 | "five", 175 | "for", 176 | "former", 177 | "formerly", 178 | "forty", 179 | "found", 180 | "four", 181 | "from", 182 | "front", 183 | "full", 184 | "further", 185 | "get", 186 | "give", 187 | "go", 188 | "had", 189 | "has", 190 | "hasnt", 191 | "have", 192 | "he", 193 | "hence", 194 | "her", 195 | "here", 196 | "hereafter", 197 | "hereby", 198 | "herein", 199 | "hereupon", 200 | "hers", 201 | "herself", 202 | "him", 203 | "himself", 204 | "his", 205 | "how", 206 | "however", 207 | "hundred", 208 | "ie", 209 | "if", 210 | "in", 211 | "inc", 212 | "indeed", 213 | "interest", 214 | "into", 215 | "is", 216 | "it", 217 | "its", 218 | "itself", 219 | "keep", 220 | "last", 221 | "latter", 222 | "latterly", 223 | "least", 224 | "less", 225 | "ltd", 226 | "made", 227 | "many", 228 | "may", 229 | "me", 230 | "meanwhile", 231 | "might", 232 | "mill", 233 | "mine", 234 | "more", 235 | "moreover", 236 | "most", 237 | "mostly", 238 | "move", 239 | "much", 240 | "must", 241 | "my", 242 | "myself", 243 | "name", 244 | "namely", 245 | "neither", 246 | "never", 247 | "nevertheless", 248 | "next", 249 | "nine", 250 | "no", 251 | "nobody", 252 | "none", 253 | "noone", 254 | "nor", 255 | "not", 256 | "nothing", 257 | "now", 258 | "nowhere", 259 | "of", 260 | "off", 261 | "often", 262 | "on", 263 | "once", 264 | "one", 265 | "only", 266 | "onto", 267 | "or", 268 | "other", 269 | "others", 270 | "otherwise", 271 | "our", 272 | "ours", 273 | "ourselves", 274 | "out", 275 | "over", 276 | "own", 277 | "part", 278 | "per", 279 | "perhaps", 280 | "please", 281 | "put", 282 | "rather", 283 | "re", 284 | "same", 285 | "see", 286 | "seem", 287 | "seemed", 288 | "seeming", 289 | "seems", 290 | "serious", 291 | "several", 292 | "she", 293 | "should", 294 | "show", 295 | "side", 296 | "since", 297 | "sincere", 298 | "six", 299 | "sixty", 300 | "so", 301 | "some", 302 | "somehow", 303 | "someone", 304 | "something", 305 | "sometime", 306 | "sometimes", 307 | "somewhere", 308 | "still", 309 | "such", 310 | "system", 311 | "take", 312 | "ten", 313 | "than", 314 | "that", 315 | "the", 316 | "their", 317 | "them", 318 | "themselves", 319 | "then", 320 | "thence", 321 | "there", 322 | "thereafter", 323 | "thereby", 324 | "therefore", 325 | "therein", 326 | "thereupon", 327 | "these", 328 | "they", 329 | "thickv", 330 | "thin", 331 | "third", 332 | "this", 333 | "those", 334 | "though", 335 | "three", 336 | "through", 337 | "throughout", 338 | "thru", 339 | "thus", 340 | "to", 341 | "together", 342 | "too", 343 | "top", 344 | "toward", 345 | "towards", 346 | "twelve", 347 | "twenty", 348 | "two", 349 | "un", 350 | "under", 351 | "until", 352 | "up", 353 | "upon", 354 | "us", 355 | "very", 356 | "via", 357 | "was", 358 | "we", 359 | "well", 360 | "were", 361 | "what", 362 | "whatever", 363 | "when", 364 | "whence", 365 | "whenever", 366 | "where", 367 | "whereafter", 368 | "whereas", 369 | "whereby", 370 | "wherein", 371 | "whereupon", 372 | "wherever", 373 | "whether", 374 | "which", 375 | "while", 376 | "whither", 377 | "who", 378 | "whoever", 379 | "whole", 380 | "whom", 381 | "whose", 382 | "why", 383 | "will", 384 | "with", 385 | "within", 386 | "without", 387 | "would", 388 | "yet", 389 | "you", 390 | "your", 391 | "yours", 392 | "yourself", 393 | "yourselves", 394 | ]; 395 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate lazy_static; 3 | extern crate serde; 4 | 5 | use std::path::Path; 6 | 7 | use ignore::Walk; 8 | use structopt::StructOpt; 9 | 10 | pub use coco_struct::{ClassInfo, MemberInfo, MethodInfo}; 11 | pub use ctags::ctags_cmd::CmdCtags; 12 | pub use ctags::ctags_opt::Opt; 13 | pub use ctags::ctags_parser::CtagsParser; 14 | pub use file_filter::*; 15 | pub use parse_option::ParseOption; 16 | 17 | use crate::file_filter::FileFilter; 18 | 19 | pub mod coco_struct; 20 | pub mod ctags; 21 | pub mod file_filter; 22 | pub mod parse_option; 23 | pub mod render; 24 | pub mod segment; 25 | 26 | /// Returns Vec with the given path. 27 | /// 28 | /// # Arguments 29 | /// 30 | /// * `path` - CODE PATH 31 | /// 32 | /// # Examples 33 | /// 34 | /// ``` 35 | /// use modeling::{by_dir, ParseOption}; 36 | /// use modeling::render::PlantUmlRender; 37 | /// 38 | /// use modeling::file_filter::FileFilter; 39 | /// let classes = by_dir("src/",FileFilter::default(), &ParseOption::default()); 40 | /// let puml = PlantUmlRender::render(&classes, &ParseOption::default()); 41 | /// ``` 42 | pub fn by_dir>(path: P, filter: FileFilter, option: &ParseOption) -> Vec { 43 | by_files(files_from_path(path, filter), option) 44 | } 45 | 46 | /// Returns Vec with the given files. 47 | /// 48 | /// # Arguments 49 | /// 50 | /// * `files` - code files in string 51 | /// 52 | /// # Examples 53 | /// 54 | /// ``` 55 | /// use modeling::{by_files, ParseOption}; 56 | /// use modeling::render::PlantUmlRender; 57 | /// 58 | /// let mut files = vec![]; 59 | /// files.push("src/lib.rs".to_string()); 60 | /// let classes = by_files(files, &ParseOption::default()); 61 | /// let puml = PlantUmlRender::render(&classes, &ParseOption::default()); 62 | /// ``` 63 | pub fn by_files(files: Vec, option: &ParseOption) -> Vec { 64 | let thread = count_thread(&files); 65 | let opt = build_opt(thread); 66 | 67 | run_ctags(&opt, &files_by_thread(files, &opt), option) 68 | } 69 | 70 | fn count_thread(origin_files: &[String]) -> usize { 71 | let mut thread = origin_files.len(); 72 | let default_ptags_thread = 8; 73 | if thread >= default_ptags_thread { 74 | thread = default_ptags_thread; 75 | } 76 | thread 77 | } 78 | 79 | fn run_ctags(opt: &Opt, files: &[String], option: &ParseOption) -> Vec { 80 | let outputs = CmdCtags::call(opt, files).unwrap(); 81 | let mut iters = Vec::new(); 82 | for o in &outputs { 83 | let iter = if opt.validate_utf8 { 84 | std::str::from_utf8(&o.stdout).unwrap().lines() 85 | } else { 86 | unsafe { std::str::from_utf8_unchecked(&o.stdout).lines() } 87 | }; 88 | iters.push(iter); 89 | } 90 | 91 | let mut parser = CtagsParser::parse_str(iters); 92 | 93 | parser.option = option.clone(); 94 | parser.classes() 95 | } 96 | 97 | fn files_from_path>(path: P, filter: FileFilter) -> Vec { 98 | let mut origin_files = vec![]; 99 | for entry in Walk::new(path).flatten() { 100 | if entry.file_type().unwrap().is_file() { 101 | let buf = entry.path().to_path_buf(); 102 | if filter.allow(buf) { 103 | origin_files.push(format!("{}", entry.path().display())) 104 | } 105 | } 106 | } 107 | 108 | origin_files 109 | } 110 | 111 | fn files_by_thread(origin_files: Vec, opt: &Opt) -> Vec { 112 | let mut files = vec![String::from(""); opt.thread]; 113 | for (i, f) in origin_files.iter().enumerate() { 114 | files[i % opt.thread].push_str(f); 115 | files[i % opt.thread].push('\n'); 116 | } 117 | files 118 | } 119 | 120 | fn build_opt(thread: usize) -> Opt { 121 | let string = thread.to_string(); 122 | let thread: &str = string.as_str(); 123 | let args = vec!["ptags", "-t", thread, "--verbose=true", "--fields=+latinK"]; 124 | let opt = Opt::from_iter(args.iter()); 125 | opt 126 | } 127 | 128 | #[cfg(test)] 129 | mod tests { 130 | use std::fs; 131 | use std::path::PathBuf; 132 | 133 | use crate::file_filter::FileFilter; 134 | use crate::render::{MermaidRender, PlantUmlRender}; 135 | use crate::{by_dir, ParseOption}; 136 | 137 | pub fn ctags_fixtures_dir() -> PathBuf { 138 | PathBuf::from(env!("CARGO_MANIFEST_DIR")) 139 | .join("_fixtures") 140 | .join("ctags") 141 | .join("source") 142 | .join("animal.ts") 143 | } 144 | 145 | #[test] 146 | fn should_run_struct_analysis() { 147 | let path = format!("{}", ctags_fixtures_dir().display()); 148 | let option = ParseOption::default(); 149 | let vec = by_dir(path, FileFilter::default(), &option); 150 | 151 | assert_eq!(3, vec.len()); 152 | let result = PlantUmlRender::render(&vec, &option); 153 | 154 | let _ = fs::write("demo.puml", result.clone()); 155 | assert!(result.contains("class Animal")); 156 | assert!(result.contains("Animal <|-- Horse")); 157 | assert!(result.contains("Animal <|-- Snake")); 158 | } 159 | 160 | #[test] 161 | fn should_only_have_one_file() { 162 | let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 163 | let ctags_dir = root_dir.join("_fixtures").join("ctags").join("source"); 164 | 165 | let path = format!("{}", ctags_dir.display()); 166 | 167 | let suffixes = vec!["store".to_string()]; 168 | 169 | let option = ParseOption::default(); 170 | let vec = by_dir( 171 | path, 172 | FileFilter::new(vec![], suffixes, "".to_string()), 173 | &option, 174 | ); 175 | 176 | assert_eq!(3, vec.len()); 177 | let result = PlantUmlRender::render(&vec, &option); 178 | 179 | let _ = fs::write("demo.puml", result.clone()); 180 | assert!(!result.contains("class Animal")); 181 | assert!(result.contains("class classinfo_st")); 182 | } 183 | 184 | #[test] 185 | fn should_support_for_rust_property() { 186 | let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 187 | let ctags_dir = root_dir.join("src").join("ctags"); 188 | 189 | let path = format!("{}", ctags_dir.display()); 190 | 191 | let option = ParseOption::default(); 192 | let vec = by_dir( 193 | path, 194 | FileFilter::new(vec![], vec![], "".to_string()), 195 | &option, 196 | ); 197 | 198 | assert_eq!(3, vec.len()); 199 | } 200 | 201 | #[test] 202 | fn should_filter_by_grep() { 203 | let root_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")); 204 | let ctags_dir = root_dir.join("_fixtures").join("ctags").join("source"); 205 | 206 | let path = format!("{}", ctags_dir.display()); 207 | 208 | let option = ParseOption::default(); 209 | let filter = FileFilter::new(vec![], vec![], "store.go".to_string()); 210 | let vec = by_dir(path, filter, &option); 211 | 212 | assert_eq!(3, vec.len()); 213 | } 214 | 215 | #[test] 216 | fn should_render_mermaid() { 217 | let path = format!("{}", ctags_fixtures_dir().display()); 218 | let option = ParseOption::default(); 219 | let vec = by_dir(path, FileFilter::default(), &option); 220 | 221 | assert_eq!(3, vec.len()); 222 | let result = MermaidRender::render(&vec, &option); 223 | 224 | let _ = fs::write("demo.puml", result.clone()); 225 | assert!(result.contains("class Animal")); 226 | assert!(result.contains("Animal <|-- Horse")); 227 | assert!(result.contains("Animal <|-- Snake")); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /src/render/mod.rs: -------------------------------------------------------------------------------- 1 | use crate::{ClassInfo, ParseOption}; 2 | pub use mermaid_render::MermaidRender; 3 | pub use plantuml_render::PlantUmlRender; 4 | use std::collections::HashMap; 5 | 6 | pub mod graphviz_render; 7 | pub mod mermaid_render; 8 | pub mod plantuml_render; 9 | 10 | pub fn process_name(parse_option: &&ParseOption, name: &str) -> String { 11 | if !parse_option.without_suffix.is_empty() { 12 | if let Some(text) = name.strip_suffix(&parse_option.without_suffix) { 13 | return text.to_string(); 14 | } 15 | } 16 | 17 | name.to_string() 18 | } 19 | 20 | pub fn render_method( 21 | clazz: &ClassInfo, 22 | dep_map: &mut HashMap, 23 | space: &str, 24 | parse_option: &ParseOption, 25 | ) -> Vec { 26 | let mut methods = vec![]; 27 | let clazz_name = process_name(&parse_option, &clazz.name); 28 | for method in &clazz.methods { 29 | let method_name = process_name(&parse_option, &method.name); 30 | if method.return_type.is_empty() { 31 | methods.push(format!("{} {}{}()\n", space, method.access, method_name)) 32 | } else { 33 | methods.push(format!( 34 | "{} {} {} {}()\n", 35 | space, method.access, method.return_type, method_name 36 | )); 37 | 38 | if method.pure_return_type.len() > 0 { 39 | dep_map.insert(method.pure_return_type.clone(), clazz_name.clone()); 40 | } else { 41 | dep_map.insert(method.return_type.clone(), clazz_name.clone()); 42 | } 43 | } 44 | } 45 | methods 46 | } 47 | 48 | pub fn render_member( 49 | clazz: &ClassInfo, 50 | dep_map: &mut HashMap, 51 | space: &str, 52 | parse_option: &ParseOption, 53 | class_map: &mut HashMap, 54 | ) -> Vec { 55 | let clazz_name = process_name(&parse_option, &clazz.name); 56 | let mut members = vec![]; 57 | for member in &clazz.members { 58 | let member_name = process_name(&parse_option, &member.name); 59 | if member.data_type.is_empty() { 60 | members.push(format!("{} {}{}\n", space, member.access, member_name)) 61 | } else { 62 | let id = "Id"; 63 | let mut data_type = member.data_type.to_string(); 64 | if parse_option.without_impl_suffix { 65 | // ex. `IRepository` will check is R uppercase 66 | if data_type.len() > id.len() && data_type.starts_with("I") { 67 | let char = data_type.chars().nth(1).unwrap(); 68 | if char.to_uppercase().to_string() == char.to_string() { 69 | data_type = (&data_type[1..data_type.len()]).to_string(); 70 | } 71 | } 72 | } 73 | 74 | if parse_option.inline_id_suffix { 75 | data_type = remove_suffix_id(class_map, &member_name, data_type, "Id"); 76 | data_type = remove_suffix_id(class_map, &member_name, data_type, "Ids"); 77 | } 78 | 79 | members.push(format!( 80 | "{} {} {} {}\n", 81 | space, member.access, data_type, member_name 82 | )); 83 | 84 | if member.pure_data_type.len() > 0 { 85 | dep_map.insert(member.pure_data_type.clone(), clazz_name.clone()); 86 | } else { 87 | dep_map.insert(data_type.to_string(), clazz_name.clone()); 88 | } 89 | } 90 | } 91 | members 92 | } 93 | 94 | fn remove_suffix_id(class_map: &mut HashMap, member_name: &String, data_type: String, ids: &str) -> String { 95 | if member_name.ends_with(ids) && member_name.len() > ids.len() { 96 | let member_name = &member_name[0..(member_name.len() - ids.len())]; 97 | if class_map.get(member_name).is_some() { 98 | return member_name.to_string() 99 | } 100 | } 101 | 102 | return data_type 103 | } 104 | 105 | #[cfg(test)] 106 | mod tests { 107 | use crate::coco_struct::{ClassInfo, MemberInfo, MethodInfo}; 108 | use crate::render::PlantUmlRender; 109 | use crate::ParseOption; 110 | 111 | #[test] 112 | fn should_render_empty() { 113 | let classes = vec![]; 114 | let str = PlantUmlRender::render(&classes, &ParseOption::default()); 115 | assert_eq!("@startuml\n\n\n\n@enduml", str); 116 | } 117 | 118 | #[test] 119 | fn should_render_single_empty_class() { 120 | let mut classes = vec![]; 121 | let demo = ClassInfo::new("Demo"); 122 | classes.push(demo); 123 | 124 | let str = PlantUmlRender::render(&classes, &ParseOption::default()); 125 | assert_eq!("@startuml\n\nclass Demo {\n}\n\n@enduml", str); 126 | } 127 | 128 | #[test] 129 | fn should_render_member_method() { 130 | let mut classes = vec![]; 131 | let mut demo = ClassInfo::new("Demo"); 132 | 133 | let member = MemberInfo::new("demo", "-".to_string(), "String".to_string()); 134 | demo.members.push(member); 135 | 136 | let method = MethodInfo::new("method", "-".to_string(), vec![], "Demo".to_string()); 137 | demo.methods.push(method); 138 | 139 | classes.push(demo); 140 | 141 | let str = PlantUmlRender::render(&classes, &ParseOption::default()); 142 | assert_eq!( 143 | "@startuml\n\nclass Demo {\n - String demo\n - Demo method()\n}\n\n@enduml", 144 | str 145 | ); 146 | } 147 | 148 | #[test] 149 | fn should_render_deps() { 150 | let mut classes = vec![]; 151 | let mut demo = ClassInfo::new("Demo"); 152 | let demo2 = ClassInfo::new("Demo2"); 153 | 154 | let member = MemberInfo::new("demo", "-".to_string(), "String".to_string()); 155 | demo.members.push(member); 156 | 157 | let method = MethodInfo::new("method", "-".to_string(), vec![], "Demo2".to_string()); 158 | demo.methods.push(method); 159 | 160 | classes.push(demo); 161 | classes.push(demo2); 162 | 163 | let str = PlantUmlRender::render(&classes, &ParseOption::default()); 164 | assert_eq!(true, str.contains("Demo -- Demo2")); 165 | assert_eq!(false, str.contains("Demo -- String")); 166 | } 167 | 168 | #[test] 169 | fn should_render_parents() { 170 | let mut classes = vec![]; 171 | let mut demo = ClassInfo::new("Demo"); 172 | let demo2 = ClassInfo::new("Demo2"); 173 | 174 | demo.parents.push(demo2.name.clone()); 175 | 176 | classes.push(demo); 177 | classes.push(demo2); 178 | 179 | let str = PlantUmlRender::render(&classes, &ParseOption::default()); 180 | println!("{}", str); 181 | assert!(str.contains("Demo2 <|-- Demo")); 182 | } 183 | 184 | #[test] 185 | fn should_render_array() { 186 | let mut classes = vec![]; 187 | let mut demo = ClassInfo::new("Demo"); 188 | let demo2 = ClassInfo::new("Demo2"); 189 | 190 | let mut method = MethodInfo::new("method", "-".to_string(), vec![], "[]Demo2".to_string()); 191 | method.pure_return_type = "Demo2".to_string(); 192 | demo.methods.push(method); 193 | 194 | classes.push(demo); 195 | classes.push(demo2); 196 | 197 | let str = PlantUmlRender::render(&classes, &ParseOption::default()); 198 | assert_eq!(true, str.contains("Demo -- Demo2")); 199 | assert_eq!(false, str.contains("Demo -- String")); 200 | } 201 | 202 | #[test] 203 | fn should_remove_suffix_text() { 204 | let mut classes = vec![]; 205 | let mut demo = ClassInfo::new("DemoDto"); 206 | let demo2 = ClassInfo::new("Demo2Dto"); 207 | 208 | let mut method = MethodInfo::new("method", "-".to_string(), vec![], "[]Demo2".to_string()); 209 | method.pure_return_type = "Demo2".to_string(); 210 | demo.methods.push(method); 211 | 212 | classes.push(demo); 213 | classes.push(demo2); 214 | 215 | let mut parse_option = ParseOption::default(); 216 | parse_option.without_suffix = "Dto".to_string(); 217 | 218 | let str = PlantUmlRender::render(&classes, &parse_option); 219 | assert_eq!(true, str.contains("Demo -- Demo2")); 220 | assert_eq!(false, str.contains("Demo -- String")); 221 | } 222 | 223 | #[test] 224 | fn should_char() { 225 | let str = "IRepo"; 226 | let char = str.chars().nth(1).unwrap(); 227 | assert_eq!('R', char); 228 | } 229 | } 230 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to coco 2 | 3 | We would love for you to contribute to coco and help make it even better than it is 4 | today! As a contributor, here are the guidelines we would like you to follow: 5 | 6 | - [Code of Conduct](#coc) 7 | - [Question or Problem?](#question) 8 | - [Issues and Bugs](#issue) 9 | - [Feature Requests](#feature) 10 | - [Submission Guidelines](#submit) 11 | - [Coding Rules](#rules) 12 | - [Commit Message Guidelines](#commit) 13 | 14 | ## Code of Conduct 15 | 16 | Help us keep coco open and inclusive. Please read and follow our [Code of Conduct][coc]. 17 | 18 | ## Got a Question or Problem? 19 | 20 | Do not open issues for general support questions as we want to keep GitHub issues for bug reports and feature requests. 21 | 22 | Segmentfault / Stack Overflow is a much better place to ask questions since: 23 | 24 | - there are thousands of people willing to help on Segmentfault 25 | - questions and answers stay available for public viewing so your question / answer might help someone else 26 | - Segmentfault's voting system assures that the best answers are prominently visible. 27 | 28 | To save your and our time, we will systematically close all issues that are requests for general support and redirect people to Segmentfault / Stack Overflow. 29 | 30 | ## Found a Bug? 31 | If you find a bug in the source code, you can help us by 32 | [submitting an issue](#submit-issue) to our [GitHub Repository][github]. Even better, you can 33 | [submit a Pull Request](#submit-pr) with a fix. 34 | 35 | ## Missing a Feature? 36 | You can *request* a new feature by [submitting an issue](#submit-issue) to our GitHub 37 | Repository. If you would like to *implement* a new feature, please submit an issue with 38 | a for your work first, to be sure that we can use it. 39 | Please consider what kind of change it is: 40 | 41 | * For a **Major Feature**, first open an issue and outline your proposal so that it can be 42 | discussed. This will also allow us to better coordinate our efforts, prevent duplication of work, 43 | and help you to craft the change so that it is successfully accepted into the project. 44 | * **Small Features** can be crafted and directly [submitted as a Pull Request](#submit-pr). 45 | 46 | ## Submission Guidelines 47 | 48 | ### Submitting an Issue 49 | 50 | Before you submit an issue, please search the issue tracker, maybe an issue for your problem already exists and the discussion might inform you of workarounds readily available. 51 | 52 | We want to fix all the issues as soon as possible, but before fixing a bug we need to reproduce and confirm it. In order to reproduce bugs we will systematically ask you to provide a minimal reproduction scenario using http://plnkr.co. Having a live, reproducible scenario gives us wealth of important information without going back & forth to you with additional questions like: 53 | 54 | - version of coco used 55 | - 3rd-party libraries and their versions 56 | - and most importantly - a use-case that fails 57 | 58 | A minimal reproduce scenario using http://plnkr.co/ allows us to quickly confirm a bug (or point out coding problem) as well as confirm that we are fixing the right problem. If plunker is not a suitable way to demonstrate the problem (for example for issues related to our npm packaging), please create a standalone git repository demonstrating the problem. 59 | 60 | We will be insisting on a minimal reproduce scenario in order to save maintainers time and ultimately be able to fix more bugs. Interestingly, from our experience users often find coding problems themselves while preparing a minimal plunk. We understand that sometimes it might be hard to extract essentials bits of code from a larger code-base but we really need to isolate the problem before we can fix it. 61 | 62 | Unfortunately we are not able to investigate / fix bugs without a minimal reproduction, so if we don't hear back from you we are going to close an issue that don't have enough info to be reproduced. 63 | 64 | You can file new issues by filling out our [new issue form](https://github.com/inherd/coco/issues/new). 65 | 66 | 67 | ### Submitting a Pull Request (PR) 68 | Before you submit your Pull Request (PR) consider the following guidelines: 69 | 70 | * Search [GitHub](https://github.com/inherd/coco/pulls) for an open or closed PR 71 | that relates to your submission. You don't want to duplicate effort. 72 | * Make your changes in a new git branch: 73 | 74 | ```shell 75 | git checkout -b my-fix-branch master 76 | ``` 77 | 78 | * Create your patch, **including appropriate test cases**. 79 | * Follow our [Coding Rules](#rules). 80 | * Run the full coco test suite, and ensure that all tests pass. 81 | * Commit your changes using a descriptive commit message that follows our 82 | [commit message conventions](#commit). Adherence to these conventions 83 | is necessary because release notes are automatically generated from these messages. 84 | 85 | ```shell 86 | git commit -a 87 | ``` 88 | Note: the optional commit `-a` command line option will automatically "add" and "rm" edited files. 89 | 90 | * Push your branch to GitHub: 91 | 92 | ```shell 93 | git push origin my-fix-branch 94 | ``` 95 | 96 | * In GitHub, send a pull request to `coco:master`. 97 | * If we suggest changes then: 98 | * Make the required updates. 99 | * Re-run the coco test suites to ensure tests are still passing. 100 | * Rebase your branch and force push to your GitHub repository (this will update your Pull Request): 101 | 102 | ```shell 103 | git rebase master -i 104 | git push -f 105 | ``` 106 | 107 | That's it! Thank you for your contribution! 108 | 109 | #### After your pull request is merged 110 | 111 | After your pull request is merged, you can safely delete your branch and pull the changes 112 | from the main (upstream) repository: 113 | 114 | * Delete the remote branch on GitHub either through the GitHub web UI or your local shell as follows: 115 | 116 | ```shell 117 | git push origin --delete my-fix-branch 118 | ``` 119 | 120 | * Check out the master branch: 121 | 122 | ```shell 123 | git checkout master -f 124 | ``` 125 | 126 | * Delete the local branch: 127 | 128 | ```shell 129 | git branch -D my-fix-branch 130 | ``` 131 | 132 | * Update your master with the latest upstream version: 133 | 134 | ```shell 135 | git pull --ff upstream master 136 | ``` 137 | 138 | ## Coding Rules 139 | To ensure consistency throughout the source code, keep these rules in mind as you are working: 140 | 141 | * All features or bug fixes **must be tested** by one or more specs (unit-tests). 142 | * All public API methods **must be documented**. 143 | 144 | ## Commit Message Guidelines 145 | 146 | We have very precise rules over how our git commit messages can be formatted. This leads to **more 147 | readable messages** that are easy to follow when looking through the **project history**. But also, 148 | we use the git commit messages to **generate the coco change log**. 149 | 150 | ### Commit Message Format 151 | Each commit message consists of a **header**, a **body** and a **footer**. The header has a special 152 | format that includes a **type**, a **scope** and a **subject**: 153 | 154 | ``` 155 | (): 156 | 157 | 158 | 159 |