diff --git a/macros/macros.jurand b/macros/macros.jurand index 24c69e9..4bb8c9b 100644 --- a/macros/macros.jurand +++ b/macros/macros.jurand @@ -1,3 +1,11 @@ +# java_remove - remove dependency statements from Java source files +# +# Usage: *%java_remove* [-n ]... [-p|-m|-pm ]... [file path]... +# +# Removes import / module requires statements from Java source files by matching +# them against the lists of simple class names and patterns. +%java_remove %{_bindir}/jurand -i + # java_remove_imports - remove import statements from Java source files # # Usage: *%java_remove_imports* [-n ]... [-p ]... [file path]... diff --git a/manpages/jurand.1.adoc b/manpages/jurand.1.adoc index d447352..ef8cb31 100644 --- a/manpages/jurand.1.adoc +++ b/manpages/jurand.1.adoc @@ -7,7 +7,7 @@ jurand - Java removal of annotations == SYNOPSIS -*jurand* [*-a*] [*-i*] [*-s*] [*-n*=__] [*-p*=] [__...] +*jurand* [*-a*] [*-i*] [*-s*] [*-n*=__] [*-p|-m|-pm*=__] [__...] == DESCRIPTION A tool for manipulating symbols present in `.java` source files. @@ -15,7 +15,7 @@ A tool for manipulating symbols present in `.java` source files. The tool can be used for patching `.java` sources in cases where using sed is insufficient due to Java language syntax. The tool follows Java language rules rather than applying simple regular expressions on the source code. -Currently the tool is able to remove `import` statements and annotations. +Currently the tool is able to remove `import` statements, annotations and module `requires` statements. == OPTIONS *-n*, *--name*=__:: @@ -24,6 +24,12 @@ Simple (not fully-qualified) class name. *-p*, *--pattern*=__:: Regex pattern to match names used in code. +*-m*, *--pattern*=__:: +Regex pattern to match module name requires fields used in `module-info.java` files. + +*-pm*, *--pattern*=__:: +Same as specifying *-p* and *-m* options with the same pattern + *-a*:: Also remove annotations used in code. diff --git a/src/java_symbols.hpp b/src/java_symbols.hpp index b344a44..d023eaa 100644 --- a/src/java_symbols.hpp +++ b/src/java_symbols.hpp @@ -113,6 +113,7 @@ struct Mutex struct Parameters { std::vector patterns_; + std::vector module_patterns_; String_view_set names_; bool also_remove_annotations_ = false; bool in_place_ = false; @@ -123,6 +124,7 @@ struct Strict_mode { std::atomic any_annotation_removed_ = false; Mutex>> patterns_matched_; + Mutex>> module_patterns_matched_; Mutex>> names_matched_; Mutex> files_truncated_; }; @@ -418,6 +420,25 @@ inline bool name_matches(std::string_view name, std::span pat return false; } +/*! + * Iterates over @p content to find newline starting from position @p pos. + * + * @return The position of the newline or `-1` if not found. + */ +inline std::ptrdiff_t find_newline(std::string_view content, std::ptrdiff_t pos) +{ + while (pos != std::ssize(content) and std::isspace(static_cast(content[pos]))) + { + if (content[pos] == '\n') + { + return pos; + } + ++pos; + } + + return -1; +} + /*! * Iterates over @p content to remove all import statements provided * as @p patterns and @p names. Patterns match the string representation @@ -472,20 +493,9 @@ inline std::tuple remove_imports( std::tie(symbol, end_pos) = next_symbol(content, end_pos); } - // Skip whitespace until one newline but only if a newline is found + if (auto skip_space = find_newline(content, end_pos); skip_space != -1) { - auto skip_space = end_pos; - - while (skip_space != std::ssize(content) and std::isspace(static_cast(content[skip_space]))) - { - ++skip_space; - - if (content[skip_space - 1] == '\n') - { - end_pos = skip_space; - break; - } - } + end_pos = skip_space + 1; } copy_end = end_pos; @@ -579,24 +589,136 @@ inline std::string remove_annotations(std::string_view content, std::span module_patterns) +{ + auto new_content = std::string(content); + new_content.reserve(content.size()); + + auto pos = std::ptrdiff_t(0); + pos = find_token(content, "module"); + pos = find_token(content, "{", pos); + + if (pos != std::ssize(content)) + { + ++pos; + new_content.clear(); + new_content.append(content, 0, pos); + while (pos != std::ssize(content)) + { + auto symbol = std::string_view(); + auto end_pos = std::ptrdiff_t(0); + auto old_pos = pos; + std::tie(symbol, end_pos) = next_symbol(content, pos); + + if (symbol == "requires") + { + pos = end_pos; + } + else + { + pos = find_token(content, ";", pos); + if (pos != std::ssize(content)) + { + ++pos; + } + new_content.append(content, old_pos, pos - old_pos); + continue; + } + + std::tie(symbol, end_pos) = next_symbol(content, pos); + if (symbol == "transitive") + { + pos = end_pos; + } + else if (symbol == "static") + { + pos = end_pos; + std::tie(symbol, end_pos) = next_symbol(content, pos); + if (symbol == "transitive") + { + pos = end_pos; + } + } + + auto module_name = std::string(); + + std::tie(symbol, end_pos) = next_symbol(content, pos); + while (symbol != ";") + { + if (symbol.empty()) + { + new_content.clear(); + new_content.append(content); + return new_content; + } + + module_name += symbol; + std::tie(symbol, end_pos) = next_symbol(content, end_pos); + } + + pos = end_pos; + + bool matched = false; + for (const auto& pattern : module_patterns) + { + if (std::regex_search(module_name.begin(), module_name.end(), pattern)) + { + if (strict_mode) + { + strict_mode->module_patterns_matched_.lock().get().at(pattern) = true; + } + + matched = true; + break; + } + } + + if (matched) + { + if (auto skip_space = find_newline(content, pos); skip_space != -1) + { + pos = skip_space; + } + } + else + { + new_content.append(content, old_pos, pos - old_pos); + } + } + } + + return new_content; +} + //////////////////////////////////////////////////////////////////////////////// -inline std::string handle_content(std::string_view content, const Parameters& parameters) +inline std::string handle_content(const Path_origin_entry& path, std::string_view content, const Parameters& parameters) { - auto [new_content, removed_classes] = remove_imports(content, parameters.patterns_, parameters.names_); + auto result = std::string(); - if (parameters.also_remove_annotations_) + if (path.filename() == "module-info.java") { - auto content_size = new_content.size(); - new_content = remove_annotations(new_content, parameters.patterns_, parameters.names_, removed_classes); + result = remove_jpms_requires(content, parameters.module_patterns_); + } + else + { + auto [new_content, removed_classes] = remove_imports(content, parameters.patterns_, parameters.names_); - if (strict_mode and new_content.size() < content_size) + if (parameters.also_remove_annotations_) { - strict_mode->any_annotation_removed_.store(true, std::memory_order_release); + auto content_size = new_content.size(); + new_content = remove_annotations(new_content, parameters.patterns_, parameters.names_, removed_classes); + + if (strict_mode and new_content.size() < content_size) + { + strict_mode->any_annotation_removed_.store(true, std::memory_order_release); + } } + + result = new_content; } - return new_content; + return result; } inline std::string handle_file(const Path_origin_entry& path, const Parameters& parameters) @@ -621,7 +743,7 @@ try original_content = std::string(std::istreambuf_iterator(ifs), {}); } - auto content = handle_content(original_content, parameters); + auto content = handle_content(path, original_content, parameters); if (not parameters.in_place_) { @@ -706,14 +828,29 @@ inline Parameters interpret_args(const Parameter_dict& parameters) if (auto it = parameters.find("-p"); it != parameters.end()) { - result.patterns_.reserve(it->second.size()); - for (const auto& pattern : it->second) { result.patterns_.emplace_back(pattern, std::regex_constants::extended); } } + if (auto it = parameters.find("-m"); it != parameters.end()) + { + for (const auto& pattern : it->second) + { + result.module_patterns_.emplace_back(pattern, std::regex_constants::extended); + } + } + + if (auto it = parameters.find("-pm"); it != parameters.end()) + { + for (const auto& pattern : it->second) + { + result.patterns_.emplace_back(pattern, std::regex_constants::extended); + result.module_patterns_.emplace_back(pattern, std::regex_constants::extended); + } + } + if (auto it = parameters.find("-n"); it != parameters.end()) { for (const auto& name : it->second) diff --git a/src/jurand.cpp b/src/jurand.cpp index 3775aaa..6ace3f4 100644 --- a/src/jurand.cpp +++ b/src/jurand.cpp @@ -21,6 +21,10 @@ Usage: jurand [optional flags] ... [file path]... simple (not fully-qualified) class name -p regex pattern to match names used in code + -m + regex pattern to match module name requires fields used in 'module-info.java' files + -pm + same as specifying '-p' and '-m' options with the same pattern Optional flags: -a also remove annotations used in code @@ -38,7 +42,7 @@ Usage: jurand [optional flags] ... [file path]... const auto parameters = interpret_args(parameter_dict); - if (parameters.names_.empty() and parameters.patterns_.empty()) + if (parameters.names_.empty() and parameters.patterns_.empty() and parameters.module_patterns_.empty()) { std::cout << "jurand: no matcher specified" << "\n"; return 1; @@ -113,6 +117,11 @@ Usage: jurand [optional flags] ... [file path]... strict_mode->patterns_matched_.lock().get().try_emplace(pattern); } + for (const auto& pattern : parameters.module_patterns_) + { + strict_mode->module_patterns_matched_.lock().get().try_emplace(pattern); + } + for (const auto& name : parameters.names_) { strict_mode->names_matched_.lock().get().try_emplace(name); @@ -196,6 +205,15 @@ Usage: jurand [optional flags] ... [file path]... } } + for (const auto& pattern_entry : strict_mode->module_patterns_matched_.lock().get()) + { + if (not pattern_entry.second) + { + std::cout << "jurand: strict mode: module pattern " << pattern_entry.first << " did not match anything" << "\n"; + exit_code = 3; + } + } + if (parameters.also_remove_annotations_ and not strict_mode->any_annotation_removed_.load(std::memory_order_acquire)) { std::cout << "jurand: strict mode: -a was specified but no annotation was removed" << "\n"; diff --git a/src/jurand_test.cpp b/src/jurand_test.cpp index af8abaf..2fa5e16 100644 --- a/src/jurand_test.cpp +++ b/src/jurand_test.cpp @@ -5,8 +5,8 @@ using namespace java_symbols; -template -static std::ostream &operator<<(std::ostream &os, const std::tuple &t) +template +static std::ostream& operator<<(std::ostream& os, const std::tuple& t) { return os << "(" << std::get<0>(t) << ", " << std::get<1>(t) << ")"; } diff --git a/test.sh b/test.sh index 93557cd..f4717c4 100755 --- a/test.sh +++ b/test.sh @@ -19,6 +19,9 @@ test_file() { local filename="${1}"; shift local expected="${1}"; shift + if [ -d "test_resources/${expected%/*}" ]; then + mkdir -p "target/test_resources/${expected%/*}" + fi cp "test_resources/${expected}" "target/test_resources/${expected}" run_tool "${filename}" "${@}" diff -u "target/test_resources/${filename}" "target/test_resources/${expected}" @@ -152,6 +155,10 @@ test_file "Array.java" "Array.5.java" -a -n C -n D -n E -n F test_file "Package_info.java" "Package_info.1.java" -a -n MyAnn +################################################################################ +# Tests for module-info handling +test_file "simple_module/module-info.java" "simple_module/module-info.1.java" -m "java[.]base" + ################################################################################ # Tests for tool termination on invalid sources, result is irrelevant diff --git a/test_resources/simple_module/module-info.1.java b/test_resources/simple_module/module-info.1.java new file mode 100644 index 0000000..76f1bfb --- /dev/null +++ b/test_resources/simple_module/module-info.1.java @@ -0,0 +1,49 @@ +/** + * Sample module-info.java demonstrating all JPMS constructs + */ +@Deprecated +module com.example.full.module { + + // Requires with modifiers + requires transitive java.sql; + requires static java.logging; + + // Combined modifiers + requires static transitive org.jetbrains.annotations; + + /* + * ---- EXPORTS ---- + */ + + // Unqualified export + exports com.example.api; + + // Qualified export + exports com.example.internal + to com.example.friend, + com.example.another.friend; + + /* + * ---- OPENS ---- + */ + + // Unqualified open + opens com.example.model; + + // Qualified open + opens com.example.secret + to com.fasterxml.jackson.databind, + com.google.gson; + + /* + * ---- SERVICES ---- + */ + + // Service usage + uses com.example.spi.Plugin; + + // Service provision + provides com.example.spi.Plugin + with com.example.impl.PluginImpl, + com.example.impl.AnotherPluginImpl; +} diff --git a/test_resources/simple_module/module-info.java b/test_resources/simple_module/module-info.java new file mode 100644 index 0000000..4650335 --- /dev/null +++ b/test_resources/simple_module/module-info.java @@ -0,0 +1,56 @@ +/** + * Sample module-info.java demonstrating all JPMS constructs + */ +@Deprecated +module com.example.full.module { + + /* + * ---- REQUIRES ---- + */ + + // Simple requires + requires java.base; + + // Requires with modifiers + requires transitive java.sql; + requires static java.logging; + + // Combined modifiers + requires static transitive org.jetbrains.annotations; + + /* + * ---- EXPORTS ---- + */ + + // Unqualified export + exports com.example.api; + + // Qualified export + exports com.example.internal + to com.example.friend, + com.example.another.friend; + + /* + * ---- OPENS ---- + */ + + // Unqualified open + opens com.example.model; + + // Qualified open + opens com.example.secret + to com.fasterxml.jackson.databind, + com.google.gson; + + /* + * ---- SERVICES ---- + */ + + // Service usage + uses com.example.spi.Plugin; + + // Service provision + provides com.example.spi.Plugin + with com.example.impl.PluginImpl, + com.example.impl.AnotherPluginImpl; +}