Skip to contents

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.

Usage

module_namespace_linter(
  io_funs = default_shiny_io_functions,
  io_args = c("inputId", "outputId"),
  ns_funs = c("ns", "NS")
)

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 and outputId 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 and NS (although we recommend to stick with the former, which is predefined in the module template).

Value

A linter closure. To be used by {lintr} only. See the first example below.

Details

How to use this linter

The easiest way is to call lint_ns() which is essentially a wrapper around:

lintr::lint_package(linters = module_namespace_linter())

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:

lintr::linters_with_tags(
 tags = NULL, packages = c("lintr", "SIAtools")
)

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:

linters: linters_with_tags(tags = NULL, packages = "SIAtools")

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.

See also

linters for a complete list of linters available in lintr.

Other linter-related functions: lint_ns()

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()
)