Require usage of ns()
in inputId
and outputId
arguments of UI functions in {shiny}
modules
module_namespace_linter.Rd
A custom linter to be used by {lintr}
package. Checks the functions in a
module's UI part for any missing ns()
calls. These are often omitted when
working with the plain {shiny}
or SIA modules. More details follows below.
Arguments
- io_funs
character,
{shiny}
input/output UI functions to check. Defaults to default_shiny_io_functions, which covers all native ones and several others from{plotly}
or{DT}
. The functions must include the namespace, i.e.,shiny::textInput
.- io_args
character, arguments of UI functions to check.
inputId
andoutputId
by default. These are checked even if unnamed. Named arguments that partially match are ignored and discouraged.- ns_funs
character, function names that are considered valid in order to "namespace" inputs' or outputs' IDs. Defaults to both
ns
andNS
(although we recommend to stick with the former, which is predefined in the module template).
Details
How to use this linter
The easiest way is to call lint_ns()
which is essentially a wrapper around:
Both calls use our linter for the whole package. However, note that only
module_namespace_linter
is considered. Using this custom linter with the
native ones is somewhat complicated, but not impossible. To the best of our
knowledge, the only place where the {lintr}
documentation mentions the
actual usage of external linters, is in
linters_with_tags() help page. According to that,
you can pass the following call to linters
argument in any supported
lintr::lint_*
function:
That should select all linters available in both packages.
It is also possible to set up a configuration file that enables you to
shorten calls to {lintr}
functions, use RStudio Addins to lint an active
file, or even apply linters during continuous integration workflows, e.g., in
GitHub Actions or in Travis. To opt for that, create .lintr
file at your
package's root and fill in the following line:
Then, you can use the provided addins or call lintr::lint_package()
to get
your modules checked.
What the linter does
By default, the linter looks for any inputId
or outputId
arguments of
{shiny}
's UI functions (such as numericInput or
plotOutput, respectively), and tests if the values
assigned to the arguments are all "namespaced", i.e., wrapped in ns()
function. This is crucial for inputs and outputs in the UI portion of a
module to match their counterparts in the server logic chunk.
Only {shiny}
UI calls that are inside of a tagList in a
function ("lambda" shorthand, \( )
, applies as well) are inspected. This is
because we don't want to cause false alarms for any "ordinary" {shiny}
apps
that aren't modules. All UI portions of modules are usually defined as
functions, and all input/output UI functions are inside a
tagList, so we opted for the this strategy to minimalize
false positive matches outside {shiny}
modules.
We look for any inputId
or outputId
arguments that are named as such. On
top of that, the ns()
omission is detected even if you call the function
without named arguments that would be evaluated as input or output IDs.
However, if you use partial matching (numericInput(inp = "input")
), the
actual input won't get linted, even though it should, as it is eventually
evaluated as inputId
. The same applies for arguments defined outside the
call and passed as a variable, e.g., inp <- "input"; numericInput(inputId = inp)
. That is tricky to catch in a static code analysis, which is employed
in this linter.
Examples
# will produce lints
lintr::lint(
text =
"module_ui <- function(id, imports, ...) {
tagList(
numericInput(inputId = \"input_id_without_ns\", ...)
)
}",
linter = module_namespace_linter()
)
#> ::warning file=<text>,line=3,col=32::file=<text>,line=3,col=32,[module_namespace_linter] `inputId` and `outputId` arguments must be wrapped in `ns()` inside shiny modules.
# is OK
lintr::lint(
text =
"module_ui <- function(id, imports, ...) {
tagList(
numericInput(inputId = ns(\"input_id_with_ns\"), ...)
)
}",
linter = module_namespace_linter()
)