├── .editorconfig ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── README.md ├── dub.json ├── source ├── app.d └── dsemver │ ├── ast.d │ ├── buildinterface.d │ ├── compare.d │ ├── git.d │ ├── options.d │ └── semver.d ├── testfilegen.d └── testfiles ├── class1.d ├── function1.d ├── function2.d ├── function3.d ├── interface1.d ├── interface2.d ├── struct1.d ├── struct2.d ├── struct3.d ├── template1.d ├── template2.d ├── template3.d ├── union1.d └── variable1.d /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = tab 6 | indent_size = 4 7 | tab_width = 4 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = tr 12 | max_line_length = 80 13 | 14 | [*.yaml] 15 | indent_style = space 16 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | on: 4 | push: 5 | pull_request: 6 | 7 | defaults: 8 | run: 9 | shell: bash 10 | 11 | jobs: 12 | Test: 13 | if: "!contains(github.event.head_commit.message, '[skip ci]')" 14 | 15 | runs-on: ${{ matrix.os }} 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | os: 20 | #- windows-latest 21 | - ubuntu-latest 22 | #- macOS-latest 23 | compiler: 24 | - 'dmd-latest' 25 | - 'dmd-beta' 26 | #- 'ldc-latest' 27 | # - 'ldc-beta' # the tests crash 28 | steps: 29 | - uses: actions/checkout@v2 30 | with: 31 | fetch-depth: 0 32 | 33 | - name: Install compiler 34 | uses: dlang-community/setup-dlang@v1 35 | with: 36 | compiler: ${{ matrix.compiler }} 37 | 38 | - name: Test 39 | run: | 40 | dmd -run testfilegen.d 41 | dub test 42 | 43 | Skip: 44 | if: "contains(github.event.head_commit.message, '[skip ci]')" 45 | runs-on: ubuntu-latest 46 | steps: 47 | - name: Skip CI 🚫 48 | run: echo skip CI 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .dub 2 | docs.json 3 | __dummy.html 4 | docs/ 5 | /dsemver 6 | dsemver.so 7 | dsemver.dylib 8 | dsemver.dll 9 | dsemver.a 10 | dsemver.lib 11 | dsemver-test-* 12 | *.exe 13 | *.o 14 | *.obj 15 | *.lst 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # dsemver 2 | 3 | Let the computer compute the SemVer of the software. 4 | 5 | # Idea 6 | 7 | Semantic versioning is useful, but 0.x.x versioning is a pointless loophole. 8 | When a piece of D software is published, and it is published if other people 9 | can find it on dub, it is released. 10 | And released means 1.0.0 at least. 11 | 12 | Now, spending your time deciding what the right next version for a release is, 13 | is a waste of time. 14 | This can be computed. 15 | This is what dsemver does. 16 | 17 | # dlang semver 18 | 19 | First release is 1.0.0. 20 | There is no 0.x.x nightmare. 21 | 22 | If a symbol is removed or its signature changed the major version is increment 23 | and the minor and the bugfix number reset to 0. 24 | 25 | If a new symbol gets added, the minor number is incremented and the bug fix 26 | number is set to 0. 27 | 28 | If all symbol stay the same the bugfix number is incremented. 29 | 30 | # Usage 31 | 32 | ```sh 33 | # print help imformation 34 | ./dsemver -h 35 | ``` 36 | 37 | ```sh 38 | # This will show the computed next version if there already exists a version tag 39 | ./dsemver -p PATH_TO_DUB_FOLDER -c 40 | ``` 41 | 42 | ```sh 43 | # compare two files 44 | ./dsemver -o OLD_FILE.json -n NEW_FILE.json 45 | ``` 46 | 47 | ```sh 48 | # using indirectly through dub 49 | dub run dsemver -- YOUR options here 50 | ``` 51 | 52 | # FAQ 53 | 54 | ## Doesn't that mean we are going to have packages with version 1000.0.0 55 | 56 | So what, all that stats for sure is that release 1000 has not backwards 57 | comparability changes in relation to version 999.x.x. 58 | 59 | ## Found a bug? 60 | 61 | Please create an issue, and if you are really nice, create a pull request that 62 | fixes it. 63 | 64 | ## Isn't this what the ELM project repository does? 65 | 66 | Yes, this is where I proudfully stole/copied/borrows the idea from. 67 | So, thank you elm people. 68 | -------------------------------------------------------------------------------- /dub.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | "Robert Schadek" 4 | ], 5 | "copyright": "Copyright © 2020, Robert Schadek", 6 | "dependencies": { 7 | "argsd": ">=1.0.0" 8 | }, 9 | "description": "A program that computes the next semver for a dlang library or program", 10 | "license": "LGPL3", 11 | "name": "dsemver" 12 | } 13 | -------------------------------------------------------------------------------- /source/app.d: -------------------------------------------------------------------------------- 1 | module app; 2 | 3 | import std.stdio; 4 | import std.array : empty, front; 5 | import std.algorithm.iteration : map; 6 | 7 | import dsemver.ast; 8 | import dsemver.buildinterface; 9 | import dsemver.compare; 10 | import dsemver.git; 11 | import dsemver.options; 12 | import dsemver.semver; 13 | 14 | int main(string[] args) { 15 | getOptOptions(args); 16 | 17 | string latest = getOptions().old; 18 | if(!getOptions().projectPath.empty 19 | && getOptions().buildNextSemVer 20 | && latest.empty 21 | ) 22 | { 23 | latest = buildInterface("latest"); 24 | } 25 | 26 | if(!getOptions().testParse.empty) { 27 | auto tp = parse(getOptions().testParse); 28 | } 29 | 30 | if(!getOptions().old.empty && !getOptions().neu.empty) { 31 | auto old = parse(getOptions().old); 32 | auto neu = parse(getOptions().neu); 33 | auto onrs = compareOldNew(old, neu); 34 | const onr = summarize(onrs); 35 | 36 | auto nors = compareOldNew(neu, old); 37 | const nor = summarize(nors); 38 | writefln("%s + %s = %s", onr, nor, combine(onr, nor)); 39 | } 40 | 41 | string latestTagFn = getOptions().neu; 42 | SemVer latestSemVer; 43 | 44 | if((getOptions().buildLastestTag || getOptions().buildNextSemVer) 45 | && latestTagFn.empty 46 | ) 47 | { 48 | const c = isClean(getOptions().projectPath); 49 | if(!c) { 50 | writefln("the git of the project '%s' has uncommited changes" 51 | ~ " this is not supported" 52 | , getOptions().projectPath); 53 | return 1; 54 | } 55 | auto tags = getTags(getOptions().projectPath); 56 | if(tags.empty) { 57 | writefln("No tags that match a semver found in '%s'" 58 | , getOptions().projectPath); 59 | return 1; 60 | } 61 | 62 | scope(exit) { 63 | checkoutRef(getOptions().projectPath, "master"); 64 | } 65 | 66 | checkoutRef(getOptions().projectPath, tags.front.gitRef); 67 | latestTagFn = buildInterface(tags.front.semver.toString()); 68 | latestSemVer = tags.front.semver; 69 | } 70 | 71 | if(getOptions().buildNextSemVer) { 72 | if(latest.empty) { 73 | writefln("No latest dsemver file available"); 74 | return 1; 75 | } 76 | 77 | if(latestTagFn.empty) { 78 | writefln("No latest git tag dsemver file available"); 79 | return 1; 80 | } 81 | 82 | auto old = parse(latestTagFn); 83 | auto neu = parse(latest); 84 | auto onrs = compareOldNew(old, neu); 85 | 86 | if(getOptions().verbose) { 87 | writefln("%--(%s\n%)", onrs.map!(i => i.reason)); 88 | } 89 | 90 | const onr = summarize(onrs); 91 | 92 | auto nors = compareOldNew(neu, old); 93 | const nor = summarize(nors); 94 | const com = combine(onr, nor); 95 | 96 | if(getOptions().verbose) { 97 | writefln("%s + %s = %s", onr, nor, com); 98 | } 99 | 100 | if(latestSemVer != SemVer.init) { 101 | latestSemVer.preRelease = []; 102 | latestSemVer.buildIdentifier = []; 103 | if(latestSemVer.major == 0) { 104 | latestSemVer = SemVer(1, 0, 0); 105 | } else if(com == ResultValue.major) { 106 | latestSemVer = SemVer(latestSemVer.major + 1, 0, 0); 107 | } else if(com == ResultValue.minor) { 108 | latestSemVer.minor++; 109 | } else if(com == ResultValue.equal) { 110 | latestSemVer.patch++; 111 | } 112 | writefln("\nNext release should have version '%s'\n", latestSemVer); 113 | } 114 | } 115 | return 0; 116 | } 117 | -------------------------------------------------------------------------------- /source/dsemver/ast.d: -------------------------------------------------------------------------------- 1 | module dsemver.ast; 2 | 3 | import std.stdio; 4 | import std.array : array, appender; 5 | import std.algorithm.iteration : map; 6 | import std.algorithm.searching : endsWith, startsWith; 7 | import std.json; 8 | import std.typecons : Nullable, nullable; 9 | import std.traits : isArray, isSomeString, isIntegral, isFloatingPoint, 10 | FieldNameTuple, isBasicType; 11 | import std.algorithm : sort, setDifference; 12 | import std.range : ElementEncodingType; 13 | import std.format; 14 | import std.exception : enforce; 15 | 16 | struct Parameter { 17 | Nullable!(string) name; 18 | Nullable!(string) type; 19 | Nullable!(string) deco; 20 | Nullable!(string) kind; 21 | Nullable!(string) defaultValue; 22 | Nullable!(string) default_; 23 | Nullable!(string[]) storageClass; 24 | } 25 | 26 | struct Member { 27 | string name; 28 | string kind; 29 | Nullable!(string) originalType; 30 | Nullable!(string) type; 31 | Nullable!(string) base; 32 | Nullable!(string) init_; 33 | Nullable!(string) value; 34 | Nullable!(string[]) storageClass; 35 | Nullable!(string) deco; 36 | Nullable!(string) baseDeco; 37 | Nullable!(long) align_; 38 | Nullable!(long) offset; 39 | Nullable!(Parameter[]) parameters; 40 | Nullable!(string[]) overrides; 41 | Nullable!(string) protection; 42 | Nullable!(string[]) selective; 43 | Nullable!(Member[]) members; 44 | } 45 | 46 | struct Module { 47 | Nullable!string name; 48 | string kind; 49 | string file; 50 | Member[] members; 51 | } 52 | 53 | string moduleToName(ref const(Module) mod) pure @safe { 54 | return mod.name.isNull 55 | ? mod.file 56 | : mod.name.get(); 57 | } 58 | 59 | string toString(ref const(Parameter) p) { 60 | auto app = appender!string(); 61 | formattedWrite(app, "\"%s", p.name); 62 | if(!p.type.isNull()) { 63 | formattedWrite(app, " type %s", p.type.get()); 64 | } 65 | if(!p.deco.isNull()) { 66 | formattedWrite(app, " deco %s", p.deco.get()); 67 | } 68 | if(!p.kind.isNull()) { 69 | formattedWrite(app, " kind %s", p.kind.get()); 70 | } 71 | if(!p.defaultValue.isNull()) { 72 | formattedWrite(app, " defaultValue %s", p.defaultValue.get()); 73 | } 74 | if(!p.default_.isNull()) { 75 | formattedWrite(app, " default %s", p.default_.get()); 76 | } 77 | if(!p.storageClass.isNull()) { 78 | formattedWrite(app, " storageClass %(%s, %)", p.storageClass.get()); 79 | } 80 | app.put("\""); 81 | return app.data; 82 | } 83 | 84 | string toString(ref const(Member) mem) { 85 | import core.demangle; 86 | auto app = appender!string(); 87 | formattedWrite(app, "\"%s", mem.kind); 88 | formattedWrite(app, " '%s'", mem.name); 89 | if(!mem.originalType.isNull) { 90 | formattedWrite(app, " original_type '%s'", mem.originalType.get()); 91 | } 92 | if(!mem.type.isNull) { 93 | formattedWrite(app, " type '%s'", mem.type.get()); 94 | } 95 | if(!mem.value.isNull) { 96 | formattedWrite(app, " value '%s'", mem.value.get()); 97 | } 98 | if(!mem.storageClass.isNull) { 99 | formattedWrite(app, " storage class '%s'", mem.storageClass.get()); 100 | } 101 | if(!mem.deco.isNull) { 102 | formattedWrite(app, " of type '%s'", demangleType(mem.deco.get())); 103 | } 104 | if(!mem.baseDeco.isNull) { 105 | formattedWrite(app, " '%s'", mem.baseDeco.get()); 106 | } 107 | if(!mem.align_.isNull) { 108 | formattedWrite(app, " align '%s'", mem.align_.get()); 109 | } 110 | if(!mem.base.isNull) { 111 | formattedWrite(app, " base '%s'", mem.base.get()); 112 | } 113 | if(!mem.offset.isNull) { 114 | formattedWrite(app, " offset '%s'", mem.offset.get()); 115 | } 116 | if(!mem.init_.isNull) { 117 | formattedWrite(app, " init '%s'", mem.init_.get()); 118 | } 119 | if(!mem.parameters.isNull) { 120 | formattedWrite(app, " parameters '%(%s, %)'" 121 | , mem.parameters.get().map!(p => p.toString())); 122 | } 123 | if(!mem.overrides.isNull) { 124 | formattedWrite(app, " overrides '%(%s, %)'", mem.overrides.get()); 125 | } 126 | if(!mem.protection.isNull) { 127 | formattedWrite(app, " protection '%s'", mem.protection.get()); 128 | } 129 | if(!mem.selective.isNull) { 130 | formattedWrite(app, " selective '%(%s, %)'", mem.selective.get()); 131 | } 132 | if(!mem.members.isNull) { 133 | formattedWrite(app, " members '%(%s, %)'" 134 | , mem.members.get().map!(m => m.toString())); 135 | } 136 | app.put("\""); 137 | return app.data; 138 | } 139 | 140 | struct Ast { 141 | Module[] modules; 142 | } 143 | 144 | Ast parse(string filename) { 145 | import std.file : readText; 146 | 147 | JSONValue jv = parseJSON(readText(filename)); 148 | return Ast(parseJson!(Module[])(jv)); 149 | } 150 | 151 | T parseJson(T)(JSONValue jv) { 152 | static if(isBasicType!T) { 153 | return jv.get!T(); 154 | } else static if(isSomeString!T) { 155 | return jv.get!string(); 156 | } else static if(isArray!T && !isSomeString!T) { 157 | enforce(jv.type == JSONType.array, format("Expected array not '%s'", 158 | jv.type)); 159 | T arr; 160 | alias ET = ElementEncodingType!T; 161 | //pragma(msg, T.stringof ~ " " ~ ET.stringof); 162 | foreach(it; jv.arrayNoRef()) { 163 | auto tmp = parseJson!ET(it); 164 | static if(is(ET == Member) || is(ET == Module)) { 165 | static if(is(typeof(tmp.name) : Nullable!F, F)) { 166 | if(tmp.name.isNull) { 167 | continue; 168 | } 169 | auto name = tmp.name.get; 170 | } else { 171 | auto name = tmp.name; 172 | } 173 | if(name.startsWith("__unittest_")) { 174 | continue; 175 | } 176 | } 177 | arr ~= tmp; 178 | } 179 | return arr; 180 | } else static if(is(T : Nullable!G, G)) { 181 | if(jv.type != JSONType.null_) { 182 | return nullable(parseJson!G(jv)); 183 | } else { 184 | return Nullable!(G).init; 185 | } 186 | } else static if(is(T == struct)) { 187 | enforce(jv.type == JSONType.object, format("Expected object '%s' not '%s'\n%s", 188 | T.stringof, jv.type, jv.toPrettyString())); 189 | T ret; 190 | 191 | string[] jsNames = jv.objectNoRef().keys().sort.array; 192 | string[] sNames = ([FieldNameTuple!T] ~ ["endchar", "endline", "char", "line"]) 193 | .map!(it => it.endsWith("_") ? it[0 .. $ - 1] : it) 194 | .array.sort.array; 195 | auto sd = setDifference(jsNames, sNames); 196 | if(!sd.empty) { 197 | writefln("%s", sd); 198 | } 199 | 200 | static foreach(mem; FieldNameTuple!T) {{ 201 | alias MT = typeof(__traits(getMember, T, mem)); 202 | //pragma(msg, T.stringof ~ " " ~ mem ~ " " ~ MT.stringof); 203 | 204 | enum memNoPostfix = mem.endsWith("_") ? mem[0 .. $ - 1] : mem; 205 | 206 | auto p = memNoPostfix in jv; 207 | static if(is(MT : Nullable!F, F)) { 208 | if(p !is null) { 209 | __traits(getMember, ret, mem) = parseJson!(F)(*p); 210 | } 211 | } else { 212 | enforce(p !is null, format("Couldn't find '%s'\n%s", mem, 213 | jv.toPrettyString())); 214 | __traits(getMember, ret, mem) = parseJson!MT(*p); 215 | } 216 | }} 217 | return ret; 218 | } 219 | } 220 | 221 | unittest { 222 | import std.file; 223 | 224 | foreach(f; dirEntries("testdirgen/", "*.json", SpanMode.depth)) { 225 | try { 226 | auto a = parse(f.name); 227 | } catch(Exception e) { 228 | assert(false, format("%s\n%s", f.name, e)); 229 | } 230 | } 231 | } 232 | -------------------------------------------------------------------------------- /source/dsemver/buildinterface.d: -------------------------------------------------------------------------------- 1 | module dsemver.buildinterface; 2 | 3 | import std.file; 4 | import std.stdio; 5 | import std.process; 6 | import std.format; 7 | import std.string; 8 | 9 | import dsemver.options; 10 | 11 | private enum dsemverDir = ".dsemver"; 12 | 13 | private string getDflags() { 14 | auto r = executeShell("dub describe --data=dflags"); 15 | return r.output.strip(); 16 | } 17 | 18 | private string jsonFile(string dfiles, string ver) { 19 | executeShell("dub clean"); 20 | const fn = format("%s/dsemver_%s.json", dsemverDir, ver); 21 | const s = "DFLAGS=\"%s -X -Xf=%s\" dub build" 22 | .format(dfiles, fn); 23 | writeln(s); 24 | auto rc = executeShell(s); 25 | if (rc.status) { 26 | throw new Exception("The project doesn't compile:\n" ~ rc.output); 27 | } 28 | return fn; 29 | } 30 | 31 | string buildInterface(string ver) { 32 | string oldCwd = getcwd(); 33 | scope(exit) { 34 | chdir(oldCwd); 35 | } 36 | chdir(getOptions().projectPath); 37 | 38 | if(!exists(dsemverDir)) { 39 | mkdir(dsemverDir); 40 | } 41 | 42 | const dflags = getDflags(); 43 | return getOptions().projectPath ~ "/" ~ jsonFile(dflags, ver); 44 | } 45 | -------------------------------------------------------------------------------- /source/dsemver/compare.d: -------------------------------------------------------------------------------- 1 | module dsemver.compare; 2 | 3 | import std.array : array, empty, front; 4 | import std.algorithm.searching; 5 | import std.algorithm.comparison : equal; 6 | import std.typecons : nullable, Nullable; 7 | import std.exception : enforce; 8 | import std.format; 9 | import std.stdio; 10 | 11 | import dsemver.ast; 12 | 13 | enum ResultValue { 14 | equal, 15 | minor, 16 | major 17 | } 18 | 19 | ResultValue combine(const ResultValue on, const ResultValue no) 20 | pure @safe 21 | { 22 | final switch(on) { 23 | case ResultValue.equal: 24 | final switch(no) { 25 | case ResultValue.equal: return ResultValue.equal; 26 | case ResultValue.minor: 27 | throw new Exception(format("%s %s", on, no)); 28 | case ResultValue.major: return ResultValue.minor; 29 | } 30 | case ResultValue.minor: assert(false); 31 | case ResultValue.major: 32 | final switch(no) { 33 | case ResultValue.equal: return on; 34 | case ResultValue.minor: 35 | throw new Exception(format("%s %s", on, no)); 36 | case ResultValue.major: return on; 37 | } 38 | } 39 | } 40 | 41 | struct Result { 42 | ResultValue value; 43 | string reason; 44 | } 45 | 46 | ResultValue summarize(const(Result)[] rslts) { 47 | ResultValue rv; 48 | foreach(ref r; rslts) { 49 | if(r.value > rv) { 50 | rv = r.value; 51 | } 52 | } 53 | return rv; 54 | } 55 | 56 | Nullable!(const(Module)) findModule(ref const(Ast) toFindIn 57 | , ref const(Module) mod) 58 | { 59 | auto f = mod.name.isNull() 60 | ? toFindIn.modules.find!(m => m.file == mod.file) 61 | : toFindIn.modules.find!(m => !m.name.isNull() 62 | && m.name.get() == mod.name.get()); 63 | 64 | return f.empty 65 | ? Nullable!(const(Module)).init 66 | : nullable(f.front); 67 | } 68 | 69 | Result[] compareOldNew(ref const(Ast) old, ref const(Ast) neu) { 70 | Result[] ret; 71 | foreach(ref mod; old.modules) { 72 | Nullable!(const(Module)) fMod = findModule(neu, mod); 73 | if(fMod.isNull()) { // Not found 74 | ret ~= Result(ResultValue.major, format( 75 | "module '%s' could no longer be found", mod.moduleToName())); 76 | continue; 77 | } else { // module name added 78 | auto fModNN = fMod.get(); 79 | if(!fModNN.name.isNull() && mod.name.isNull()) { 80 | ret ~= Result(ResultValue.major, format( 81 | "module '%s' added module name '%s'", mod.moduleToName() 82 | , fModNN.name.get())); 83 | continue; 84 | } else { // recurse into module 85 | foreach(ref mem; mod.members) { 86 | // we can ignore private members 87 | if(!mem.protection.isNull() 88 | && mem.protection.get() == "private") 89 | { 90 | continue; 91 | } 92 | Nullable!(const(Member)) fm = findMember(fModNN, mem); 93 | if(fm.isNull()) { 94 | ret ~= Result(ResultValue.major, format( 95 | "Ast Member '%s' of module '%s' couldn't be found" 96 | , mem.name 97 | , mod.moduleToName())); 98 | continue; 99 | } 100 | const memsRslt = compareOldNew(mem, fm.get() 101 | , [mod.moduleToName()]); 102 | ret ~= memsRslt; 103 | } 104 | } 105 | } 106 | } 107 | 108 | return ret; 109 | } 110 | 111 | Result[] compareOldNew(ref const(Member) old, ref const(Member) neu 112 | , string[] path) 113 | { 114 | Result[] ret; 115 | 116 | if(old.members.isNull()) { 117 | return ret; 118 | } 119 | 120 | Member[] emptyArray; 121 | foreach(ref mem; old.members.get(emptyArray)) { 122 | Nullable!(const(Member)) f = neu.findMember(mem); 123 | if(f.isNull()) { 124 | ret ~= Result(ResultValue.major, format( 125 | "%s of '%--(%s.%)' couldn't be found" 126 | , mem.toString(), path)); 127 | continue; 128 | } 129 | 130 | string[] np = path ~ mem.name; 131 | if(mem.members.isNull()) { 132 | continue; 133 | } 134 | foreach(ref const(Member) sub; mem.members.get(emptyArray)) { 135 | // we can ignore private members 136 | if(!sub.protection.isNull() && sub.protection.get() == "private") { 137 | continue; 138 | } 139 | Nullable!(const(Member)) fm = findMember(f.get(), sub); 140 | if(fm.isNull()) { 141 | ret ~= Result(ResultValue.major, format( 142 | "%s of '%--(%s.%)' couldn't be found" 143 | , sub.toString(), np)); 144 | continue; 145 | } 146 | const memsRslt = compareOldNew(sub, fm.get(), np); 147 | ret ~= memsRslt; 148 | } 149 | } 150 | 151 | return ret; 152 | } 153 | 154 | private bool areEqualImpl(T)(ref const(T) a, ref const(T) b) { 155 | import std.traits : isSomeString, isArray, FieldNameTuple, Unqual; 156 | import std.range : ElementEncodingType; 157 | 158 | static if(isSomeString!T) { 159 | return a == b; 160 | } else static if(isArray!T) { 161 | if(a.length != b.length) { 162 | return false; 163 | } else { 164 | alias ET = Unqual!(ElementEncodingType!T); 165 | static if(is(ET == string)) { 166 | return a.all!(i => canFind(b, i)); 167 | } else { 168 | return equal!((g,h) => areEqualImpl(g, h))(a, b); 169 | } 170 | } 171 | } else static if(is(T == long)) { 172 | return a == b; 173 | } else static if(is(T == Nullable!F, F)) { 174 | if(a.isNull() != b.isNull()) { 175 | return false; 176 | } else if(!a.isNull()) { 177 | return areEqualImpl(a.get(), b.get()); 178 | } else { 179 | return true; 180 | } 181 | } else static if(is(T == struct)) { 182 | static foreach(mem; FieldNameTuple!T) { 183 | if(mem != "members" 184 | && !areEqualImpl(__traits(getMember, a, mem) 185 | , __traits(getMember, b, mem)) 186 | ) 187 | { 188 | return false; 189 | } 190 | } 191 | return true; 192 | } else { 193 | static assert(false, "Unhandled type " ~ T.stringof); 194 | } 195 | } 196 | 197 | Nullable!(const(Member)) findMember(ref const(Member) toFindIn 198 | , ref const(Member) mem) 199 | { 200 | import std.range : isForwardRange; 201 | 202 | if(toFindIn.members.isNull()) { 203 | return Nullable!(const(Member)).init; 204 | } 205 | 206 | auto n = toFindIn.members.get().find!(a => a.name == mem.name)().array; 207 | auto f = n.find!(areEqualImpl)(mem); 208 | return f.empty 209 | ? Nullable!(const(Member)).init 210 | : nullable(f.front); 211 | } 212 | 213 | Nullable!(const(Member)) findMember(ref const(Module) toFindIn 214 | , ref const(Member) mem) 215 | { 216 | import std.range : isForwardRange; 217 | 218 | static assert(isForwardRange!(typeof(cast()toFindIn.members))); 219 | auto n = toFindIn.members.find!(a => a.name == mem.name)().array; 220 | auto f = n.find!(areEqualImpl)(mem); 221 | return f.empty 222 | ? Nullable!(const(Member)).init 223 | : nullable(f.front); 224 | } 225 | 226 | unittest { 227 | import std.file : dirEntries, SpanMode, readText; 228 | import std.algorithm.searching : canFind, startsWith, find; 229 | import std.algorithm.iteration : splitter, map; 230 | import std.string : strip; 231 | 232 | struct ExpectedResult { 233 | ResultValue old; 234 | ResultValue neu; 235 | ResultValue oldC; 236 | ResultValue neuC; 237 | } 238 | 239 | ExpectedResult getExpected(string fn) { 240 | import std.conv : to; 241 | enum sstr = "// Result"; 242 | string t = readText(fn); 243 | auto s = t.splitter("\n").find!(l => l.startsWith(sstr)); 244 | assert(!s.empty, fn); 245 | 246 | string[] ws = s.front[sstr.length .. $].strip("()") 247 | .splitter(",") 248 | .map!(r => r.strip()) 249 | .array; 250 | 251 | enforce(ws.length == 4, fn); 252 | return ExpectedResult 253 | ( ws[0].to!ResultValue() 254 | , ws[1].to!ResultValue() 255 | , ws[2].to!ResultValue() 256 | , ws[3].to!ResultValue() 257 | ); 258 | } 259 | 260 | enum o = "old.d.json"; 261 | foreach(fn; dirEntries("testdirgen/", "*old.d.json", SpanMode.depth)) { 262 | auto a = parse(fn.name); 263 | auto b = parse(fn.name[0 .. $ - o.length] ~ "new.d.json"); 264 | 265 | string expected = fn.name[0 .. $ - 5]; 266 | const ExpectedResult er = getExpected(expected); 267 | 268 | auto cmpsON = compareOldNew(a, b); 269 | auto sON = summarize(cmpsON); 270 | assert(sON == er.old, format("\nwhich: old new\nfile: %s\ngot: %s\nexp: %s", fn.name 271 | , sON, er.old)); 272 | 273 | auto cmpsNO = compareOldNew(b, a); 274 | auto sNO = summarize(cmpsNO); 275 | assert(sNO == er.neu, format("\nwhich: neu old\nfile: %s\ngot: %s\nexp: %s", fn.name 276 | , sNO, er.neu)); 277 | 278 | auto compAB = combine(sON, sNO); 279 | assert(compAB == er.oldC, format( 280 | "\nwhich: old new comb\nfile: %s\ngot: %s\nexp: %s", fn.name 281 | , compAB, er.oldC)); 282 | 283 | auto compBA = combine(sNO, sON); 284 | assert(compBA == er.neuC, format( 285 | "\nwhich: new old comb\nfile: %s\ngot: %s\nexp: %s", fn.name 286 | , compBA, er.neuC)); 287 | } 288 | } 289 | -------------------------------------------------------------------------------- /source/dsemver/git.d: -------------------------------------------------------------------------------- 1 | module dsemver.git; 2 | 3 | import std.array : array; 4 | import std.process : executeShell; 5 | import std.datetime; 6 | import std.algorithm.iteration : filter, map, splitter; 7 | import std.algorithm.searching : any, canFind, startsWith; 8 | import std.algorithm.sorting : sort; 9 | import std.string : stripLeft; 10 | import std.typecons : nullable, Nullable, tuple; 11 | import std.exception : enforce; 12 | import std.file : getcwd, chdir; 13 | import std.format : format; 14 | 15 | import dsemver.semver; 16 | 17 | struct SheelRslt { 18 | const int rslt; 19 | string output; 20 | } 21 | 22 | private SheelRslt execute(string projectPath, string command) { 23 | const cwd = getcwd(); 24 | chdir(projectPath); 25 | scope(exit) { 26 | chdir(cwd); 27 | } 28 | 29 | const r = executeShell(command); 30 | enforce(r.status == 0, format("'%s' failed in '%s'" 31 | ~ "\nWith return code '%s' and msg %s" 32 | , command, projectPath, r.status, r.output)); 33 | return SheelRslt(r.status, r.output); 34 | } 35 | 36 | bool isClean(string projectPath) { 37 | enum cmd = "git status --porcelain"; 38 | 39 | const r = execute(projectPath, cmd); 40 | 41 | const m = r.output.splitter("\n").map!(l => l.stripLeft) 42 | .any!(l => l.startsWith("M ")); 43 | 44 | return !m; 45 | } 46 | 47 | private Nullable!SemVer toSemVer(string sv) { 48 | try { 49 | SemVer ret = parseSemVer(sv); 50 | return nullable(ret); 51 | } catch(Exception e) { 52 | return Nullable!(SemVer).init; 53 | } 54 | } 55 | 56 | struct RefSemVer { 57 | string gitRef; 58 | SemVer semver; 59 | } 60 | 61 | RefSemVer[] getTags(string projectPath) { 62 | enum cmd = "git for-each-ref --sort=creatordate --format '%(refname)' refs/tags"; 63 | const r = execute(projectPath, cmd); 64 | 65 | enum prefix = "refs/tags/"; 66 | 67 | SemVer dummy; 68 | 69 | return r.output.splitter("\n").filter!(l => l.startsWith(prefix)) 70 | .map!(v => tuple(v, toSemVer(v[prefix.length .. $]))) 71 | .filter!(v => !v[1].isNull()) 72 | .map!(v => RefSemVer(v[0], v[1].get())) 73 | .array 74 | .sort!((a,b) => a.semver > b.semver) 75 | .array; 76 | } 77 | 78 | void checkoutRef(string projectPath, string tag) { 79 | const cmd = format("git checkout %s", tag); 80 | const r = execute(projectPath, cmd); 81 | } 82 | -------------------------------------------------------------------------------- /source/dsemver/options.d: -------------------------------------------------------------------------------- 1 | module dsemver.options; 2 | 3 | import std.typecons : Nullable, nullable, Tuple, tuple; 4 | import args; 5 | 6 | struct Options { 7 | //@Arg('p', Optional.no, 8 | @Arg('p', "The path to the project the SemVer should be calculated for") 9 | string projectPath; 10 | 11 | @Arg('o') 12 | string old; 13 | 14 | @Arg('n') 15 | string neu; 16 | 17 | @Arg('t') 18 | string testParse; 19 | 20 | @Arg('l', "Compute the interface of the latest git tag as reference") 21 | bool buildLastestTag; 22 | 23 | @Arg('c', "Compute the next version number") 24 | bool buildNextSemVer; 25 | 26 | @Arg('v', "Enable verbose output") 27 | bool verbose; 28 | } 29 | 30 | ref const(Options) getOptions() { 31 | return getWritableOptions(); 32 | } 33 | 34 | ref Options getWritableOptions() { 35 | static Options ret; 36 | return ret; 37 | } 38 | 39 | void getOptOptions(ref string[] args) { 40 | import core.stdc.stdlib : exit; 41 | const len = args.length; 42 | bool helpWanted = parseArgsWithConfigFile(getWritableOptions(), args); 43 | if(helpWanted || len == 1) { 44 | printArgsHelp(getOptions(), 45 | ` 46 | 47 | dsemver lets the computer compute the SemVer of your dlang software. 48 | 49 | If this is the first time running dsemver on your project you like want to 50 | run 51 | 52 | ''' 53 | $ ./dsemver -- -p PATH_TO_PROJECT -l 54 | ''' 55 | 56 | To compute the public interface of your latest git tag that looked it a SemVer 57 | than you can run 58 | 59 | ''' 60 | $ ./dsemver -- -p PATH_TO_PROJECT -c 61 | ''' 62 | 63 | To compute the next SemVer of your project.`); 64 | exit(0); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /source/dsemver/semver.d: -------------------------------------------------------------------------------- 1 | module dsemver.semver; 2 | 3 | import std.ascii : isAlpha, isDigit; 4 | import std.array : array, empty, front; 5 | import std.typecons : nullable, Nullable; 6 | import std.algorithm.iteration : each, filter, map, splitter; 7 | import std.algorithm.searching : all, countUntil; 8 | import std.conv : to; 9 | import std.format : format; 10 | import std.exception : enforce, basicExceptionCtors, assertThrown, 11 | assertNotThrown; 12 | import std.utf : byChar, byUTF; 13 | import std.range : popFront; 14 | 15 | @safe pure: 16 | 17 | struct SemVer { 18 | @safe: 19 | 20 | uint major; 21 | uint minor; 22 | uint patch; 23 | 24 | string[] preRelease; 25 | string[] buildIdentifier; 26 | 27 | static immutable(SemVer) MinRelease = SemVer(0, 0, 0); 28 | static immutable(SemVer) MaxRelease = SemVer(uint.max, uint.max, uint.max); 29 | 30 | bool opEquals(const(SemVer) other) scope const nothrow pure @nogc { 31 | return compare(this, other) == 0; 32 | } 33 | 34 | int opCmp(const(SemVer) other) scope const nothrow pure @nogc { 35 | return compare(this, other); 36 | } 37 | 38 | size_t toHash() scope const nothrow @nogc pure { 39 | size_t hash = this.major.hashOf(); 40 | hash = this.minor.hashOf(hash); 41 | hash = this.patch.hashOf(hash); 42 | this.preRelease.each!(it => hash = it.hashOf(hash)); 43 | this.buildIdentifier.each!(it => hash = it.hashOf(hash)); 44 | return hash; 45 | } 46 | 47 | @property SemVer dup() const pure { 48 | auto ret = SemVer(this.major, this.minor, this.patch, 49 | this.preRelease.dup(), this.buildIdentifier.dup()); 50 | return ret; 51 | } 52 | 53 | static SemVer max() pure { 54 | return SemVer(uint.max, uint.max, uint.max); 55 | } 56 | 57 | static SemVer min() pure { 58 | return SemVer(uint.min, uint.min, uint.min); 59 | } 60 | 61 | string toString() const @safe pure { 62 | import std.array : appender, empty; 63 | import std.format : format; 64 | string ret = format("%s.%s.%s", this.major, this.minor, this.patch); 65 | if(!this.preRelease.empty) { 66 | ret ~= format("-%-(%s.%)", this.preRelease); 67 | } 68 | if(!this.buildIdentifier.empty) { 69 | ret ~= format("+%-(%s.%)", this.buildIdentifier); 70 | } 71 | return ret; 72 | } 73 | } 74 | 75 | int compare(const(SemVer) a, const(SemVer) b) nothrow pure @nogc { 76 | if(a.major != b.major) { 77 | return a.major < b.major ? -1 : 1; 78 | } 79 | 80 | if(a.minor != b.minor) { 81 | return a.minor < b.minor ? -1 : 1; 82 | } 83 | 84 | if(a.patch != b.patch) { 85 | return a.patch < b.patch ? -1 : 1; 86 | } 87 | 88 | if(a.preRelease.empty != b.preRelease.empty) { 89 | return a.preRelease.empty ? 1 : -1; 90 | } 91 | 92 | size_t idx; 93 | while(idx < a.preRelease.length && idx < b.preRelease.length) { 94 | string aStr = a.preRelease[idx]; 95 | string bStr = b.preRelease[idx]; 96 | if(aStr.length != bStr.length && aStr.isAllNumImpl && bStr.isAllNumImpl) { 97 | return aStr.length < bStr.length ? -1 : 1; 98 | } 99 | if(aStr != bStr) { 100 | return aStr < bStr ? -1 : 1; 101 | } 102 | ++idx; 103 | } 104 | 105 | if(idx == a.preRelease.length && idx == b.preRelease.length) { 106 | return 0; 107 | } 108 | 109 | return idx < a.preRelease.length ? 1 : -1; 110 | } 111 | 112 | private bool isAllNumImpl(string s) nothrow pure @nogc { 113 | import std.utf : byUTF; 114 | import std.ascii : isDigit; 115 | import std.algorithm.searching : all; 116 | return s.byUTF!char().all!isDigit(); 117 | } 118 | 119 | Nullable!uint isAllNum(string s) nothrow pure { 120 | const bool allNum = s.isAllNumImpl; 121 | if(allNum) { 122 | try { 123 | return nullable(to!uint(s)); 124 | } catch(Exception e) { 125 | assert(false, s); 126 | } 127 | } 128 | return Nullable!(uint).init; 129 | } 130 | 131 | unittest { 132 | import std.format : format; 133 | auto i = isAllNum("hello world"); 134 | assert(i.isNull()); 135 | 136 | i = isAllNum("12354"); 137 | assert(!i.isNull()); 138 | assert(i.get() == 12354); 139 | 140 | i = isAllNum("0002354"); 141 | assert(!i.isNull()); 142 | assert(i.get() == 2354); 143 | } 144 | 145 | SemVer parseSemVer(string input) { 146 | SemVer ret; 147 | 148 | char[] inputRange = to!(char[])(input); 149 | 150 | if(!inputRange.empty && inputRange.front == 'v') { 151 | inputRange.popFront(); 152 | } 153 | 154 | ret.major = splitOutNumber!isDot("Major", "first", inputRange); 155 | ret.minor = splitOutNumber!isDot("Minor", "second", inputRange); 156 | ret.patch = toNum("Patch", dropUntilPredOrEmpty!isPlusOrMinus(inputRange)); 157 | if(!inputRange.empty && inputRange[0].isMinus()) { 158 | inputRange.popFront(); 159 | ret.preRelease = splitter(dropUntilPredOrEmpty!isPlus(inputRange), '.') 160 | .map!(it => checkNotEmpty(it)) 161 | .map!(it => checkASCII(it)) 162 | .map!(it => to!string(it)) 163 | .array; 164 | } 165 | if(!inputRange.empty) { 166 | enforce!InvalidSeperator(inputRange[0] == '+', 167 | format("Expected a '+' got '%s'", inputRange[0])); 168 | inputRange.popFront(); 169 | ret.buildIdentifier = 170 | splitter(dropUntilPredOrEmpty!isFalse(inputRange), '.') 171 | .map!(it => checkNotEmpty(it)) 172 | .map!(it => checkASCII(it)) 173 | .map!(it => to!string(it)) 174 | .array; 175 | } 176 | enforce!InputNotEmpty(inputRange.empty, 177 | format("Surprisingly input '%s' left", inputRange)); 178 | return ret; 179 | } 180 | 181 | char[] checkNotEmpty(char[] cs) { 182 | enforce!EmptyIdentifier(!cs.empty, 183 | "Build or prerelease identifier must not be empty"); 184 | return cs; 185 | } 186 | 187 | char[] checkASCII(char[] cs) { 188 | foreach(it; cs.byUTF!char()) { 189 | enforce!NonAsciiChar(isDigit(it) || isAlpha(it) || it == '-', format( 190 | "Non ASCII character '%s' surprisingly found input '%s'", 191 | it, cs 192 | )); 193 | } 194 | return cs; 195 | } 196 | 197 | uint toNum(string numName, char[] input) { 198 | enforce!OnlyDigitAllowed(all!(isDigit)(input.byUTF!char()), 199 | format("%s range must solely consist of digits not '%s'", 200 | numName, input)); 201 | return to!uint(input); 202 | } 203 | 204 | uint splitOutNumber(alias pred)(const string numName, const string dotName, 205 | ref char[] input) 206 | { 207 | const ptrdiff_t dot = input.byUTF!char().countUntil!pred(); 208 | enforce!InvalidSeperator(dot != -1, 209 | format("Couldn't find the %s dot in '%s'", dotName, input)); 210 | char[] num = input[0 .. dot]; 211 | const uint ret = toNum(numName, num); 212 | enforce!EmptyInput(input.length > dot + 1, 213 | format("Input '%s' ended surprisingly after %s version", 214 | input, numName)); 215 | input = input[dot + 1 .. $]; 216 | return ret; 217 | } 218 | 219 | char[] dropUntilPredOrEmpty(alias pred)(ref char[] input) @nogc nothrow pure { 220 | size_t pos; 221 | while(pos < input.length && !pred(input[pos])) { 222 | ++pos; 223 | } 224 | char[] ret = input[0 .. pos]; 225 | input = input[pos .. $]; 226 | return ret; 227 | } 228 | 229 | bool isFalse(char c) @nogc nothrow pure { 230 | return false; 231 | } 232 | 233 | bool isDot(char c) @nogc nothrow pure { 234 | return c == '.'; 235 | } 236 | 237 | bool isMinus(char c) @nogc nothrow pure { 238 | return c == '-'; 239 | } 240 | 241 | bool isPlus(char c) @nogc nothrow pure { 242 | return c == '+'; 243 | } 244 | 245 | bool isPlusOrMinus(char c) @nogc nothrow pure { 246 | return isPlus(c) || isMinus(c); 247 | } 248 | 249 | class SemVerParseException : Exception { 250 | mixin basicExceptionCtors; 251 | } 252 | 253 | class NonAsciiChar : SemVerParseException { 254 | mixin basicExceptionCtors; 255 | } 256 | 257 | class EmptyInput : SemVerParseException { 258 | mixin basicExceptionCtors; 259 | } 260 | 261 | class OnlyDigitAllowed : SemVerParseException { 262 | mixin basicExceptionCtors; 263 | } 264 | 265 | class InvalidSeperator : SemVerParseException { 266 | mixin basicExceptionCtors; 267 | } 268 | 269 | class InputNotEmpty : SemVerParseException { 270 | mixin basicExceptionCtors; 271 | } 272 | 273 | class EmptyIdentifier : SemVerParseException { 274 | mixin basicExceptionCtors; 275 | } 276 | 277 | private struct StrSV { 278 | string str; 279 | SemVer sv; 280 | } 281 | 282 | unittest { 283 | StrSV[] tests = [ 284 | StrSV("0.0.4", SemVer(0,0,4)), 285 | StrSV("1.2.3", SemVer(1,2,3)), 286 | StrSV("10.20.30", SemVer(10,20,30)), 287 | StrSV("1.1.2-prerelease+meta", SemVer(1,1,2,["prerelease"], ["meta"])), 288 | StrSV("1.1.2+meta", SemVer(1,1,2,[],["meta"])), 289 | StrSV("1.0.0-alpha", SemVer(1,0,0,["alpha"],[])), 290 | StrSV("1.0.0-beta", SemVer(1,0,0,["beta"],[])), 291 | StrSV("1.0.0-alpha.beta", SemVer(1,0,0,["alpha", "beta"],[])), 292 | StrSV("1.0.0-alpha.beta.1", SemVer(1,0,0,["alpha", "beta", "1"],[])), 293 | StrSV("1.0.0-alpha.1", SemVer(1,0,0,["alpha", "1"],[])), 294 | StrSV("1.0.0-alpha0.valid", SemVer(1,0,0,["alpha0", "valid"],[])), 295 | StrSV("1.0.0-alpha.0valid", SemVer(1,0,0,["alpha", "0valid"],[])), 296 | StrSV("1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay", 297 | SemVer(1,0,0,["alpha-a", "b-c-somethinglong"], 298 | ["build","1-aef","1-its-okay"])), 299 | StrSV("1.0.0-rc.1+build.1", SemVer(1,0,0,["rc", "1"],["build","1"])), 300 | StrSV("2.0.0-rc.1+build.123", SemVer(2,0,0,["rc", "1"],["build", "123"])), 301 | StrSV("1.2.3-beta", SemVer(1,2,3,["beta"],[])), 302 | StrSV("10.2.3-DEV-SNAPSHOT", SemVer(10,2,3,["DEV-SNAPSHOT"],[])), 303 | StrSV("1.2.3-SNAPSHOT-123", SemVer(1,2,3,["SNAPSHOT-123"],[])), 304 | StrSV("1.0.0", SemVer(1,0,0,[],[])), 305 | StrSV("2.0.0", SemVer(2,0,0,[],[])), 306 | StrSV("1.1.7", SemVer(1,1,7,[],[])), 307 | StrSV("2.0.0+build.1848", SemVer(2,0,0,[],["build","1848"])), 308 | StrSV("2.0.1-alpha.1227", SemVer(2,0,1,["alpha", "1227"],[])), 309 | StrSV("1.0.0-alpha+beta", SemVer(1,0,0,["alpha"],["beta"])), 310 | StrSV("1.0.0-0A.is.legal", SemVer(1,0,0,["0A", "is", "legal"],[])), 311 | StrSV("1.1.2+meta-valid", SemVer(1,1,2, [], ["meta-valid"])), 312 | 313 | StrSV("v0.0.4", SemVer(0,0,4)), 314 | StrSV("v1.2.3", SemVer(1,2,3)), 315 | StrSV("v10.20.30", SemVer(10,20,30)), 316 | StrSV("v1.1.2-prerelease+meta", SemVer(1,1,2,["prerelease"], ["meta"])), 317 | StrSV("v1.1.2+meta", SemVer(1,1,2,[],["meta"])), 318 | StrSV("v1.0.0-alpha", SemVer(1,0,0,["alpha"],[])), 319 | StrSV("v1.0.0-beta", SemVer(1,0,0,["beta"],[])), 320 | StrSV("v1.0.0-alpha.beta", SemVer(1,0,0,["alpha", "beta"],[])), 321 | StrSV("v1.0.0-alpha.beta.1", SemVer(1,0,0,["alpha", "beta", "1"],[])), 322 | StrSV("v1.0.0-alpha.1", SemVer(1,0,0,["alpha", "1"],[])), 323 | StrSV("v1.0.0-alpha0.valid", SemVer(1,0,0,["alpha0", "valid"],[])), 324 | StrSV("v1.0.0-alpha.0valid", SemVer(1,0,0,["alpha", "0valid"],[])), 325 | StrSV("v1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay", 326 | SemVer(1,0,0,["alpha-a", "b-c-somethinglong"], 327 | ["build","1-aef","1-its-okay"])), 328 | StrSV("v1.0.0-rc.1+build.1", SemVer(1,0,0,["rc", "1"],["build","1"])), 329 | StrSV("v2.0.0-rc.1+build.123", SemVer(2,0,0,["rc", "1"],["build", "123"])), 330 | StrSV("v1.2.3-beta", SemVer(1,2,3,["beta"],[])), 331 | StrSV("v10.2.3-DEV-SNAPSHOT", SemVer(10,2,3,["DEV-SNAPSHOT"],[])), 332 | StrSV("v1.2.3-SNAPSHOT-123", SemVer(1,2,3,["SNAPSHOT-123"],[])), 333 | StrSV("v1.0.0", SemVer(1,0,0,[],[])), 334 | StrSV("v2.0.0", SemVer(2,0,0,[],[])), 335 | StrSV("v1.1.7", SemVer(1,1,7,[],[])), 336 | StrSV("v2.0.0+build.1848", SemVer(2,0,0,[],["build","1848"])), 337 | StrSV("v2.0.1-alpha.1227", SemVer(2,0,1,["alpha", "1227"],[])), 338 | StrSV("v1.0.0-alpha+beta", SemVer(1,0,0,["alpha"],["beta"])), 339 | StrSV("v1.0.0-0A.is.legal", SemVer(1,0,0,["0A", "is", "legal"],[])), 340 | StrSV("v1.1.2+meta-valid", SemVer(1,1,2, [], ["meta-valid"])) 341 | ]; 342 | 343 | foreach(test; tests) { 344 | SemVer sv = assertNotThrown(parseSemVer(test.str), 345 | format("An exception was thrown while parsing '%s'", test.str)); 346 | assert(sv == test.sv, format("\ngot: %s\nexp: %s", sv, test.sv)); 347 | } 348 | } 349 | 350 | unittest { 351 | assertThrown!InvalidSeperator(parseSemVer("Hello World")); 352 | assertThrown!OnlyDigitAllowed(parseSemVer("Hello World.")); 353 | assertThrown!OnlyDigitAllowed(parseSemVer("1.2.332a")); 354 | assertThrown!NonAsciiChar(parseSemVer("1.2.3+ßßßßääü")); 355 | assertThrown!EmptyInput(parseSemVer("1.2.")); 356 | assertThrown!EmptyInput(parseSemVer("1.")); 357 | assertThrown!EmptyIdentifier(parseSemVer("2.0.1-alpha.1227..")); 358 | assertThrown!EmptyIdentifier(parseSemVer("2.0.1+alpha.1227..")); 359 | } 360 | -------------------------------------------------------------------------------- /testfilegen.d: -------------------------------------------------------------------------------- 1 | enum temp = 2 | `module %s; 3 | 4 | %s`; 5 | 6 | void main() { 7 | import std.array : array; 8 | import std.file : dirEntries, SpanMode, readText, isDir, mkdir, exists, 9 | rmdirRecurse; 10 | import std.process : executeShell; 11 | import std.algorithm : splitter; 12 | import std.stdio; 13 | import std.format; 14 | 15 | enum dir = "testdirgen"; 16 | 17 | if(exists(dir)) { 18 | rmdirRecurse(dir); 19 | } 20 | mkdir(dir); 21 | 22 | enum testfiles = "testfiles/"; 23 | 24 | foreach(de; dirEntries(testfiles, SpanMode.depth).array) { 25 | string[] t = readText(de.name).splitter( 26 | `// 27 | // SPLIT_HERE 28 | //`) 29 | .array; 30 | assert(t.length == 2, de.name); 31 | 32 | foreach(idx, it; ["_old", "_new"]) { 33 | string fn = format("%s%s.d", de.name[testfiles.length .. $], it); 34 | string dfn = format("%s/%s", dir, fn); 35 | { 36 | auto f = File(dfn, "w"); 37 | f.write(format(temp, de.name[testfiles.length .. $ - 2], t[idx])); 38 | } 39 | string dmd = format("dmd -od=%s -X %s -Xf=%s/%s.json", dir, dfn, dir, fn); 40 | executeShell(dmd); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /testfiles/class1.d: -------------------------------------------------------------------------------- 1 | // Result(major, major, major, major) 2 | 3 | class Foo { 4 | class Bar { 5 | int a; 6 | } 7 | int a; 8 | } 9 | 10 | // 11 | // SPLIT_HERE 12 | // 13 | 14 | class Foo { 15 | class Bar { 16 | string a; 17 | } 18 | int a; 19 | } 20 | -------------------------------------------------------------------------------- /testfiles/function1.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | int fun(float d) { 4 | return cast(int)d; 5 | } 6 | 7 | // 8 | // SPLIT_HERE 9 | // 10 | 11 | int fun(double d) { 12 | return cast(int)d; 13 | } 14 | -------------------------------------------------------------------------------- /testfiles/function2.d: -------------------------------------------------------------------------------- 1 | // Result(major,equal,major,minor) 2 | 3 | int fun(int a) { 4 | return a; 5 | } 6 | 7 | int fun(double a) { 8 | return cast(int)a; 9 | } 10 | 11 | int fun(string s) { 12 | return cast(int)s.length; 13 | } 14 | 15 | // 16 | // SPLIT_HERE 17 | // 18 | 19 | int fun(int a) { 20 | return a; 21 | } 22 | 23 | int fun(string s) { 24 | return cast(int)s.length; 25 | } 26 | -------------------------------------------------------------------------------- /testfiles/function3.d: -------------------------------------------------------------------------------- 1 | // Result(equal,equal,equal,equal) 2 | 3 | private int fun() { 4 | return 0; 5 | } 6 | 7 | // 8 | // SPLIT_HERE 9 | // 10 | 11 | private int bar() { 12 | return 0; 13 | } 14 | -------------------------------------------------------------------------------- /testfiles/interface1.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | interface Foo { 4 | void Bla(Exception); 5 | } 6 | 7 | // 8 | // SPLIT_HERE 9 | // 10 | 11 | interface Foo { 12 | void Blup(Exception f); 13 | } 14 | -------------------------------------------------------------------------------- /testfiles/interface2.d: -------------------------------------------------------------------------------- 1 | // Result(equal,equal,equal,equal) 2 | 3 | interface Foo { 4 | void Bla(Exception); 5 | } 6 | 7 | // 8 | // SPLIT_HERE 9 | // 10 | 11 | interface Foo { 12 | void Bla(Exception); 13 | } 14 | -------------------------------------------------------------------------------- /testfiles/struct1.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | struct Foo { 4 | int bar; 5 | } 6 | 7 | // 8 | // SPLIT_HERE 9 | // 10 | 11 | struct Foo { 12 | int bar2; 13 | } 14 | -------------------------------------------------------------------------------- /testfiles/struct2.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | struct Foo { 4 | struct Bar { 5 | int a; 6 | } 7 | int c; 8 | } 9 | 10 | // 11 | // SPLIT_HERE 12 | // 13 | 14 | struct Foo { 15 | struct Bar { 16 | double a; 17 | } 18 | int c; 19 | } 20 | -------------------------------------------------------------------------------- /testfiles/struct3.d: -------------------------------------------------------------------------------- 1 | // Result(equal,major,minor,major) 2 | 3 | struct Foo { 4 | struct Bar { 5 | int a; 6 | } 7 | int c; 8 | } 9 | 10 | // 11 | // SPLIT_HERE 12 | // 13 | 14 | struct Foo { 15 | struct Bar { 16 | int a; 17 | 18 | int b; 19 | } 20 | int c; 21 | } 22 | -------------------------------------------------------------------------------- /testfiles/template1.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | template Foo(T) { 4 | int fun(T t) { 5 | return cast(int)t; 6 | } 7 | } 8 | 9 | // 10 | // SPLIT_HERE 11 | // 12 | 13 | template Foo(T) { 14 | int fun(T t, int foo) { 15 | return cast(int)foo; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /testfiles/template2.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | int fun(T)(T t) { 4 | return cast(int)t; 5 | } 6 | 7 | // 8 | // SPLIT_HERE 9 | // 10 | 11 | T fun(T)(T t) { 12 | return t; 13 | } 14 | -------------------------------------------------------------------------------- /testfiles/template3.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | template Foo(T) { 4 | T var; 5 | } 6 | 7 | // 8 | // SPLIT_HERE 9 | // 10 | 11 | template Foo(T) { 12 | const(T) var; 13 | } 14 | -------------------------------------------------------------------------------- /testfiles/union1.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | union Union { 4 | string a; 5 | double b; 6 | } 7 | 8 | // 9 | // SPLIT_HERE 10 | // 11 | 12 | union Union { 13 | string a; 14 | int b; 15 | } 16 | -------------------------------------------------------------------------------- /testfiles/variable1.d: -------------------------------------------------------------------------------- 1 | // Result(major,major,major,major) 2 | 3 | int a; 4 | 5 | // 6 | // SPLIT_HERE 7 | // 8 | 9 | int b; 10 | --------------------------------------------------------------------------------