Skip to content

Refactoring with checkmate and rlang - #548

Open
PavanLomati wants to merge 12 commits into
humanpred:mainfrom
PavanLomati:checkmate-rlang-refactor
Open

Refactoring with checkmate and rlang#548
PavanLomati wants to merge 12 commits into
humanpred:mainfrom
PavanLomati:checkmate-rlang-refactor

Conversation

@PavanLomati

Copy link
Copy Markdown
Contributor

No description provided.

@PavanLomati
PavanLomati requested a review from billdenney May 27, 2026 04:18
@billdenney

Copy link
Copy Markdown
Member

This looks like it will make all of the code more consistent. Thank you!. Please merge in the current origin/main branch and resolve the conflicts.

Also, for many of the checkmate calls, you've added .var.name = "name" or similar where name is the parameter name used. Please omit those as they are not necessary since they are the default that checkmate will provide.

…ng-refactor

# Conflicts:
#	R/001-add.interval.col.R
#	R/PKNCA.options.R
#	R/assertions.R
#	R/auc.R
#	R/aucint.R
#	R/class-PKNCAdata.R
#	R/class-PKNCAdose.R
#	R/class-general.R
#	R/class-summary_PKNCAresults.R
#	R/exclude.R
#	R/interpolate.conc.R
#	R/pk.calc.all.R
#	R/pk.calc.c0.R
#	R/superposition.R
#	R/tss.R
#	R/tss.monoexponential.R
#	R/tss.stepwise.linear.R
#	tests/testthat/test-PKNCA.options.R
Comment thread R/assertions.R Outdated
end,
interval[2]
),
class = "pknca_error_interval_mismatch"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make sure that all class values differ. This is the same as the one a few lines above, and part of the goal of setting the classes is to be able to differentiate errors by class for programmatic capturing by other tools (e.g. ANCA or ruminate).

Comment thread R/assertions.R Outdated
# stricter.
stop(unit_col, call. = FALSE)
rlang::abort(
message = conditionMessage(attr(unit_col, "condition")),

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

conditionMessage() here seems like it is not required. Please remove it or let me know why it is necessary.

Comment thread R/auc.R Outdated
if ("auc.type" %in% names(list(...)))
stop("auc.type cannot be changed when calling pk.calc.auc.inf, please use pk.calc.auc")
rlang::abort(
message = paste(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please do not use paste() as that can make finding the error message harder to diagnose problems.

Comment thread R/auc.R Outdated
if ("auc.type" %in% names(list(...)))
stop("auc.type cannot be changed when calling pk.calc.auc.all, please use pk.calc.auc")
rlang::abort(
message = paste(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same comment about not using paste. Please apply that throughout.

Comment thread R/auc.R Outdated
"auc.type cannot be changed when calling pk.calc.aumc.all,",
"please use pk.calc.aumc"
),
class = "pknca_error_aumc_type_override"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please make the class unique here, too.

Comment thread R/aucint.R Outdated
#' quantification; this is used for AUCinf calculations. If provided as
#' `clast.obs` (observed clast value, default), AUCinf is AUCinf,obs. If
#' provided as `clast.pred`, AUCinf is AUCinf,pred.
#' provided as `clast.pred`, AUCinf is AUCinf,pred.#'

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An extraneous #' was added to the end of this line. Please remove it.


test_that("add.interval.col", {
# Invalid inputs fail
expect_error(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please reinstate all of the tests where they are using the new regexp, if applicable. (You can remove the "info" part.)

@billdenney

Copy link
Copy Markdown
Member

@PavanLomati

Here are the comprehensive findings:

Additional review findings

These supplement the 6 inline comments already on the diff (non-unique classes, conditionMessage(), paste() in messages, stray #'). They're organized by class, highest-leverage first. All behavioral claims were verified against the base branch.

Severity token missing from some preexisting condition classes — breaks class-based capture

Three internal "report a bug" errors are raised with a pknca_internal_* class instead of pknca_error_*:

  • pknca_internal_pk_tss_ret_non_naR/tss.R
  • pknca_internal_dose_scalingR/superposition.R
  • pknca_internal_unknown_nca_unitsR/unit-support.R

Their siblings correctly use pknca_error_internal_* (pknca_error_internal_tlast in R/auc.R, pknca_error_internal_clast_na in R/aucint.R). A handler filtering on pknca_error_* will silently miss the three pknca_internal_* ones. Please standardize on pknca_error_internal_*.

Negative-path tests deleted instead of migrated — coverage regression

Please restore the tests to match the previous intent.

Across ~10 test files, the expect_error(..., regexp=) cases that exercised the exact validation being refactored were removed rather than updated to the new message/class:

  • test-001-add.interval.col.R — entire ~20-case input-validation block (name/FUN/pretty_name/datatype/desc/depends/formalsmap) deleted
  • test-PKNCA.options.R — ~18 scalar/factor/NA/"must be a number" checks across nine options, plus PKNCA.set.summary point/spread/description checks
  • test-superpostion.Radditional.times non-numeric/negative/>tau and steady.state.tol checks
  • test-set_and_assert_intervals.R — 3 whole test_that blocks
  • Same pattern in test-time.above.R, test-pk.calc.c0.R, test-class-PKNCAconc.R, test-class-PKNCAdata.R, test-class-PKNCAdose.R, test-exclude.R, test-assertions.R, test-time.to.steady.state.R

The new behavior is now less tested than the old. Please convert these to expect_error(..., class="pknca_error_...") instead of deleting them.

checkmate semantics quietly differ from the manual checks they replaced

Please add tests for where these tests are stricter:

CI can't catch these because the negative tests (finding 2) were deleted. All verified against base:

  • finite=TRUE added where the old code accepted Inf: pk.calc.aucabove now uses assert_number(conc_above, na.ok=TRUE, finite=TRUE); old stopifnot(is.numeric, length==1) allowed Inf. conc_above=Inf now errors.
  • all-NA acceptance: assert_character/assert_numeric accept an all-NA logical vector by default, where is.character()/is.numeric() rejected it. In R/exclude.R the old else if (!is.character(reason)) became an unconditional assert_character(reason), so reason=NA now passes the type gate. (add.interval.col intentionally relies on this for FUN=NA, but the relaxation is silent elsewhere.)
  • Newly stricter, not in the original: add.interval.col adds min.chars=1 to name (rejects "") and unique=TRUE on formalsmap names; R/impute.R assert_scalar(impute) rejects a length-1 list that length(impute)==1 accepted.

None look dangerous, but they're undocumented behavior changes with no test pinning them.

Please find remaining stop and warning calls

"Replaced all stop() and warning() calls" — but genuine sites remain at the PR head:

  • stop() in R/auc_integrate.R (the idx_tlast "must occur exactly once" check — not nocov), R/prepare_data.R:165, R/time.above.R:86
  • warning() in R/pk.calc.all.R:143 (re-raises a captured condition)

.Rbuildignore references a file not in the repo

The PR adds ^coverage_check\.R$ and strips the trailing newline, but coverage_check.R is not part of the PR or repo — a leftover local-scratch reference. Please drop the line and restore the trailing newline.

Whitespace / formatting issues

Pervasive enough to be a class:

  • ) } — closing brace fused onto an abort()/warn() close paren: ~11 sites (e.g. R/auc.R, R/class-PKNCAdata.R, R/superposition.R, R/tss.monoexponential.R, R/exclude.R, R/class-general.R, R/001-add.interval.col.R)
  • Zero/under-indentation: R/pk.calc.c0.R (assert_number + if at column 0), R/PKNCA.options.R (if (default && check)), R/class-summary_PKNCAresults.R (} else if)
  • Comments displaced onto code lines: R/PKNCA.options.R (assert_number(...) # Must be between 0 and 1), R/aucint.R (} else if (...) { # If using clast.pred…)
  • Inconsistent ){ (no space), stray trailing whitespace, and two new "No newline at end of file" (R/pk.calc.simple.R, .Rbuildignore)

A styler/lintr pass clears all of these.

Commented-out dead code left behind (8 sites)

Beyond the #stop(unit_col, …) already noted: #c("interval", …) (R/001-add.interval.col.R), #checkmate::assert_number(… max.missing) (R/PKNCA.options.R), #checkmate::assertNumeric(x$start …) (R/check.intervals.R), #stopifnot(inherits(… o_conc/o_dose)) ×2 and # stopifnot(…cols…) ×2 (R/prepare_data.R), and the trailing assert_number(tlast)#, finite = TRUE) (R/auc_integrate.R).

PavanLomati and others added 6 commits July 27, 2026 15:42
…ng-refactor

 Conflicts:
	NEWS.md
	R/001-add.interval.col.R
	R/aucint.R
	R/pk.calc.all.R
	R/pk.calc.urine.R
	tests/testthat/test-001-add.interval.col.R
NEWS.md:
- Restore the deleted changelog entry for the unsorted concentration-time
  fix, which a merge dropped while leaving its code and test in place.
- Split two bullets that a merge glued together, and put the "Bug Fixes"
  heading back on its own line so the section renders.
- Remove the pk_nca_result_to_df() entry: that regression was introduced
  and fixed within this branch, so no released version exhibited it.
- Document the pknca_* condition class renames under Breaking changes,
  including the two sparse classes merged into one.

Revert the new S3method(AIC,list) registration. AIC.list() keeps @nord,
so AIC() dispatch on lists is unchanged from main.

Restore the base formatting of aucint.R, pk.calc.c0.R, time.above.R, and
AIC.list.R, keeping only the condition-signaling changes. These four were
reindented wholesale, which inflated the diff without changing behavior:
aucint.R alone was 991 changed lines carrying 22 real ones. The branch
diff drops from 4760 changed lines to 3184; the test suite is unchanged
at 2745 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Expanding one-line stop() calls into multi-line rlang::abort() calls left
`# nocov` on only some lines of each call, so the unmarked middle lines
still counted toward coverage. Per the contributing guide, regions of
three or more lines are bracketed with `# nocov start` / `# nocov end`;
statements of one or two lines keep a trailing marker on each line.

35 sites across 14 files. Every converted region contains exactly one
rlang::abort() or rlang::warn() call and nothing else.

Also bracket the closure in zero_len_summary(), where a pre-existing pair
of bare `#nocov` markers on the first and last line left the nine-line
body counted.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…isc_arg

The class names were built with sprintf() from the argument name, so the
six resulting classes could not be grepped for and only the three
pptestcd_cdisc ones were ever evaluated -- a typo in the pptest_cdisc
trio would have gone unnoticed. Three literal classes cover both
arguments and are exercised by the existing tests.

Replace the hand-rolled scalar and name checks with checkmate::test_string()
and checkmate::test_names(), keeping rlang::abort() so each failure retains
its class. Drop the redundant length(x) != 1 test, which
identical(names(x), "route") already implies.

Document the validation itself in NEWS: it is stricter than the previous
character-or-list check and can reject input that add.interval.col()
used to accept.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread R/001-add.interval.col.R Outdated
# Validate formalsmap
checkmate::assert_list(
x = formalsmap,
names = if (length(formalsmap) > 0) "unique" else NULL

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The complex assignment of names here can just be names = "unique" because it is unique if the length is zero.

Simplify to checkmate::assert_lits(x = formalsmap, names = "unique")

Comment thread R/assertions.R Outdated
checkmate::assert_number(x = interval[1], na.ok = FALSE, finite = TRUE)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do not add extra spaces (fix this where changed throughout, but do not do a general pass of all of PKNCA).

Comment thread R/assertions.R Outdated
checkmate::assert_character(unit, len = 1)
checkmate::assert_data_frame(data)

if (!(unit %in% names(data))) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use checkmate::assert_names(names(data), must.include = unit) here instead

Comment thread R/assertions.R Outdated
)
}

if (!is.character(data[[unit]])) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

use checkmate::assert_character(data[[unit]])

Comment thread R/auc.R Outdated
Comment on lines +182 to +187
# nocov start
rlang::abort(
message = "Unknown error with NA tlast but non-BLQ concentrations",
class = "pknca_error_internal_tlast"
)
# nocov end

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Condense this onto one line so that it can have a simpler # nocov. Consider this throughout because a single line with (especially with adding nocov start and end) vs multiple lines can often be more readable. If this causes linting issues, resolve those, too.

Also, all the rlang::abort and rlang::warn calls do not need the message argument named since it is so distinctly the first argument to the call. Do this throughout, too.

Comment thread R/auc.R
pptestcd_cdisc="AUCALL",
pptest_cdisc="AUC All"
)
pptest_cdisc="AUC All")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Revert moving the parenthesis to the same line. That is an unrelated change. Revert all similar indentation or punctuation movements.

Comment thread R/auc_integrate.R Outdated
stopifnot(length(conc) == length(time))
checkmate::assert_numeric(conc, any.missing = FALSE)
checkmate::assert_numeric(time, any.missing = FALSE)
checkmate::assert_numeric(conc, len = length(time))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The same test from this line can go one line above by asserting that time has len = length(conc) saving a line and being more readable. Consider for similar throughout.

Comment thread R/auc_integrate.R
} else {
stopifnot(is.numeric(tlast))
stopifnot(length(tlast) == 1)
checkmate::assert_number(tlast)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add the length check back in (len = 1) on this line.

Comment thread R/auc_integrate.R Outdated
"tlast was found multiple times"
}
rlang::abort(
message = sprintf(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move the complex message generation outside of the call to rlang::abort. Consider that throughout for anything more complex that e.g. a sprintf or a paste.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(This is as far as I got in this review pass. Consider all above comments in light of the remaining code and apply consistently.)

Condition signaling:
- Drop the named `message =` argument from every rlang::abort/warn/inform
  call (301 sites); it is distinctly the first argument.
- Condense 108 short calls onto one line so `# nocov` can be a trailing
  marker rather than a start/end bracket.
- Give every condition class a distinct value (17 sites across 10 reused
  names), so callers such as ANCA or ruminate can capture a specific
  failure rather than a shared one.
- Replace paste()/paste0() message assembly with sprintf templates (20
  sites) so the message a user reports can be grepped for in the source,
  and hoist the branching message in choose_interval_method() out of the
  abort call.

checkmate:
- assert_unit_col() uses assert_names(must.include=) and
  assert_character() in place of two hand-rolled classed aborts.
- add.interval.col() uses assert_list(names = "unique"); the empty-list
  conditional was dead because checkmate accepts a zero-length list.
- choose_interval_method() folds the conc/time length check into the
  time assertion via len = length(conc).
- pk_nca_result_to_df() reads warning_prep$message directly rather than
  through conditionMessage().

Formatting, limited to lines this PR introduced:
- Restore `) {`, drop trailing whitespace, and put closing braces on
  their own line (80 lines). Lines unchanged from the base are untouched.
- Remove the leftover `info` arguments from test-001-add.interval.col.R.

Tests follow the renamed classes: the Tobit half-life cases now pin
pknca_warning_halflife_too_few_points_tobit, and the assert_unit_col
cases pin checkmate's messages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Comment thread R/assertions.R Outdated
# Re-raise the unit_col error. That is better than unit_value since it is
# stricter.
stop(unit_col, call. = FALSE)
rlang::abort(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line

Comment thread R/auc_integrate.R Outdated
} else {
stop("Unknown integration method, please report a bug: ", method) # nocov
# nocov start
rlang::abort(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line because it has nocov

Comment thread R/auc_integrate.R Outdated
} else if (interval_method_extrap != "zero") {
stop("Invalid interval_method_extrap, please report a bug: ", interval_method_extrap) # nocov
# nocov start
rlang::abort(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line because it has nocov

Comment thread R/aucint.R Outdated
} else if (is.na(clast)) {
stop("Please report a bug. clast is NA and the half-life is not NA") # nocov
# nocov start
rlang::abort(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line because it has nocov

Comment thread R/aucint.R Outdated
paste(missing_times, collapse=", "))
}
warning(warning_message)
rlang::warn(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line because it should fit

Comment thread R/check.intervals.R Outdated
} else {
stop("Invalid 'values' for column specification ", n, " (please report this as a bug).") # nocov
# nocov start
rlang::abort(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line because it has nocov

Comment thread R/check.intervals.R Outdated
# intervals without using add.interval.col)
stop("Invalid interval definition with no function and multiple dependencies.") # nocov
# nocov start
rlang::abort(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line because it has nocov

Comment thread R/class-general.R Outdated
# React to using the default value, if requested
if (!missing(stop_if_default)) {
stop(stop_if_default)
rlang::abort(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line because it should fit

Comment thread R/class-general.R Outdated
)
} else if (!missing(warn_if_default)) {
warning(warn_if_default)
rlang::warn(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line

Comment thread R/class-general.R Outdated
)
} else if (!missing(message_if_default)) {
message(message_if_default)
rlang::inform(

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Simplify this to a single line

Extends the earlier pass, which only recognized a call whose message was a
string literal.  A message that is a variable or a nested sprintf() is now
flattened too, which is what left calls such as
rlang::abort(unit_col, class = ...) spread over four lines.

A call inside a `# nocov start` / `# nocov end` bracket is allowed a longer
line, since collapsing it also removes the two marker lines and leaves a
trailing `# nocov`; the limit is 175 characters there and 120 elsewhere.
44 calls collapsed and nocov brackets drop from 42 to 12.

Whitespace is only collapsed outside string literals, so no message text
changes.  The 12 remaining brackets are not single calls (.onLoad, the
lifecycle shims, a closure), wrap two statements, or would exceed 180
characters on one line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants