This is an archive of the discontinued LLVM Phabricator instance.

[clang-tidy] New checker to replace dynamic exception specifications
ClosedPublic

Authored by hintonda on May 26 2016, 10:55 AM.

Details

Summary

New checker to replace dynamic exception
specifications

This is an alternative to D18575 which relied on reparsing the decl to
find the location of dynamic exception specifications, but couldn't
deal with preprocessor conditionals correctly without reparsing the
entire file.

This approach uses D20428 to find dynamic exception specification
locations and handles all cases correctly.

Event Timeline

hintonda updated this revision to Diff 58653.May 26 2016, 10:55 AM
hintonda retitled this revision from to New checker to replace dynamic exception specifications.
hintonda updated this object.
hintonda added reviewers: alexfh, aaron.ballman.
hintonda retitled this revision from New checker to replace dynamic exception specifications to [clang-tidy] New checker to replace dynamic exception specifications.May 26 2016, 11:03 AM
hintonda updated this object.
alexfh requested changes to this revision.Jun 3 2016, 3:56 PM
alexfh edited edge metadata.

Removing from my dashboard until D20428 is submitted.

This revision now requires changes to proceed.Jun 3 2016, 3:56 PM
hintonda updated this revision to Diff 59794.Jun 6 2016, 3:38 PM
hintonda edited edge metadata.

Update matcher to use functionProtoType instead of functionType.

Removing from my dashboard until D20428 is submitted.

alexfh requested changes to this revision.Jun 17 2016, 5:42 AM
alexfh edited edge metadata.
This revision now requires changes to proceed.Jun 17 2016, 5:42 AM
alexfh requested changes to this revision.Jun 17 2016, 5:42 AM
hintonda updated this revision to Diff 83371.Jan 6 2017, 8:44 AM
hintonda edited edge metadata.

Updated patch.

malcolm.parsons added inline comments.
clang-tidy/modernize/UseNoexceptCheck.h
20

Unused?

docs/clang-tidy/checks/modernize-use-noexcept.rst
39

typo: equivelent -> equivalent.

hintonda added inline comments.Jan 6 2017, 9:05 AM
clang-tidy/modernize/UseNoexceptCheck.h
20

Good catch -- left over from a previous version.

docs/clang-tidy/checks/modernize-use-noexcept.rst
39

Thanks! Spelling isn't my strong suit.

hintonda updated this revision to Diff 83375.Jan 6 2017, 9:28 AM
hintonda edited edge metadata.
  • Addressed comments.
mgehre added a subscriber: mgehre.Jan 6 2017, 11:22 AM

Hi,
this is a good idea for a check.

I would prefer when the FixIt just removes throw(A,B) specifier, instead of replacing it by noexcept(false),
because noexcept(false) means the same things as having no noexcept specifier at all.
And less code to read means its easier to understand.

I could be wrong (please let me know if I am), but my understanding is:

// Does not throw
throw() --> noexcept == noexcept(true)

// Does throw
throw(something) --> noexcept(false)

Please see http://en.cppreference.com/w/cpp/language/noexcept_spec

aaron.ballman edited edge metadata.Jan 6 2017, 11:51 AM

Hi,
this is a good idea for a check.

I would prefer when the FixIt just removes throw(A,B) specifier, instead of replacing it by noexcept(false),
because noexcept(false) means the same things as having no noexcept specifier at all.
And less code to read means its easier to understand.

If the API designer explicitly specified "this function can throw A or B", I think a more helpful FixIt is to specify noexcept(false) explicitly rather than leave off any information about the exception specification. The explicit nature of the dynamic exception specification suggests the API designer intended for the user to know more information about whether the function throws or not, so it's a bit hostile to replace that with no information about whether the function throws or not (even if it's functionally equivalent).

I'm working on code bases where their is no manually written noexcept(false) anywhere,
and I don't think the FixIt should look different than manually written code.
At least a configuration option for the check would be nice.

Matthias, I think you make a good point. While noexcept(expr) is valuable, noexcept(false) adds no value. Therefore, I think we should do as you suggest, i.e.:

s/throw()/noexcept/
s/throw(something)//

Aaron, does this work for you?

Matthias, I think you make a good point. While noexcept(expr) is valuable, noexcept(false) adds no value. Therefore, I think we should do as you suggest, i.e.:

s/throw()/noexcept/
s/throw(something)//

Aaron, does this work for you?

I think it makes a reasonable option, but I think it should be off by default. Again, I think that's hostile towards users to remove an explicit exception specification entirely when there was an explicit dynamic exception specification written on the function signature. noexcept(false) is a stronger signal to anyone reading the signature than no exception specification whatsoever.

Be careful, though, not to break code by removing the exception specification. These two are *not* equivalent, for instance:

struct S {
  ~S() noexcept(false);
  void operator delete(void *ptr) noexcept(false); 
};

struct T {
  ~T(); // Is actually noexcept(true) by default!
  void operator delete(void *ptr); // Is actually noexcept(true) by default!
};
hintonda updated this revision to Diff 83588.Jan 8 2017, 11:33 PM
hintonda edited edge metadata.
  • Omit noexcept(false) replacement except for dtor and operator delete.
alexfh requested changes to this revision.Jan 9 2017, 1:53 AM
alexfh edited edge metadata.
alexfh added inline comments.
clang-tidy/modernize/UseNoexceptCheck.cpp
101

Did you consider auto-detection approach like in getFallthroughAttrSpelling in tools/clang/lib/Sema/AnalysisBasedWarnings.cpp?

106

I suspect this won't work when the range is not contiguous, e.g. starts in a macro definition and ends outside of it:

#define T throw
void f() T(a, b) {}

Can you try this test (or construct something similar that will actually break this code)? In case it doesn't work, Lexer::makeFileCharRange is the standard way to get a contiguous file range corresponding to a source range (if possible).

docs/clang-tidy/checks/modernize-use-noexcept.rst
7–9

This description doesn't say why noexcept is better.

This revision now requires changes to proceed.Jan 9 2017, 1:53 AM
malcolm.parsons added inline comments.Jan 9 2017, 2:45 AM
clang-tidy/modernize/UseNoexceptCheck.cpp
101

cpp11-migrate used to do this for -add-override - rL183001.
clang-tidy's modernize-use-override check doesn't even have an option.

aaron.ballman requested changes to this revision.Jan 10 2017, 8:09 AM
aaron.ballman edited edge metadata.
aaron.ballman added inline comments.
clang-tidy/modernize/UseNoexceptCheck.cpp
42

Shouldn't we require C++11 or later?

65

Will this catch pointers to member functions as well?

struct S {
  void f() throw();    
};

void f(void (S::*)() throw()) {
    
}
69

Also missing: typedefs and using declarations.

docs/clang-tidy/checks/modernize-use-noexcept.rst
31

I continue to object to removing the explicit exception specification entirely as the default behavior without strong justification. Further. there is no option to control this behavior.

39

I'm not certain I understand the justification of calling out older compilers or suggesting use of throw(). The check will continually flag throw() and try to recommend noexcept in its place, won't it? At least, that's how I read the preceding paragraph.

I think the macro replacement is a reasonable feature, but I think the check only makes sense for C++11 or later, since C++98 users really have no alternatives to dynamic exception specifications.

hintonda marked 7 inline comments as done.Jan 10 2017, 10:23 AM
hintonda added inline comments.
clang-tidy/modernize/UseNoexceptCheck.cpp
65

Added your test, but will need to change matcher to catch it.

69

As far as I know, it isn't legal to add dynamic exception specifications to typedefs and using declarations.

101

NoexceptMacro is a user option. I'm just tested whether or not the user provided anything. Perhaps I should pick a better name. Would NoexceptMacroOption be better?

Did I misunderstand your comment?

106

As you suspected, I wasn't handling this case correctly. I've made the change you suggested and will check in shortly.

docs/clang-tidy/checks/modernize-use-noexcept.rst
31

I tried to make sure it's only applied where appropriate. If I missed a case, please let me know, but I'm not sure an option "just in case" is right solution.

However, I did try to structure the code in a way to make it easy to add an option if that turns out the be the right thing to do.

39

Libraries, e.g., libc++, that need to support both multiple versions of the standard, use macros to switch between throw() and noexcept.

So this option was designed to be libc++ friendly. If that's not appropriate, I can remove it.

hintonda updated this revision to Diff 83827.Jan 10 2017, 10:36 AM
hintonda edited edge metadata.
hintonda marked an inline comment as done.
  • Addressed comments.
aaron.ballman added inline comments.Jan 10 2017, 10:49 AM
clang-tidy/modernize/UseNoexceptCheck.cpp
69

Hmm, I thought you could, at least in C++17, since it's now part of the function's type. However, I don't have a standard in front of me at the moment, so I'll have to double-check. It can always be added in a follow-up patch and need not block this one.

However, I do know the following is allowed in a typedef, and I don't think your existing ParmVarDecl matcher will catch it:

typedef void (*fp)(void (*f)(int) throw());
docs/clang-tidy/checks/modernize-use-noexcept.rst
31

The behavior as it's implemented now is not the behavior I would expect from such a check -- I think that throw(int) should get a FixIt to noexcept(false) rather than no noexcept clause whatsoever. Despite them being functionally equivalent in most cases, the explicit nature of the dynamic exception specification should be retained with an explicit noexcept exception specification, not discarded. If you really want throw(int) to be modified to have no explicit exception specification, I think that should be an option (off by default). If you would rather not make an option, then I think the explicit exception specification should remain.

39

I think the *option* is fine; I think the wording is confusing. How about:

Alternatively, users can also use :option:`ReplacementString` to
specify a macro to use instead of ``noexcept``.  This is useful when
maintaining source code that uses custom exception specification marking
other than ``noexcept``.
hintonda added inline comments.Jan 10 2017, 11:09 AM
clang-tidy/modernize/UseNoexceptCheck.cpp
69

clang doesn't allow dynamic exception specs in typedef or using declarations for either c++11 or c++14, but does in c++1z since throw() is an alias for noexcept. I'll try to add an appropriate test to highlight this.

I've added your typedef example -- thanks for suggesting it -- and the matcher does catch it and does the appropriate fixit.

docs/clang-tidy/checks/modernize-use-noexcept.rst
31

Makes sense. Will do.

39

Ah, okay, that sounds much better. I'll make this change shortly.

alexfh requested changes to this revision.Jan 10 2017, 3:49 PM
alexfh edited edge metadata.
alexfh added inline comments.
clang-tidy/modernize/UseNoexceptCheck.cpp
100

I suspect there might be valid code that will trigger this assertion. I would issue the diagnostic, just without a FixItHint.

112

The message neither explains what's wrong with the code, nor says what is a better alternative. Something like "dynamic exception specification '%0' is so 1998 <actually explain what's wrong with it>; consider using 'noexcept(...)' <real suggestion>" would lead to less user confusion.

docs/clang-tidy/checks/modernize-use-noexcept.rst
7–9

This still needs to be addressed.

This revision now requires changes to proceed.Jan 10 2017, 3:49 PM
hintonda added inline comments.Jan 10 2017, 4:46 PM
docs/clang-tidy/checks/modernize-use-noexcept.rst
7–9

Absolutely, but I'll probably do all the code changes first while it's still fresh in my mind.

aaron.ballman added inline comments.Jan 10 2017, 5:04 PM
docs/clang-tidy/checks/modernize-use-noexcept.rst
7–9

Since we're talking about documentation, should we note that this check does produce a subtle, obscure change in behavior? When you have throw() on a function signature and that function winds up throwing something, this would result in std::unexpected() being called. When that gets modified to be noexcept, the call to std::unexpected() is replaced by a call to std::terminate(). This hopefully does not break any code, but we may want to document the fact that failing to honor a dynamic exception specification is slightly different than failing to honor an exception specification so that users are aware of the slightly different semantics.

hintonda updated this revision to Diff 84926.Jan 18 2017, 8:09 PM
hintonda edited edge metadata.
hintonda marked 22 inline comments as done.
  • Add support for matching MemberPointerType's.
  • Add noexcept(false) option plus test; allow invalid replacement ranges; enhance diagnostics.
  • Updated docs.

Thanks for all the feedback. I've tried to address all your comments, and reworked the documentation. Please let me know if I missed or misunderstood anything.

hintonda updated this revision to Diff 84975.Jan 19 2017, 8:27 AM
  • Fix diagnostic when removing throwing specs.
hintonda updated this revision to Diff 85291.Jan 22 2017, 11:42 AM
  • Rebased.
alexfh requested changes to this revision.Jan 24 2017, 7:34 AM
alexfh added inline comments.
clang-tidy/modernize/UseNoexceptCheck.cpp
21–34

Lexer::getSourceText should do the same thing, I guess.

100

Does it actually need template to compile?

104

CharSourceRange::getTokenRange(Range) is easier to understand.

test/clang-tidy/modernize-use-noexcept.cpp
88

Please add a test with templates and multiple instantiations, e.g.:

template<typename T>
void qqq() noexcept {}
void www() { qqq<int>(); qqq<double>(); }

Same for member functions.

This revision now requires changes to proceed.Jan 24 2017, 7:34 AM
hintonda updated this revision to Diff 85593.Jan 24 2017, 8:58 AM
hintonda edited edge metadata.
hintonda marked 4 inline comments as done.
  • Addressed comments and added additional test cases.
alexfh accepted this revision.Jan 24 2017, 10:06 AM

LGTM, if Aaron has no concerns.

Thank you for the new check!

aaron.ballman accepted this revision.Jan 26 2017, 2:26 PM

Yes, this LGTM as well. Thank you for working on this!

This revision is now accepted and ready to land.Jan 26 2017, 2:26 PM

Great, thanks. Could you commit for me?

Great, thanks. Could you commit for me?

Certainly! I've commit in r293217.

aaron.ballman closed this revision.Jan 26 2017, 2:46 PM

There were some issues with failing tests that caused this commit to need to be reverted in r293267

See for instance: http://lab.llvm.org:8011/builders/clang-cmake-aarch64-full/builds/1039

Eugene Zelenko also had some small fixes you might want to incorporate as well before recommitting (see r293234).

Thanks and sorry for the breakage. Unfortunately, I'm unable to reproduce locally (OSX), but will try to get access to linux box this weekend.

Seems to be related to memory corruption wrt to improper StringRef usage, but I can't say for sure yet.

hintonda reopened this revision.Jun 6 2017, 7:24 AM

Reopening due to test failures on Linux -- was rolled back.

This revision is now accepted and ready to land.Jun 6 2017, 7:24 AM
hintonda updated this revision to Diff 101659.Jun 6 2017, 6:36 PM

In order to fix diagnostic corruption in some of the buildbot tests
(unable to reproduce locally):

  • make NoexceptMacro a static variable so it's lifetime doesn't end when UseNoexceptCheck is destroyed.
  • pass a const char* instead of a StringRef to DiagnosticBuilder so it won't create a temporary std::string and cache the address of the temporary char * it owns.
alexfh added a comment.Jun 7 2017, 1:48 AM

In order to fix diagnostic corruption in some of the buildbot tests
(unable to reproduce locally):

  • make NoexceptMacro a static variable so it's lifetime doesn't end when UseNoexceptCheck is destroyed.
  • pass a const char* instead of a StringRef to DiagnosticBuilder so it won't create a temporary std::string and cache the address of the temporary char * it owns.

That's pretty hacky, even if this fixes the immediate issue (which I'm somewhat skeptical about). Do you have failure logs saved somewhere, by chance? If not, I'd suggest to resubmit the original patch (without these two hacks) and grab the fresh failure logs. Another suggestion is to run the tests with asan (and maybe with msan) to see, whether the issue can be detected by the sanitizers.

alexfh requested changes to this revision.Jun 7 2017, 1:48 AM

In any case, I'm strongly against these hacks, please revert them.

This revision now requires changes to proceed.Jun 7 2017, 1:48 AM
hintonda updated this revision to Diff 101708.Jun 7 2017, 5:31 AM
hintonda edited edge metadata.
  • Rollback to previous version: rebased + r293218 and r293234.

Unfortunately, the logs are no longer available, and I don't have copies.

However, I think I know what's going on, so I'll try to submit a fix later today.

hintonda updated this revision to Diff 101722.Jun 7 2017, 6:16 AM
  • Only pass %2 parameter if %2 is included in format.
alexfh added a comment.Jun 7 2017, 8:19 AM
  • Only pass %2 parameter if %2 is included in format.

I thought, DiagnosticsBuilder handles placeholders in conditional parts correctly. Did you find an evidence of the opposite? Can you add a test that consistently fails?

clang-tidy/modernize/UseNoexceptCheck.h
43

This can also be const for consistency with how options are handled in this and other checks.

alexfh added a comment.Jun 7 2017, 8:20 AM
  • Only pass %2 parameter if %2 is included in format.

I thought, DiagnosticsBuilder handles placeholders in conditional parts correctly. Did you find an evidence of the opposite? Can you add a test that consistently fails?

(without your latest change that is)

I have not, as yet, been able to reproduce the buildbot failures. They were essentially intermittent seg-faults, and corrupt diag output.

I will work on creating a test that can reproduce the problem.

I have not, as yet, been able to reproduce the buildbot failures. They were essentially intermittent seg-faults, and corrupt diag output.

I will work on creating a test that can reproduce the problem.

As I said, we could re-submit the patch (without speculative fixes) and see whether the buildbots break again. One more suggestion is to run the tests with asan. Have you tried this?

hintonda updated this revision to Diff 101819.Jun 7 2017, 2:50 PM
  • Rollback last change.
hintonda added a comment.EditedJun 7 2017, 4:18 PM

Just ran asan on linux and we have a heap-use-after-free in the std::string ctor.

Here's a partial stack dump:

4980==ERROR: AddressSanitizer: heap-use-after-free on address 0x604000024328 at pc 0x00000057ad32 bp 0x7ffd240a7f50 sp 0x7ffd240a7700

READ of size 8 at 0x604000024328 thread T0

#0 0x57ad31 in __interceptor_memcpy.part.36 /home/d80049854/projects/clang/4.0.0/llvm/projects/compiler-rt/lib/asan/../sanitizer_common/sanitizer_common_interceptors.inc:655
#1 0x6c5960 in char* std::string::_S_construct<char const*>(char const*, char const*, std::allocator<char> const&, std::forward_iterator_tag) /usr/lib/gcc/x86_64-linux-gnu/6.2.0/../../../../include/c++/6.2.0/bits/basic_string.tcc:580:6
#2 0x7fe7bd7bc98a in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::basic_string(char const*, unsigned long, std::allocator<char> const&) (/usr/lib/x86_64-linux-gnu/libstdc++.so.6+0xc598a)
#3 0x67b89f in llvm::StringRef::str() const /home/d80049854/projects/clang/llvm/include/llvm/ADT/StringRef.h:230:14
#4 0x67b3dd in llvm::StringRef::operator std::string() const /home/d80049854/projects/clang/llvm/include/llvm/ADT/StringRef.h:257:14
#5 0xd679d2 in clang::FixItHint::CreateReplacement(clang::CharSourceRange, llvm::StringRef) /home/d80049854/projects/clang/llvm/tools/clang/include/clang/Basic/Diagnostic.h:131:25
#6 0x1213006 in clang::tidy::modernize::UseNoexceptCheck::check(clang::ast_matchers::MatchFinder::MatchResult const&) /home/d80049854/projects/clang/llvm/tools/clang/tools/extra/clang-tidy/modernize/U

$ ../../4.0.0/build/Release/bin/clang -v
clang version 4.0.0 (tags/RELEASE_400/final)
Target: x86_64-unknown-linux-gnu
Thread model: posix
InstalledDir: /home/d80049854/projects/clang/build/Debug/../../4.0.0/build/Release/bin
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.6
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.6.4
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.8
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.8.4
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.9
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/4.9.3
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/5
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/5.4.1
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/6
Found candidate GCC installation: /usr/lib/gcc/x86_64-linux-gnu/6.2.0
Selected GCC installation: /usr/lib/gcc/x86_64-linux-gnu/6.2.0
Candidate multilib: .;@m64
Selected multilib: .;@m64

stdlibc++ from gcc 6.2:

$ g++-6 -v
Using built-in specs.
COLLECT_GCC=g++-6
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/6/lto-wrapper
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 6.2.0-3ubuntu11~14.04' --with-bugurl=file:///usr/share/doc/gcc-6/README.Bugs --enable-languages=c,ada,c++,java,go,d,fortran,objc,obj-c++ --prefix=/usr --program-suffix=-6 --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=gcc4-compatible --disable-libstdcxx-dual-abi --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --disable-browser-plugin --enable-java-awt=gtk --enable-gtk-cairo --with-java-home=/usr/lib/jvm/java-1.5.0-gcj-6-amd64/jre --enable-java-home --with-jvm-root-dir=/usr/lib/jvm/java-1.5.0-gcj-6-amd64 --with-jvm-jar-dir=/usr/lib/jvm-exports/java-1.5.0-gcj-6-amd64 --with-arch-directory=amd64 --with-ecj-jar=/usr/share/java/eclipse-ecj.jar --enable-objc-gc --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 6.2.0 20160901 (Ubuntu 6.2.0-3ubuntu11~14.04)

$ ldd bin/clang-tidy

linux-vdso.so.1 =>  (0x00007ffde1996000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f6104f2a000)
librt.so.1 => /lib/x86_64-linux-gnu/librt.so.1 (0x00007f6104d22000)
libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f6104b1e000)
libtinfo.so.5 => /lib/x86_64-linux-gnu/libtinfo.so.5 (0x00007f61048f5000)
libm.so.6 => /lib/x86_64-linux-gnu/libm.so.6 (0x00007f61045ef000)
libstdc++.so.6 => /usr/lib/x86_64-linux-gnu/libstdc++.so.6 (0x00007f61042dd000)
libgcc_s.so.1 => /lib/x86_64-linux-gnu/libgcc_s.so.1 (0x00007f61040c6000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f6103d01000)
/lib64/ld-linux-x86-64.so.2 (0x00007f6105148000)

btw, here's how I built it, in case that matters...

CC=../../4.0.0/build/Release/bin/clang CXX=../../4.0.0/build/Release/bin/clang++ \
cmake ../../llvm/ \
-GNinja \
-DLLVM_USE_SANITIZER=Address \
-DCMAKE_BUILD_TYPE=Debug \
-DLLVM_TARGETS_TO_BUILD="X86" \
-DLLVM_PARALLEL_LINK_JOBS=4

Here's a simple example that demonstrates the corruption I'm seeing:

#include "llvm/ADT/StringRef.h"

int main() {

std::string ss = "";
llvm::StringRef Ref = true ? "noexcept" : ss;
std::string s = Ref;
return 0;

}

hintonda updated this revision to Diff 101860.Jun 7 2017, 11:22 PM
  • Make sure types for ternary operator are the same.
alexfh accepted this revision.Jun 8 2017, 12:08 AM

Now that you found the root cause, this looks like a proper fix ;)

Thank you! LG

This revision is now accepted and ready to land.Jun 8 2017, 12:08 AM

Great, thanks for you help.

Could you commit this for me?

alexfh added a comment.Jun 8 2017, 6:43 AM

Great, thanks for you help.

Could you commit this for me?

Sure, running tests...

This revision was automatically updated to reflect the committed changes.