diff --git a/.codecov.yml b/.codecov.yml index b1699134e..9ec352888 100644 --- a/.codecov.yml +++ b/.codecov.yml @@ -1,7 +1,17 @@ -comment: - layout: "header, diff, tree, changes" - behavior: default - require_changes: false # if true: only post the comment if coverage changes - branches: null - flags: null - paths: null +comment: false +coverage: + status: + project: + default: + target: 70% # the (on purpose low) required coverage value + threshold: 2% # the permitted delta in hitting the target + patch: + default: + target: 0% # the (on purpose low) required coverage value + +# layout: "header, diff, tree, changes" +# behavior: default +# require_changes: false # if true: only post the comment if coverage changes +# branches: null +# flags: null +# paths: null diff --git a/ChangeLog b/ChangeLog index 9f056253c..17df141c5 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,8 @@ +2021-10-15 Travers Ching + + * inst/tinytest/testRcppAttributePackage: Tests `signature` attribute + * inst/tinytest/test_attribute_package.R: Tests `signature` attribute + 2021-10-13 Dirk Eddelbuettel * README.md: Switch JSS url to doi form per JSS request @@ -5,6 +10,17 @@ * man/RcppLdFlags.Rd: Idem * inst/CITATION: Only use doi entries in three citEntry blocks +2021-10-11 Dirk Eddelbuettel + + * DESCRIPTION (Version, Date): Roll minor version + * inst/include/Rcpp/config.h: Idem + + * .codecov.yml (comment): Disable codecov comments on PRs + +2021-10-10 Travers Ching + + * src/attributes.cpp: Add `signature` attribute and syntax checks + 2021-10-02 Dirk Eddelbuettel * .github/workflows/docker.yaml (jobs): Add container builder action diff --git a/DESCRIPTION b/DESCRIPTION index 4b09ca8f0..ab8d44449 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: Rcpp Title: Seamless R and C++ Integration -Version: 1.0.7.3 +Version: 1.0.7.4 Date: 2021-10-01 Author: Dirk Eddelbuettel, Romain Francois, JJ Allaire, Kevin Ushey, Qiang Kou, Nathan Russell, Inaki Ucar, Douglas Bates and John Chambers diff --git a/inst/include/Rcpp/config.h b/inst/include/Rcpp/config.h index dfbb98740..9262f27b1 100644 --- a/inst/include/Rcpp/config.h +++ b/inst/include/Rcpp/config.h @@ -30,7 +30,7 @@ #define RCPP_VERSION_STRING "1.0.7" // the current source snapshot (using four components, if a fifth is used in DESCRIPTION we ignore it) -#define RCPP_DEV_VERSION RcppDevVersion(1,0,7,3) -#define RCPP_DEV_VERSION_STRING "1.0.7.3" +#define RCPP_DEV_VERSION RcppDevVersion(1,0,7,4) +#define RCPP_DEV_VERSION_STRING "1.0.7.4" #endif diff --git a/inst/tinytest/testRcppAttributePackage/DESCRIPTION b/inst/tinytest/testRcppAttributePackage/DESCRIPTION new file mode 100644 index 000000000..5c9a88427 --- /dev/null +++ b/inst/tinytest/testRcppAttributePackage/DESCRIPTION @@ -0,0 +1,11 @@ +Package: testRcppAttributePackage +Type: Package +Title: Tests the signature attribute and other attributes in package compilation +Version: 1.0 +Date: 2021-10-09 +Author: Travers Ching +Maintainer: Dirk Eddelbuettel +Description: Small test package part of Rcpp unit tests +License: GPL (>= 2) +Imports: Rcpp (>= 1.0.7) +LinkingTo: Rcpp diff --git a/inst/tinytest/testRcppAttributePackage/NAMESPACE b/inst/tinytest/testRcppAttributePackage/NAMESPACE new file mode 100644 index 000000000..7d63c4e69 --- /dev/null +++ b/inst/tinytest/testRcppAttributePackage/NAMESPACE @@ -0,0 +1,3 @@ +useDynLib(testRcppAttributePackage, .registration=TRUE) +importFrom(Rcpp, evalCpp) +exportPattern("^[[:alpha:]]+") diff --git a/inst/tinytest/testRcppAttributePackage/src/rcpp_test.cpp b/inst/tinytest/testRcppAttributePackage/src/rcpp_test.cpp new file mode 100644 index 000000000..465c78889 --- /dev/null +++ b/inst/tinytest/testRcppAttributePackage/src/rcpp_test.cpp @@ -0,0 +1,95 @@ +#include +using namespace Rcpp; + +// [[Rcpp::interfaces(r, cpp)]] + +// [[Rcpp::export]] +List test_no_attributes(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} + +// [[Rcpp::export( signature = {x = list("{A}", "B"), verbose = getOption("verbose")} )]] +List test_signature(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} + +// [[Rcpp::export( rng = false, signature = {x = list("{A}", "B"), verbose = getOption("verbose")}, invisible = true )]] +List test_rng_false_signature_invisible_true(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} + +// [[Rcpp::export( rng = false )]] +List test_rng_false(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} + +// [[Rcpp::export( rng = true )]] +List test_rng_true(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} + +// [[Rcpp::export( signature = {x = list("{A}", "B"), verbose = getOption("verbose")}, rng = true )]] +List test_rng_true_signature(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} + + +// [[Rcpp::export( invisible = true, rng = true )]] +List test_invisible_true_rng_true(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} + +// [[Rcpp::export( invisible = true )]] +List test_invisible_true(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} + +// [[Rcpp::export( invisible = true, signature = {x = list("{A}", "B"), verbose = getOption("verbose")} )]] +List test_invisible_true_signature(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +} \ No newline at end of file diff --git a/inst/tinytest/test_attribute_package.R b/inst/tinytest/test_attribute_package.R new file mode 100644 index 000000000..563038149 --- /dev/null +++ b/inst/tinytest/test_attribute_package.R @@ -0,0 +1,180 @@ +## Copyright (C) 2010 - 2020 Dirk Eddelbuettel and Romain Francois +## Copyright (C) 2021 Dirk Eddelbuettel, Romain Francois and Travers Ching +## +## This file is part of Rcpp. +## +## Rcpp is free software: you can redistribute it and/or modify it +## under the terms of the GNU General Public License as published by +## the Free Software Foundation, either version 2 of the License, or +## (at your option) any later version. +## +## Rcpp is distributed in the hope that it will be useful, but +## WITHOUT ANY WARRANTY; without even the implied warranty of +## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +## GNU General Public License for more details. +## +## You should have received a copy of the GNU General Public License +## along with Rcpp. If not, see . + +.runThisTest <- Sys.getenv("RunAllRcppTests") == "yes" && Sys.getenv("RunVerboseRcppTests") == "yes" + +if (! .runThisTest) exit_file("Set 'RunVerboseRcppTests' and 'RunAllRcppTests' to 'yes' to run.") + +td <- tempfile() +cwd <- getwd() +dir.create(td) +pkg <- "testRcppAttributePackage" +file.copy(pkg, td, recursive = TRUE) # simpler direct path thanks to tinytest +setwd(td) +on.exit( { setwd(cwd); unlink(td, recursive = TRUE) } ) +R <- shQuote(file.path( R.home(component = "bin"), "R")) +Rcpp::compileAttributes(pkg) +cmd <- paste(R, "CMD build", pkg) +invisible(system(cmd, intern=TRUE)) +dir.create("templib") +pkgfile <- paste0(pkg, "_1.0.tar.gz") +install.packages(pkgfile, "templib", repos = NULL, type = "source") +require(pkg, lib.loc = "templib", character.only = TRUE) + +# Test Package +options(verbose=TRUE) +expect_equal(test_no_attributes(list("{A}"), FALSE),list("{A}", FALSE)) +expect_equal(test_signature(),list("{A}", TRUE)) +expect_equal(test_rng_false_signature_invisible_true(),list("{A}", TRUE)) +expect_equal(test_rng_false(list("{A}"), FALSE),list("{A}", FALSE)) +expect_equal(test_rng_true(list("{A}"), FALSE),list("{A}", FALSE)) +expect_equal(test_rng_true_signature(),list("{A}", TRUE)) +expect_equal(test_invisible_true_rng_true(list("{A}"), FALSE),list("{A}", FALSE)) +expect_equal(test_invisible_true(list("{A}"), FALSE),list("{A}", FALSE)) +expect_equal(test_invisible_true_signature(),list("{A}", TRUE)) +options(verbose=FALSE) + +# Test inline + +# test 0 +# This example should just run and not crash +Rcpp::sourceCpp(code=' +#include +using namespace Rcpp; +// [[Rcpp::export( rng = false, signature = {x=list("{A}", "B"), verbose = getOption("verbose")}, invisible = TRUE )]] +List test_inline(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +}') +expect_equal(test_inline(), list("{A}", FALSE)) +options(verbose=TRUE) +expect_equal(test_inline(), list("{A}", TRUE)) +options(verbose=FALSE) + +# test 1, from Enchufa2 +# The verbose argument should be replaced with FALSE +Rcpp::sourceCpp(code=' +#include +using namespace Rcpp; +// [[Rcpp::export( rng = false, signature = {x=list("{A}", "B"), verbose=FALSE} )]] +List test_inline(List x, bool verbose=true) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } +}') +expect_equal(test_inline(), list("{A}", FALSE)) + +# test 2, from Enchufa2 +# This second example should not compile because of missing parameter verbose +expect_error({ + Rcpp::sourceCpp(code=' + #include + using namespace Rcpp; + // [[Rcpp::export( rng = false, signature = {x=list("{A}", "B")} )]] + List test_inline(List x, bool verbose=true) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } + }') +}) + +# test 3, from Enchufa2 +# This third example should not compile because of missing end bracket } +# The bracket within the signature is taken as the end bracket, which results in +# invalid R code. There are some additional warnings due to the incorrect syntax +expect_warning({ + expect_error({ + Rcpp::sourceCpp(code=' + #include + using namespace Rcpp; + // [[Rcpp::export( rng = false, signature = {x=list("{A}", "B"), verbose=FALSE )]] + List test_inline(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } + }', verbose=T) + }) +}) + +# test 4, from Enchufa2 +# This 4th example is missing the end bracket and will not compile +expect_error({ + Rcpp::sourceCpp(code=' + #include + using namespace Rcpp; + // [[Rcpp::export( rng = false, signature = {x=list("A", "B"), verbose=FALSE )]] + List test_inline(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } + }') +}) + +# This 5th example has brackets but incorrect R syntax +expect_error({ + Rcpp::sourceCpp(code=' + #include + using namespace Rcpp; + // [[Rcpp::export( rng = false, signature = {x=list("A", ###, verbose=FALSE} )]] + List test_inline(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } + }') +}) + +# This 6th example is missing a parameter in the signature +expect_error({ + Rcpp::sourceCpp(code=' + #include + using namespace Rcpp; + // [[Rcpp::export( rng = false, signature = {x=list("A", "B")} )]] + List test_inline(List x, bool verbose) { + if(x.size() > 0) { + CharacterVector first_element = x[0]; + return List::create(first_element, verbose); + } else { + return List::create(verbose); + } + }') +}) + + +remove.packages(pkg, lib="templib") +unlink("templib", recursive = TRUE) +setwd(cwd) +unlink(pkgfile) diff --git a/src/attributes.cpp b/src/attributes.cpp index 7288f4fba..bcb525ec8 100644 --- a/src/attributes.cpp +++ b/src/attributes.cpp @@ -2,7 +2,7 @@ // attributes.cpp: Rcpp R/C++ interface class library -- Rcpp attributes // // Copyright (C) 2012 - 2020 JJ Allaire, Dirk Eddelbuettel and Romain Francois -// Copyright (C) 2021 JJ Allaire, Dirk Eddelbuettel, Romain Francois and Iñaki Ucar +// Copyright (C) 2021 JJ Allaire, Dirk Eddelbuettel, Romain Francois, Iñaki Ucar and Travers Ching // // This file is part of Rcpp. // @@ -154,6 +154,7 @@ namespace attributes { const char * const kExportName = "name"; const char * const kExportRng = "rng"; const char * const kExportInvisible = "invisible"; + const char * const kExportSignature = "signature"; const char * const kInitAttribute = "init"; const char * const kDependsAttribute = "depends"; const char * const kPluginsAttribute = "plugins"; @@ -164,6 +165,8 @@ namespace attributes { const char * const kParamValueTrue = "true"; const char * const kParamValueFALSE = "FALSE"; const char * const kParamValueTRUE = "TRUE"; + const char * const kParamBlockStart = "{;"; + const char * const kParamBlockEnd = "}"; // Type info class Type { @@ -393,6 +396,20 @@ namespace attributes { const std::vector& roxygen() const { return roxygen_; } + std::string customRSignature() const { + Param sigParam = paramNamed(kExportSignature); + std::string sig = sigParam.value(); + trimWhitespace(&sig); + if(sig.empty()) return sig; + if(sig.back() == '}') + sig = sig.substr(0, sig.size()-1); + // check sig.empty again since we deleted an element + if(sig.empty()) return sig; + if(sig.front() == '{') + sig.erase(0,1); + return sig; + } + private: std::string name_; std::vector params_; @@ -796,6 +813,8 @@ namespace attributes { std::string generateRArgList(const Function& function); + bool checkRSignature(const Function& function, std::string args); + void initializeGlobals(std::ostream& ostr); void generateCpp(std::ostream& ostr, @@ -1312,7 +1331,6 @@ namespace attributes { Attribute SourceFileAttributesParser::parseAttribute( const std::vector& match, int lineNumber) { - // Attribute name std::string name = match[1]; @@ -1368,7 +1386,8 @@ namespace attributes { else if (!value.empty() && (name != kExportName) && (name != kExportRng) && - (name != kExportInvisible)) { + (name != kExportInvisible) && + (name != kExportSignature)) { rcppExportWarning("Unrecognized parameter '" + name + "'", lineNumber); } @@ -1422,22 +1441,41 @@ namespace attributes { // Parse attribute parameters std::vector SourceFileAttributesParser::parseParameters( const std::string& input) { + std::string::size_type blockstart = input.find_first_of(kParamBlockStart); + std::string::size_type blockend = input.find_last_of(kParamBlockEnd); const std::string delimiters(","); - std::vector params; std::string::size_type current; - std::string::size_type next = -1; + std::string::size_type next = std::string::npos; + std::string::size_type signature_param_start = std::string::npos; do { // #nocov next = input.find_first_not_of(delimiters, next + 1); if (next == std::string::npos) break; // #nocov - next -= 1; - current = next + 1; - next = input.find_first_of(delimiters, current); + current = next; + do { + next = input.find_first_of(delimiters, next + 1); + } while((next >= blockstart) && (next <= blockend) && + (next != std::string::npos)); params.push_back(Param(input.substr(current, next - current))); + if(params.back().name() == kExportSignature) { + signature_param_start = current; + } } while(next != std::string::npos); + // if the signature param was found, then check that the name, + // start block and end block exist and are in the correct order + if(signature_param_start != std::string::npos) { + bool sigchecks = + signature_param_start < blockstart && + blockstart < blockend && + blockstart != std::string::npos && + blockend != std::string::npos; + if(!sigchecks) { + throw Rcpp::exception("signature parameter found but missing {}"); + } + } return params; } @@ -2443,7 +2481,14 @@ namespace attributes { // build the parameter list std::string args = generateRArgList(function); - + // check if has a custom signature + if(attribute.hasParameter(kExportSignature)) { + args = attribute.customRSignature(); + if(!checkRSignature(function, args)) { + std::string rsig_err_msg = "Missing args in " + args; + throw Rcpp::exception(rsig_err_msg.c_str()); + } + } // determine the function name std::string name = attribute.exportedName(); @@ -2747,6 +2792,34 @@ namespace attributes { return argsOstr.str(); } + bool checkRSignature(const Function& function, + std::string args) { + std::vector required_args; + const std::vector& arguments = function.arguments(); + for (size_t i = 0; i parsed_args = + Rcpp::as>(pargs_cv); + + for(size_t i=0; i