forked from dlang/dlang.org
-
Notifications
You must be signed in to change notification settings - Fork 0
/
assert_writeln_magic.d
executable file
·401 lines (342 loc) · 10.7 KB
/
assert_writeln_magic.d
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
#!/usr/bin/env dub
/++
dub.sdl:
dependency "libdparse" version="0.7.0-beta.7"
name "assert_writeln_magic"
+/
/*
* Tries to convert `assert`'s into user-friendly `writeln` calls.
* The objective of this tool is to be conservative as
* broken example look a lot worse than a few statements
* that could have potentially been rewritten.
*
* - only EqualExpressions are "lowered"
* - static asserts are ignored
* - only single-line assers are rewritten
*
* Copyright (C) 2017 by D Language Foundation
*
* Author: Sebastian Wilzbach
*
* Distributed under the Boost Software License, Version 1.0.
* (See accompanying file LICENSE_1_0.txt or copy at
* http://www.boost.org/LICENSE_1_0.txt)
*/
// Written in the D programming language.
import dparse.ast;
import std.algorithm;
import std.conv;
import std.experimental.logger;
import std.range;
import std.stdio;
import std.typecons;
private string formatNode(T)(const T t)
{
import dparse.formatter;
import std.array : appender;
auto writer = appender!string();
auto formatter = new Formatter!(typeof(writer))(writer);
formatter.format(t);
return writer.data;
}
class TestVisitor(Out) : ASTVisitor
{
import dparse.lexer : tok, Token;
this(Out fl)
{
this.fl = fl;
}
alias visit = ASTVisitor.visit;
override void visit(const Unittest test)
{
resetTestState();
inTest = true;
scope(exit) inTest = false;
test.accept(this);
processLastAssert();
}
override void visit(const EqualExpression expr)
{
enum eqToken = tok!"==";
if (inAssert && expr.operator == eqToken && expr.left !is null && expr.right !is null)
lastEqualExpression = expr;
}
override void visit(const AssertExpression expr)
{
if (inFunctionCall)
return;
// only look at `a == b` within the AssertExpression
if (typeid(expr.assertion) != typeid(CmpExpression))
return;
lastAssert = expr;
inAssert = true;
expr.accept(this);
inAssert = false;
fromAssert = true;
}
// for now static asserts are ignored
override void visit(const StaticAssertStatement expr)
{
fromStaticAssert = true;
expr.accept(this);
}
/**
The following code (in std.concurrency) leads to false positives:
assertNotThrown!AssertError(assert(receiveOnly!int() == i));
Hence we simply ignore all asserts in function calls.
*/
override void visit(const FunctionCallExpression expr)
{
inFunctionCall = true;
expr.accept(this);
inFunctionCall = false;
}
/// A single line
override void visit(const DeclarationOrStatement expr)
{
processLastAssert();
expr.accept(this);
}
void processLastAssert()
{
import std.uni : isWhite;
import std.format : format;
if (fromAssert && !fromStaticAssert &&
lastEqualExpression !is null && lastAssert !is null)
{
auto e = lastEqualExpression;
if (e.left !is null && e.right !is null)
{
// libdparse starts the line count with 1
auto lineNr = lastAssert.line - 1;
// only replace single-line expressions (for now)
if (fl[lineNr].endsWith(";"))
{
auto wsLen = fl[lineNr].countUntil!(u => !u.isWhite);
auto indent = fl[lineNr][0 .. wsLen];
if (fl[lineNr][wsLen .. $].startsWith("assert", "static assert"))
{
auto left = lastEqualExpression.left.formatNode;
auto right = lastEqualExpression.right.formatNode;
if (left.length + right.length > 80)
fl[lineNr] = format("%s// %s\n%swriteln(%s);", indent, right, indent, left);
else
fl[lineNr] = format("%swriteln(%s); // %s", indent, left, right);
//writefln("line: %d, column: %d", lastAssert.line, lastAssert.column);
}
}
}
}
resetTestState();
}
private:
void resetTestState()
{
fromAssert = false;
fromStaticAssert = false;
lastEqualExpression = null;
lastAssert = null;
}
/// within in the node
bool inTest;
bool inAssert;
bool inFunctionCall;
/// at a sibling after the node was seen, but the upper parent hasn't been reached yet
bool fromAssert;
bool fromStaticAssert;
Rebindable!(const AssertExpression) lastAssert;
Rebindable!(const EqualExpression) lastEqualExpression;
Out fl;
}
void parseString(Visitor)(ubyte[] sourceCode, string fileName, Visitor visitor)
{
import dparse.lexer;
import dparse.parser : parseModule;
import dparse.rollback_allocator : RollbackAllocator;
LexerConfig config;
auto cache = StringCache(StringCache.defaultBucketCount);
const(Token)[] tokens = getTokensForParser(sourceCode, config, &cache).array;
RollbackAllocator rba;
auto m = parseModule(tokens, fileName, &rba);
visitor.visit(m);
}
void parseFile(string fileName, string destFile)
{
import std.array : uninitializedArray;
auto inFile = File(fileName);
if (inFile.size == 0)
warningf("%s is empty", inFile.name);
ubyte[] sourceCode = uninitializedArray!(ubyte[])(to!size_t(inFile.size));
if (sourceCode.length == 0)
return;
inFile.rawRead(sourceCode);
auto fl = FileLines(fileName, destFile);
auto visitor = new TestVisitor!(typeof(fl))(fl);
parseString(sourceCode, fileName, visitor);
delete visitor;
}
// Modify a path under oldBase to a new path with the same subpath under newBase.
// E.g.: `/foo/bar`.rebasePath(`/foo`, `/quux`) == `/quux/bar`
string rebasePath(string path, string oldBase, string newBase)
{
import std.path : absolutePath, buildPath, relativePath;
return buildPath(newBase, path.absolutePath.relativePath(oldBase.absolutePath));
}
version(unittest) { void main(){} } else
void main(string[] args)
{
import std.file;
import std.getopt;
import std.path;
string inputDir, outputDir;
string[] ignoredFiles;
auto helpInfo = getopt(args, config.required,
"inputdir|i", "Folder to start the recursive search for unittest blocks (can be a single file)", &inputDir,
"outputdir|o", "Alternative folder to use as output (can be a single file)", &outputDir,
"ignore", "List of files to exclude (partial matching is supported)", &ignoredFiles);
if (helpInfo.helpWanted)
{
return defaultGetoptPrinter(`assert_writeln_magic
Tries to lower EqualExpression in AssertExpressions of Unittest blocks to commented writeln calls.
`, helpInfo.options);
}
inputDir = inputDir.asNormalizedPath.array;
DirEntry[] files;
// inputDir as default output directory
if (!outputDir.length)
outputDir = inputDir;
if (inputDir.isFile)
{
files = [DirEntry(inputDir)];
inputDir = "";
}
else
{
files = dirEntries(inputDir, SpanMode.depth).filter!(
a => a.name.endsWith(".d") && !a.name.canFind(".git")).array;
}
foreach (file; files)
{
if (!ignoredFiles.any!(x => file.name.canFind(x)))
{
// single files
if (inputDir.length == 0)
parseFile(file.name, outputDir);
else
parseFile(file.name, file.name.rebasePath(inputDir, outputDir));
}
}
}
/**
A simple line-based in-memory representation of a file.
- will automatically write all changes when the object is destructed
- will use a temporary file to do safe, whole file swaps
*/
struct FileLines
{
import std.array, std.file, std.path;
string[] lines;
string destFile;
bool overwriteInputFile;
bool hasWrittenChanges;
this(string inputFile, string destFile)
{
stderr.writefln("%s -> %s", inputFile, destFile);
this.overwriteInputFile = inputFile == destFile;
this.destFile = destFile;
lines = File(inputFile).byLineCopy.array;
destFile.dirName.mkdirRecurse;
}
// dumps all changes
~this()
{
if (overwriteInputFile)
{
if (hasWrittenChanges)
{
auto tmpFile = File(destFile ~ ".tmp", "w");
writeLinesToFile(tmpFile);
tmpFile.close;
tmpFile.name.rename(destFile);
}
}
else
{
writeLinesToFile(File(destFile, "w"));
}
}
// writes all changes to a random, temporary file
void writeLinesToFile(File outFile) {
// dump file
foreach (line; lines)
outFile.writeln(line);
// within the docs we automatically inject std.stdio (hence we need to do the same here)
// writeln needs to be @nogc, @safe, pure and nothrow (we just fake it)
outFile.writeln("// \nprivate void writeln(T)(T l) { }");
outFile.flush;
}
string opIndex(size_t i) { return lines[i]; }
void opIndexAssign(string line, size_t i) {
hasWrittenChanges = true;
lines[i] = line;
}
}
version(unittest)
{
struct FileLinesMock
{
string[] lines;
string opIndex(size_t i) { return lines[i]; }
void opIndexAssign(string line, size_t i) {
lines[i] = line;
}
}
auto runTest(string sourceCode)
{
import std.string : representation;
auto mock = FileLinesMock(sourceCode.split("\n"));
auto visitor = new TestVisitor!(typeof(mock))(mock);
parseString(sourceCode.representation.dup, "testmodule", visitor);
delete visitor;
return mock;
}
}
unittest
{
"Running tests for assert_writeln_magic".writeln;
// purposefully not indented
string testCode = q{
unittest
{
assert(equal(splitter!(a => a == ' ')("hello world"), [ "hello", "", "world" ]));
assert(equal(splitter!(a => a == 0)(a), w));
}
};
auto res = runTest(testCode);
assert(res.lines[3 .. $ - 2] == [
"assert(equal(splitter!(a => a == ' ')(\"hello world\"), [ \"hello\", \"\", \"world\" ]));",
"assert(equal(splitter!(a => a == 0)(a), w));"
]);
}
unittest
{
string testCode = q{
unittest
{
assert(1 == 2);
assert(foo() == "bar");
assert(foo() == bar);
assert(arr == [0, 1, 2]);
assert(r.back == 1);
}
};
auto res = runTest(testCode);
assert(res.lines[3 .. $ - 2] == [
"writeln(1); // 2",
"writeln(foo()); // \"bar\"",
"writeln(foo()); // bar",
"writeln(arr); // [0, 1, 2]",
"writeln(r.back); // 1",
]);
"Successfully ran tests for assert_writeln_magic".writeln;
}