With the new {rhino} 1.8.0, we’re happy to introduce {box.linters}. This new package helps check code for projects using {box}.
Looking to upgrade your enterprise dashboard to Rhino? Check out how it’s done with the World Bank's Carbon Pricing dashboard.
In this article, we’ll show you how {box.linters}
works with {rhino} 1.8.0
and how it improves code quality and efficiency with linter functions compatible with {box}
modules.
Code Quality Practices
At Appsilon, we take code quality seriously; here’s how:
- We implement code testing using
{testthat}
for unit testing, and{shinytest2}
- We write modular code using the
{box}
package to manage namespace conflicts in enterprise-scale applications. - We include
{lintr}
in our automated CI pipelines to check that the code we write follows a standard style.
Adding end-to-end tests to your application can ensure quality and help catch bugs early. Read our blog post to learn an easy way to automate testing using Cypress and Rhino.
Challenges with Existing Linters and {box} Modules:
These are evident in our {rhino}
framework for Shiny. However, {lintr}
does not know how to handle modules and packages/libraries imported using {box}
. The following is a simple example:
We have to disable lintr::
object_usage_linter()
in {rhino}
to work around this. This means, however, that we lose valuable source code checking provided by lintr::object_usage_linter()
.
Some of the actions performed by lintr::object_usage_linter()
are checking if all of the objects (functions, variables, function parameters) you define inside closures (typically a function scope) are later used within the closure. It also performs checks in the opposite direction, checking that all of the objects (functions, variables, data, parameters) you use or call in a closure were previously defined or imported.
With a small project, leaving such code may not have any real impact. With a larger project, however, logical errors can error as the code misbehaves due to misplaced and orphaned lines.
Introducing {box.linters}: Features and Benefits
Over the past few months, we have worked to provide compatibility between {box}
and {lintr}
. Some of these were already included in {rhino} v1.7.x
.
Since then, we have moved the linter functions to their own package and are happy to announce that {box.linters}
is now available for use with{rhino}
and any projects that use {box}
.
Linter Extensions for {lintr}
{box.linters}
default box_default_linters extends lintr::
linters_with_defaults()
.
object_usage_linter
is still disabled and in place are {box}
-compatible linter functions which:
- Checks if all the attached functions exist in the modules or packages.
- Checks if all the attached modules or packages are used in the source file
- Checks if all the attached objects and functions from modules or packages are used in the source file
- Checks if all locally defined functions and objects are used in the source file
- Checks if all calls inside an R6 class exist. And that all private methods and attributes are used within the R6 class.
- Checks if all the function and data object calls made within a source file are valid.
All linter functions work with {box}
aliases for both modules/packages and functions.
{rhino}
comes with additional box::
use()
checks described in the Rhino Style Guide.
How to Use {box.linters}
{rhino}
version 1.8.0 comes with {box.linters}
support. New {rhino} projects created with rhino::
init()
come with a .lintr
file configured for {box.linters}
. As before, rhino::
lint_r()
will run static code analysis for the project.
Existing {rhino}
projects need to be migrated to 1.8.0 to take advantage of {box.linters}
.
For non-{rhino}
projects, we provide a helper function to set up the .lintr
config file.
box.linters::use_box_lintr()
This will create a .lintr
file:
linters:
linters_with_defaults(
defaults = box.linters::box_default_linters
)
encoding: "UTF-8"
Then, use the lint()
or lint_dir()
functions of {lintr}
to check your source code.
Here’s an Example:
Revisiting the box module example at the start of this article:
Notice that the original lint on str_trim()
is now resolved, but we have a new lint on dplyr[...]
. This is an expected and desired lint.
It is good practice to attach or import only those that you need. This is to avoid namespace conflicts of two or more functions or variables having the same name. R library packages export and attach many function names.
With a larger project with many packages imported, these function names can be duplicates of function names in another package. It becomes difficult to track down from which package a function comes from. {box}
keeps imported packages, modules, and the functions contained within a project under control.
{rhino} 1.8.0 GitHub Workflow Update
Included in the 1.8.0 release of {rhino}
are streamlined GitHub Actions workflow pipelines. These remove unnecessary, duplicated workflow runs. Run the following to update your existing {rhino}
project’s CI workflow.
file.copy(
system.file("templates", "github_ci", "dot.github", "workflows", "rhino-test.yml", package = "rhino"),
file.path(".github", "workflows", "rhino-test.yml")
)
Trivia
The {box.linters}
hex logo depicts two oxpeckers (Genus Buphagus). Oxpeckers are found in sub-Saharan Africa. They eat insects and ticks and are often seen perched on large wild mammals, such as rhinoceros. Actually, one of the species is known in Swahili as Askari wa kifaru which means "the rhino's guard".
Summing Up {box.linters} in {rhino} 1.8.0
In conclusion, the {box.linters}
package offers robust solutions for enhancing code quality in projects using the {box}
package. We encourage you to integrate {box.linters}
into your projects and experience the improvements firsthand.
For any feedback or support, reach out to our team on our community Slack channel or visit our GitHub repository.
Enjoyed this blog post on {box.linters}? Sign up for our weekly newsletter to stay up-to-date on the latest in Shiny and data science.