importsort-d

Sort and format imports in DLang
Log | Files | Refs | README

commit 9acb077b41c59053143f0ca2394249fbfb678303
parent 678425f35dfc8a44d1a74a36dc20dbed688cd1d8
Author: Friedel Schoen <[email protected]>
Date:   Tue, 18 Oct 2022 22:51:22 +0200

implementing --recursive, --keep, --binding

Diffstat:
MREADME.md | 54+++++++++++++++++++++++++++++++++++++++++++-----------
Aassets/help.txt | 16++++++++++++++++
Mdub.sdl | 4++--
Msrc/main.d | 252+++++++++++++++++++++++++++++++++++++++++++++++++------------------------------
4 files changed, 219 insertions(+), 107 deletions(-)

diff --git a/README.md b/README.md @@ -6,12 +6,11 @@ ## Installation -### Prerequirements +## Prerequisite - [`dub`](https://dub.pm/) -- [`dmd`](https://dlang.org/) -### Building +### Building from HEAD Get the repository with `git` and compile everything with `dub` ``` @@ -22,29 +21,62 @@ $ dub build If everything went fine, there should be a binary at `bin/importsort-d`. -Copy this into a directory included in `$PATH` (`/usr/bin` for example) to make this command working global. +Copy this into a directory included in `$PATH` (`/usr/bin` for example) to make this command work globally. -## Usage +### Building with DUB ``` -$ importsort-d [--inline [--keep]] [--out <output>] [--original] [--special] [<input>] +$ dub fetch importsort-d +$ dub run importsort-d -- --help ``` -> `input` may be omitted or set to `-` to read from STDIN +This won't install the command globally, you always have to run `dub run importsort-d <args>` + +## Usage + +``` +$ importsort-d [-h] [-v] [-r] [-i] [-o <out>] [-k] [-a] [-r] <input...> +``` +`input` may be omitted or set to `-` to read from STDIN | option | description | | --------------------- | ---------------------------------------------- | -| `-i, --inline` | changes the input | +| `-h, --help` | prints a help message | +| `-v, --verbose` | prints useful debug messages | +| | | | `-k, --keep` | keeps the line as-is instead of formatting | +| `-a, --attribute` | public and static imports first | +| `-b, --binding` | sorts by binding rather then the original | +| | | +| `-r, --recursive` | recursively search in directories | +| `-i, --inline` | changes the input | | `-o, --output <path>` | writes to `path` rather then writing to STDOUT | -| `-r, --original` | sorts by original rather then the binding | -| `-s, --special` | public and static imports first | +## TODO's + +- [x] recursive searching (`v0.2.0`) +- [ ] watch-mode (struggling with save-timings - can clear files) + - you can add importsort-d into your onSave-hooks (e. g. [Run on Save](https://marketplace.visualstudio.com/items?itemName=emeraldwalk.RunOnSave) on VSCode) +- [ ] support multiple imports in one line (demilited by `;`) +- [ ] stripping unused imports (maybe) + +## Changelog + +### `v0.1.0` +- the very first version +- not a lot is implemented + +### `v0.2.0` +- added `--recursive` (see above) +- option `--keep` becomes disabling formatting +- option `--inline` doen't copy the original but creates a `*.new` and renames it afterwards +- option `--original` becomes `--binding` and sorts by original by default +- refactoring code ## License This whole project is licensed under the beautiful terms of the `zlib-license`. -Further information [here](/LICENSE) +Further information [here](LICENSE) > made with love and a lot of cat memes \ No newline at end of file diff --git a/assets/help.txt b/assets/help.txt @@ -0,0 +1,16 @@ +{binary} v{version} + +Usage: {binary} [-h] [-v] [-r] [-i] [-o <out>] [-k] [-a] [-r] <input...> + <input> can be set to '-' to read from stdin +` +Options: + -h, --help .......... prints this message + -v, --verbose ....... prints useful messages + + -k, --keep .......... keeps the line as-is instead of formatting + -a, --attribute ..... public and static imports first + -b, --binding ....... sorts by binding rather then the original + + -r, --recursive ..... recursively search in directories + -i, --inline ........ writes to the input + -o, --output <path> . writes to `path` instead of stdout diff --git a/dub.sdl b/dub.sdl @@ -4,6 +4,7 @@ authors "Friedel Schoen" copyright "Copyright © 2022, Friedel Schoen" license "zlib" +dflags "-Jassets" targetType "executable" targetPath "bin" -targetName "importsort-d" -\ No newline at end of file +targetName "importsort-d" diff --git a/src/main.d b/src/main.d @@ -1,30 +1,37 @@ // (c) 2022 Friedel Schon <[email protected]> -module importsort; +module test.main; import core.stdc.stdlib : exit; -import std.algorithm : findSplit, map, sort; -import std.array : array; -import std.file : exists, isFile, rename; +import core.thread : Thread; +import core.time : Duration = dur; +import std.algorithm : canFind, each, endsWith, filter, findSplit, map, sort; +import std.array : array, replace; +import std.conv : ConvException, parse; +import std.datetime : SysTime; +import std.file : DirEntry, SpanMode, dirEntries, exists, isDir, isFile, rename, timeLastModified; +import std.functional : unaryFun; +import std.range : ElementType, empty; import std.regex : ctRegex, matchFirst; import std.stdio : File, stderr, stdin, stdout; import std.string : format, indexOf, split, strip, stripLeft; +import std.traits : isIterable; import std.typecons : Tuple, Yes, tuple; struct Identifier { - bool byOriginal; + bool byBinding; string original; - string alias_; + string binding; string sortBy() { - if (byOriginal) - return original; + if (byBinding) + return hasBinding ? binding : original; else - return hasAlias ? alias_ : original; + return original; } - bool hasAlias() { - return alias_ != null; + bool hasBinding() { + return binding != null; } } @@ -46,30 +53,19 @@ struct Import { } } -enum VERSION = "0.1.0"; - -enum pattern = ctRegex!`^(\s*)(?:(public|static)\s+)?import\s+(?:(\w+)\s*=\s*)?([a-zA-Z._]+)\s*(:\s*\w+(?:\s*=\s*\w+)?(?:\s*,\s*\w+(?:\s*=\s*\w+)?)*)?\s*;[ \t]*([\n\r]*)$`; - -enum help = "importsort-d v" ~ VERSION ~ " - -Usage: importsort-d [-i [-k]] [-o <output>] [-r] [-s] [-w [-i <msec>]] <input...> - <input> can be set to '-' to read from stdin +enum PATTERN = ctRegex!`^(\s*)(?:(public|static)\s+)?import\s+(?:(\w+)\s*=\s*)?([a-zA-Z._]+)\s*(:\s*\w+(?:\s*=\s*\w+)?(?:\s*,\s*\w+(?:\s*=\s*\w+)?)*)?\s*;[ \t]*([\n\r]*)$`; -Options: - -k, --keep ............ keeps the line as-is instead of formatting - -i, --inline .......... writes to the input - -o, --out <path> ...... writes to `path` instead of stdout - - -a, --attribute ....... public and static imports first - -r, --original ........ sort by original not by binding - - -h, --help ............ prints this message - -v, --verbose ......... prints useful messages"; +enum BINARY = "importsort-d"; +enum VERSION = "0.1.0"; +enum HELP = import("help.txt") + .replace("{binary}", BINARY) + .replace("{version}", VERSION); struct SortConfig { bool keepLine = false; bool byAttribute = false; - bool byOriginal = false; + bool byBinding = false; + bool verbose = false; } void writeImports(File outfile, SortConfig config, Import[] matches) { @@ -86,15 +82,15 @@ void writeImports(File outfile, SortConfig config, Import[] matches) { outfile.write("public "); if (m.static_) outfile.write("static "); - if (m.name.hasAlias) { - outfile.writef("import %s = %s", m.name.alias_, m.name.original); + if (m.name.hasBinding) { + outfile.writef("import %s = %s", m.name.binding, m.name.original); } else { outfile.write("import " ~ m.name.original); } foreach (i, ident; m.idents) { auto begin = i == 0 ? " : " : ", "; - if (ident.hasAlias) { // hasAlias - outfile.writef("%s%s = %s", begin, ident.alias_, ident.original); + if (ident.hasBinding) { // hasBinding + outfile.writef("%s%s = %s", begin, ident.binding, ident.original); } else { outfile.write(begin ~ ident.original); } @@ -104,13 +100,35 @@ void writeImports(File outfile, SortConfig config, Import[] matches) { } } -void sortImports(SortConfig config, File infile, File outfile) { +void sortImports(alias P = "true", R)(R entries, SortConfig config) + if (isIterable!R && is(ElementType!R == DirEntry)) { + alias postFunc = unaryFun!P; + + File infile, outfile; + foreach (entry; entries) { + stderr.writef("\033[34msorting \033[0;1m%s\033[0m\n", entry.name); + + infile = File(entry.name); + outfile = File(entry.name ~ ".new", "w"); + + sortImports(infile, outfile, config); + + infile.close(); + outfile.close(); + + rename(entry.name ~ ".new", entry.name); + + cast(void) postFunc(entry.name); + } +} + +void sortImports(File infile, File outfile, SortConfig config) { string softEnd = null; Import[] matches; foreach (line; infile.byLine(Yes.keepTerminator)) { auto linestr = line.idup; - if (auto match = matchFirst(linestr, pattern)) { // is import + if (auto match = linestr.matchFirst(PATTERN)) { // is import if (softEnd) { if (!matches) outfile.write(softEnd); @@ -119,9 +137,9 @@ void sortImports(SortConfig config, File infile, File outfile) { auto im = Import(config.byAttribute, linestr); if (match[3]) { - im.name = Identifier(config.byOriginal, match[4], match[3]); + im.name = Identifier(config.byBinding, match[4], match[3]); } else { - im.name = Identifier(config.byOriginal, match[4]); + im.name = Identifier(config.byBinding, match[4]); } im.begin = match[1]; im.end = match[6]; @@ -134,9 +152,9 @@ void sortImports(SortConfig config, File infile, File outfile) { if (match[5]) { foreach (id; match[5][1 .. $].split(",")) { if (auto pair = id.findSplit("=")) { // has alias - im.idents ~= Identifier(config.byOriginal, pair[2].strip, pair[0].strip); + im.idents ~= Identifier(config.byBinding, pair[2].strip, pair[0].strip); } else { - im.idents ~= Identifier(config.byOriginal, id.strip); + im.idents ~= Identifier(config.byBinding, id.strip); } } im.idents.sort!((a, b) => a.sortBy < b.sortBy); @@ -161,92 +179,138 @@ void sortImports(SortConfig config, File infile, File outfile) { outfile.writeImports(config, matches); } +DirEntry[] listEntries(alias F = "true")(string[] input, bool recursive) { + alias filterFunc = unaryFun!F; + + DirEntry[] entries; + + foreach (path; input) { + if (!exists(path)) { + stderr.writef("error: '%s' does not exist\n", path); + exit(1); + } else if (isDir(path)) { + foreach (entry; dirEntries(path, recursive ? SpanMode.depth : SpanMode.shallow)) { + if (entry.isFile && entry.name.endsWith(".d") && filterFunc(entry.name)) + entries ~= entry; + } + } else if (isFile(path)) { + if (!path.endsWith(".d")) { + stderr.writef("error: '%s' is not a .d-file\n", path); + exit(1); + } + if (filterFunc(path)) + entries ~= DirEntry(path); + } else { + stderr.writef("error: '%s' is not a file or directory\n", path); + exit(1); + } + } + return entries; +} + void main(string[] args) { SortConfig config; - bool inline = false; - string output = null; - string path = null; + bool inline; + string output; + string[] input; + bool watcher; + bool watcherDelaySet; + double watcherDelay = 0.1; // sec + bool recursive; - bool nextout = false; + // -*- option parser -*- + bool nextOutput; + bool nextWatcherDelay; foreach (arg; args[1 .. $]) { - if (nextout) { + if (nextOutput) { output = arg; - nextout = false; - } - if (arg == "--help" || arg == "-h") { - stdout.writeln(help); + nextOutput = false; + } else if (nextWatcherDelay) { + try { + watcherDelay = parse!double(arg); + } catch (ConvException) { + stderr.writef("error: cannot parse delay '%s' to an integer\n", arg); + exit(1); + } + watcherDelaySet = true; + nextWatcherDelay = false; + } else if (arg == "--help" || arg == "-h") { + stdout.writeln(HELP); return; + } else if (arg == "--verbose" || arg == "-v") { + config.verbose = true; } else if (arg == "--keep" || arg == "-k") { config.keepLine = true; } else if (arg == "--attribute" || arg == "-a") { config.byAttribute = true; + } else if (arg == "--binding" || arg == "-b") { + config.byBinding = true; } else if (arg == "--inline" || arg == "-i") { inline = true; - } else if (arg == "--original" || arg == "-r") { - config.byOriginal = true; - } else if (arg == "--out" || arg == "-o") { + } else if (arg == "--recursive" || arg == "-r") { + recursive = true; + // TODO: --watch + /*} else if (arg == "--watch" || arg == "-w") { + watcher = true; + } else if (arg == "--delay" || arg == "-d") { + if (watcherDelaySet) { + stderr.writeln("error: watcher-delay already specified"); + stderr.writeln(HELP); + exit(1); + } + nextWatcherDelay = true;*/ + } else if (arg == "--output" || arg == "-o") { if (output != null) { stderr.writeln("error: output already specified"); - stderr.writeln(help); + stderr.writeln(HELP); exit(1); } - nextout = true; + nextOutput = true; } else if (arg[0] == '-') { stderr.writef("error: unknown option '%s'\n", arg); - stderr.writeln(help); + stderr.writeln(HELP); exit(1); } else { - if (path != null) { - stderr.writeln("error: input already specified"); - stderr.writeln(help); - exit(1); - } - path = arg; + input ~= arg; } } - if (output != null && output == path) { - stderr.writeln("error: input and output cannot be the same; use '--inline'"); - stderr.writeln(help); + if (recursive && input.length == 0) { + stderr.writeln("error: cannot use '--recursive' and specify no input"); exit(1); } - if (inline && output != null) { - stderr.writeln("error: you cannot specify '--inline' and '--out' at the same time"); - stderr.writeln(help); + if (inline && input.length == 0) { + stderr.writeln("error: cannot use '--inline' and read from stdin"); exit(1); } - if (!path) { - path = "-"; - } - if (inline && path == "-") { - stderr.writeln("error: you cannot specify '--inline' and read from stdin"); - stderr.writeln(help); + if ((!inline || output.length > 0) && input.length > 0) { + stderr.writeln("error: if you use inputs you must use '--inline'"); exit(1); } + // -*- operation -*- - File infile, outfile; - if (path == "-") { - infile = stdin; - } else { - if (!path.exists() || !path.isFile()) { - stderr.writef("error: file '%s' does not exist or is not a file.\n", path); - exit(1); + /* if (watcher) { + stderr.writeln("\033[1;34mwatching files...\033[0m"); + SysTime[string] lastModified; + for (;;) { + auto entries = listEntries!(x => x !in lastModified + || lastModified[x] != x.timeLastModified)(input, recursive); + + foreach (entry; entries) { + lastModified[entry.name] = entry.timeLastModified; + } + entries.sortImports(config); + Thread.sleep(Duration!"msecs"(cast(long) watcherDelay * 1000)); } - infile = File(path); - } + } else + */ + if (input == null) { + File outfile = (output == null) ? stdout : File(output); - if (inline) { - outfile = File(path ~ ".new", "w"); - scope (exit) - rename(path ~ ".new", path); - } else if (output) { - outfile = File(output, "w"); + sortImports(stdin, outfile, config); + if (output) + outfile.close(); } else { - outfile = stdout; + listEntries(input, recursive).sortImports(config); } - - sortImports(config, infile, outfile); - - infile.close(); - outfile.close(); }