diff --git a/DESCRIPTION b/DESCRIPTION index f1413cf..84a042a 100644 --- a/DESCRIPTION +++ b/DESCRIPTION @@ -1,6 +1,6 @@ Package: tinyrox Title: Minimal R Documentation Generator -Version: 0.3.3.4 +Version: 0.3.3.5 Authors@R: person("Troy", "Hernandez", email = "troy@cornball.ai", role = c("aut", "cre"), comment = c(ORCID = "0009-0005-4248-604X")) @@ -12,5 +12,7 @@ License: GPL-3 URL: https://github.com/cornball-ai/tinyrox BugReports: https://github.com/cornball-ai/tinyrox/issues Encoding: UTF-8 +Imports: + utils Suggests: tinytest diff --git a/NEWS.md b/NEWS.md index 6f4351a..e279637 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,3 +1,7 @@ +# tinyrox 0.3.3.5 + +* The CRAN code checker scans parse tokens (`utils::getParseData()`) instead of raw source lines (#20). Comments and string literals can no longer trigger findings, `torch.cat()` is no longer `cat()`, `print()`/`cat()` are allowed inside `print.*`/`format.*` S3 methods, and a local variable named `T` or `F` is no longer mistaken for the logical shorthand. `setwd()`/`on.exit()` pairing and `set.seed()` literals are judged within the enclosing function instead of a fixed line window. Unparseable files report one finding instead of erroring. + # tinyrox 0.3.3.4 * Documentation blocks are now strictly consecutive `#'` lines, roxygen2-style. Blocks separated by blank lines no longer merge, so an orphaned block's `@export` can't bleed into the next function and export a `@noRd` helper (#18). Orphaned blocks warn instead of being silently dropped. diff --git a/R/check_code.R b/R/check_code.R new file mode 100644 index 0000000..b1b08eb --- /dev/null +++ b/R/check_code.R @@ -0,0 +1,307 @@ +# Token-based CRAN code checking. Scans utils::getParseData() tokens +# instead of raw source lines, so comments and string literals can never +# trigger findings, and function names match whole tokens (torch.cat() is +# not cat()). + +#' Check R Code for CRAN Issues +#' +#' Scans R files for common CRAN policy violations. Files are parsed and +#' checked token-wise: comments and string literals are never flagged, +#' print()/cat() are allowed inside print./format. S3 methods, and T/F +#' shorthand is not confused with a local variable named T or F. +#' +#' @param path Path to package root directory +#' @return List with issues found +#' +#' @export +#' @examples +#' \donttest{ +#' # Create a minimal package in tempdir +#' pkg <- file.path(tempdir(), "mypkg") +#' dir.create(file.path(pkg, "R"), recursive = TRUE, showWarnings = FALSE) +#' writeLines("Package: mypkg\nTitle: Test\nVersion: 0.1.0", +#' file.path(pkg, "DESCRIPTION")) +#' writeLines("add <- function(x, y) x + y", +#' file.path(pkg, "R", "add.R")) +#' +#' check_code_cran(pkg) +#' +#' # Clean up +#' unlink(pkg, recursive = TRUE) +#' } +check_code_cran <- function(path = ".") { + r_dir <- file.path(path, "R") + if (!dir.exists(r_dir)) { + stop("No R/ directory found in ", path, call. = FALSE) + } + + r_files <- list.files(r_dir, pattern = "\\.R$", full.names = TRUE, + ignore.case = TRUE) + + if (length(r_files) == 0) { + message("No R files found") + return(invisible(list())) + } + + all_issues <- list() + + for (file in r_files) { + file_issues <- check_code_file(file) + if (length(file_issues) > 0) { + all_issues[[basename(file)]] <- file_issues + } + } + + # Report issues + if (length(all_issues) > 0) { + for (fname in names(all_issues)) { + for (issue in all_issues[[fname]]) { + warning("CRAN [", fname, ":", issue$line, "]: ", issue$message, + call. = FALSE) + } + } + } + + invisible(all_issues) +} + +#' Check One R File for CRAN Issues +#' +#' Parses the file and runs the token-level checks. A file that does not +#' parse is reported as a single issue rather than stopping the whole check. +#' +#' @param file Path to an R source file. +#' @return List of issues (each a list with line and message). +#' @keywords internal +check_code_file <- function(file) { + parsed <- tryCatch(parse(file, keep.source = TRUE), error = function(e) e) + + if (inherits(parsed, "error")) { + return(list(list(line = NA_integer_, + message = paste0("File does not parse: ", + conditionMessage(parsed))))) + } + + pd <- utils::getParseData(parsed) + if (is.null(pd) || nrow(pd) == 0) { + return(list()) + } + + check_code_tokens(pd) +} + +#' Run Token-Level CRAN Checks on Parse Data +#' +#' @param pd Parse data from utils::getParseData() with source kept (the +#' srcfile attribute is needed to reconstruct call text). +#' @return List of issues, sorted by line. +#' @keywords internal +check_code_tokens <- function(pd) { + issues <- list() + add_issue <- function(line, message) { + issues[[length(issues) + 1]] <<- list(line = line, message = message) + } + + parents <- pd$parent + names(parents) <- as.character(pd$id) + + # Top-level expression each token belongs to + pd$root <- vapply(pd$id, token_root, numeric(1), parents = parents) + + terms <- pd[pd$terminal,, drop = FALSE] + terms <- terms[order(terms$line1, terms$col1),, drop = FALSE] + + # Names assigned (or declared as formals) per top-level expression + assigned <- assigned_names(terms) + + # Name of the function each top-level expression defines (or NA) + fn_names <- vapply(unique(terms$root), root_function_name, character(1), + terms = terms) + names(fn_names) <- as.character(unique(terms$root)) + + # T/F instead of TRUE/FALSE. Only bare SYMBOL tokens: comments, strings, + # argument names (SYMBOL_SUB), and $/@ member access don't count, and a + # T or F assigned in the same top-level expression is a variable, not + # the logical shorthand. + prev_token <- c("", terms$token[-nrow(terms)]) + is_tf <- terms$token == "SYMBOL" & terms$text %in% c("T", "F") & + prev_token != "'$'" & prev_token != "'@'" + for (i in which(is_tf)) { + root_key <- as.character(terms$root[i]) + if (terms$text[i] %in% assigned[[root_key]]) { + next + } + add_issue(terms$line1[i], "Use TRUE/FALSE instead of T/F") + } + + # print()/cat() outside print./format. S3 methods + is_printcat <- terms$token == "SYMBOL_FUNCTION_CALL" & + terms$text %in% c("print", "cat") + for (i in which(is_printcat)) { + fn <- fn_names[[as.character(terms$root[i])]] + if (!is.na(fn) && grepl("^(print|format)\\.", fn)) { + next + } + add_issue(terms$line1[i], + "Avoid print()/cat() - use message() or verbose parameter") + } + + # installed.packages() + is_instpkg <- terms$token == "SYMBOL_FUNCTION_CALL" & + terms$text == "installed.packages" + for (i in which(is_instpkg)) { + add_issue(terms$line1[i], + "Avoid installed.packages() - use requireNamespace() instead") + } + + # .GlobalEnv + is_globalenv <- terms$token == "SYMBOL" & terms$text == ".GlobalEnv" + for (i in which(is_globalenv)) { + add_issue(terms$line1[i], "Avoid modifying .GlobalEnv") + } + + # options(warn = -1) + is_options <- terms$token == "SYMBOL_FUNCTION_CALL" & + terms$text == "options" + for (i in which(is_options)) { + call_text <- call_expr_text(pd, terms$id[i], parents) + if (grepl("warn\\s*=\\s*-", call_text)) { + add_issue(terms$line1[i], + "Avoid options(warn = -1) - use suppressWarnings() instead") + } + } + + # setwd() without on.exit() in the same top-level expression + is_setwd <- terms$token == "SYMBOL_FUNCTION_CALL" & terms$text == "setwd" + onexit_roots <- terms$root[terms$token == "SYMBOL_FUNCTION_CALL" & + terms$text == "on.exit"] + for (i in which(is_setwd)) { + if (terms$root[i] %in% onexit_roots) { + next + } + add_issue(terms$line1[i], "setwd() should be restored with on.exit()") + } + + # Hardcoded set.seed() in a function without a seed formal + is_setseed <- terms$token == "SYMBOL_FUNCTION_CALL" & + terms$text == "set.seed" + for (i in which(is_setseed)) { + call_text <- call_expr_text(pd, terms$id[i], parents) + if (!grepl("^set\\.seed\\s*\\(\\s*[0-9]+\\s*\\)$", call_text)) { + next + } + root_key <- as.character(terms$root[i]) + root_terms <- terms[terms$root == terms$root[i],, drop = FALSE] + has_seed_formal <- any(root_terms$token == "SYMBOL_FORMALS" & + root_terms$text == "seed") + if (!has_seed_formal) { + add_issue(terms$line1[i], + "Hardcoded set.seed() - consider adding seed parameter") + } + } + + if (length(issues) > 0) { + issues <- issues[order(vapply(issues, function(x) { + if (is.na(x$line)) 0L else as.integer(x$line) + }, integer(1)))] + } + + issues +} + +#' Find the Top-Level Expression Containing a Token +#' +#' @param id Token id from parse data. +#' @param parents Named vector mapping token id to parent id. +#' @return Id of the top-level expression. +#' @keywords internal +token_root <- function(id, parents) { + repeat { + p <- parents[[as.character(id)]] + if (is.null(p) || is.na(p) || p <= 0) { + return(as.numeric(id)) + } + id <- p + } +} + +#' Collect Assigned Names per Top-Level Expression +#' +#' A symbol counts as assigned if it is the target of <-, =, or ->, the +#' loop variable of a for(), or a function formal. Used to tell a local +#' variable named T or F apart from the logical shorthand. +#' +#' @param terms Terminal tokens sorted by position, with a root column. +#' @return Named list mapping root id to character vector of names. +#' @keywords internal +assigned_names <- function(terms) { + result <- list() + add <- function(root, name) { + key <- as.character(root) + result[[key]] <<- c(result[[key]], name) + } + + n <- nrow(terms) + for (i in seq_len(n)) { + tok <- terms$token[i] + if (tok == "SYMBOL_FORMALS") { + add(terms$root[i], terms$text[i]) + } else if (tok %in% c("LEFT_ASSIGN", "EQ_ASSIGN", "IN")) { + # Target symbol is the terminal just before <-, =, or in + if (i > 1 && terms$token[i - 1] == "SYMBOL") { + add(terms$root[i], terms$text[i - 1]) + } + } else if (tok == "RIGHT_ASSIGN") { + # Target symbol is the terminal just after -> + if (i < n && terms$token[i + 1] == "SYMBOL") { + add(terms$root[i], terms$text[i + 1]) + } + } + } + + result +} + +#' Name of the Function a Top-Level Expression Defines +#' +#' Recognizes the pattern name <- function(...) (also = and quoted or +#' backticked names). Anything else returns NA. +#' +#' @param root_id Top-level expression id. +#' @param terms Terminal tokens sorted by position, with a root column. +#' @return Function name as a string, or NA. +#' @keywords internal +root_function_name <- function(root_id, terms) { + rt <- terms[terms$root == root_id,, drop = FALSE] + if (nrow(rt) >= 3 && + rt$token[1] %in% c("SYMBOL", "STR_CONST") && + rt$token[2] %in% c("LEFT_ASSIGN", "EQ_ASSIGN") && + rt$token[3] == "FUNCTION") { + return(gsub("^[`\"']|[`\"']$", "", rt$text[1])) + } + NA_character_ +} + +#' Reconstruct the Source Text of a Call +#' +#' Given the SYMBOL_FUNCTION_CALL token id, climbs two levels to the call +#' expression and returns its source text. +#' +#' @param pd Full parse data (srcfile attribute required). +#' @param id Token id of the SYMBOL_FUNCTION_CALL. +#' @param parents Named vector mapping token id to parent id. +#' @return Call source text, or "" if unavailable. +#' @keywords internal +call_expr_text <- function(pd, id, parents) { + name_expr <- parents[[as.character(id)]] + if (is.null(name_expr) || is.na(name_expr) || name_expr <= 0) { + return("") + } + call_expr <- parents[[as.character(name_expr)]] + if (is.null(call_expr) || is.na(call_expr) || call_expr <= 0) { + return("") + } + text <- tryCatch(utils::getParseText(pd, call_expr), error = function(e) "") + paste(text, collapse = "\n") +} + diff --git a/R/cran.R b/R/cran.R index f2af038..db3d7bf 100644 --- a/R/cran.R +++ b/R/cran.R @@ -349,170 +349,6 @@ fix_description_cran <- function(path = ".", backup = TRUE) { invisible(made_changes) } -# ============================================================================ -# Code Checking Functions -# ============================================================================ - -#' Check R Code for CRAN Issues -#' -#' Scans R files for common CRAN policy violations. -#' -#' @param path Path to package root directory -#' @return List with issues found -#' -#' @export -#' @examples -#' \donttest{ -#' # Create a minimal package in tempdir -#' pkg <- file.path(tempdir(), "mypkg") -#' dir.create(file.path(pkg, "R"), recursive = TRUE, showWarnings = FALSE) -#' writeLines("Package: mypkg\nTitle: Test\nVersion: 0.1.0", -#' file.path(pkg, "DESCRIPTION")) -#' writeLines("add <- function(x, y) x + y", -#' file.path(pkg, "R", "add.R")) -#' -#' check_code_cran(pkg) -#' -#' # Clean up -#' unlink(pkg, recursive = TRUE) -#' } -check_code_cran <- function(path = ".") { - r_dir <- file.path(path, "R") - if (!dir.exists(r_dir)) { - stop("No R/ directory found in ", path, call. = FALSE) - } - - r_files <- list.files(r_dir, pattern = "\\.R$", full.names = TRUE, - ignore.case = TRUE) - - if (length(r_files) == 0) { - message("No R files found") - return(invisible(list())) - } - - all_issues <- list() - - for (file in r_files) { - lines <- readLines(file, warn = FALSE) - file_issues <- check_code_lines(lines, basename(file)) - if (length(file_issues) > 0) { - all_issues[[basename(file)]] <- file_issues - } - } - - # Report issues - if (length(all_issues) > 0) { - for (fname in names(all_issues)) { - for (issue in all_issues[[fname]]) { - warning("CRAN [", fname, ":", issue$line, "]: ", issue$message, - call. = FALSE) - } - } - } - - invisible(all_issues) -} - -#' Check Code Lines for Issues -#' -#' @param lines Character vector of code lines -#' @param filename Filename for reporting -#' @return List of issues -check_code_lines <- function(lines, filename) { - issues <- list() - - for (i in seq_along(lines)) { - line <- lines[i] - - # Skip comments - if (grepl("^\\s*#", line)) { - next - } - - # Strip string literals so patterns inside quotes don't trigger - # false positives (e.g., grepl("\\bcat\\s*\\(", ...) matching cat()) - line <- gsub('"[^"]*"', '""', line) - line <- gsub("'[^']*'", "''", line) - - # Check for T/F instead of TRUE/FALSE - # Match T or F as standalone tokens (word boundaries) - if (grepl("(^|[^A-Za-z0-9_.])T($|[^A-Za-z0-9_.])", line) || - grepl("(^|[^A-Za-z0-9_.])F($|[^A-Za-z0-9_.])", line)) { - # Exclude common false positives like T.test, F.stat, etc - if (!grepl("\\bT\\.", line) && !grepl("\\bF\\.", line) && - !grepl("\".*[TF].*\"", line) && !grepl("'.*[TF].*'", line)) { - issues <- c(issues, list(list(line = i, - message = "Use TRUE/FALSE instead of T/F"))) - } - } - - # Check for print()/cat() outside of print methods - if (grepl("\\b(print|cat)\\s*\\(", line)) { - # Skip if it's a print method definition - if (!grepl("print\\.[A-Za-z]", line) && !grepl("#.*print", line)) { - issues <- c(issues, list(list( - line = i, - message = "Avoid print()/cat() - use message() or verbose parameter" - ))) - } - } - - # Check for installed.packages() - if (grepl("\\binstalled\\.packages\\s*\\(", line)) { - issues <- c(issues, list(list( - line = i, - message = "Avoid installed.packages() - use requireNamespace() instead" - ))) - } - - # Check for .GlobalEnv - if (grepl("\\.GlobalEnv", line)) { - issues <- c(issues, list(list( - line = i, - message = "Avoid modifying .GlobalEnv" - ))) - } - - # Check for options(warn = -1) - if (grepl("options\\s*\\(\\s*warn\\s*=\\s*-", line)) { - issues <- c(issues, list(list( - line = i, - message = "Avoid options(warn = -1) - use suppressWarnings() instead" - ))) - } - - # Check for setwd() without on.exit - if (grepl("\\bsetwd\\s*\\(", line)) { - # Look for on.exit in nearby lines - context_start <- max(1, i - 5) - context_end <- min(length(lines), i + 5) - context <- paste(lines[context_start:context_end], collapse = "\n") - if (!grepl("on\\.exit", context)) { - issues <- c(issues, list(list( - line = i, - message = "setwd() should be restored with on.exit()" - ))) - } - } - - # Check for hardcoded set.seed without parameter - if (grepl("\\bset\\.seed\\s*\\(\\s*[0-9]+\\s*\\)", line)) { - # Check if it's in a function with seed parameter - # Simple heuristic: look for seed parameter in recent lines - context_start <- max(1, i - 20) - context <- paste(lines[context_start:i], collapse = "\n") - if (!grepl("seed\\s*=", context)) { - issues <- c(issues, list(list( - line = i, - message = "Hardcoded set.seed() - consider adding seed parameter" - ))) - } - } - } - - issues -} - #' Full CRAN Compliance Check #' #' Runs all CRAN compliance checks (DESCRIPTION + code). diff --git a/R/rd.R b/R/rd.R index 8aa6187..e1872bc 100644 --- a/R/rd.R +++ b/R/rd.R @@ -1,6 +1,6 @@ #' Render User-Defined @section Blocks to Rd #' -#' Emits one `\\section{title}{content}` per parsed `@section`. Content is +#' Emits one Rd section macro per parsed @section tag. Content is #' passed through verbatim as Rd (tinyrox does no markdown parsing), matching #' how the title-only macros elsewhere treat hand-written Rd markup. #' diff --git a/inst/tinytest/test_check_code.R b/inst/tinytest/test_check_code.R new file mode 100644 index 0000000..f997902 --- /dev/null +++ b/inst/tinytest/test_check_code.R @@ -0,0 +1,177 @@ +# Tests for check_code.R (token-based CRAN code checks) + +check_src <- function(lines) { + tmp <- tempfile(fileext = ".R") + on.exit(unlink(tmp)) + writeLines(lines, tmp) + tinyrox:::check_code_file(tmp) +} + +messages_of <- function(issues) { + vapply(issues, function(x) x$message, character(1)) +} + +# --- T/F shorthand --- + +# T used as logical shorthand is flagged +issues <- check_src("f <- function(x) sum(x, na.rm = T)") +expect_equal(length(issues), 1) +expect_true(grepl("TRUE/FALSE", issues[[1]]$message)) + +# Regression (#20): T/F inside comments are not flagged +issues <- check_src(c( + "f <- function(x) {", + " # output is (B, T, dim)", + " x # F is for fast", + "}" +)) +expect_equal(length(issues), 0) + +# Regression (#20): T/F inside string literals are not flagged +issues <- check_src('f <- function(x) paste("T", \'F\', x)') +expect_equal(length(issues), 0) + +# Regression (#20): a local variable named T is not the logical shorthand +issues <- check_src(c( + "f <- function(x) {", + " T <- x$size(3)", + " seq_len(T)", + "}" +)) +expect_equal(length(issues), 0) + +# T as a formal or for-loop variable is also a variable +issues <- check_src(c( + "f <- function(T) T + 1", + "g <- function(x) {", + " for (F in x) print(F)", + "}" +)) +expect_equal(length(issues), 1) # only the print() finding +expect_true(grepl("print\\(\\)/cat\\(\\)", issues[[1]]$message)) + +# A local T in one function does not excuse shorthand T in another +issues <- check_src(c( + "f <- function(x) {", + " T <- 1", + " x + T", + "}", + "g <- function(x) sum(x, na.rm = T)" +)) +expect_equal(length(issues), 1) +expect_equal(issues[[1]]$line, 5) + +# $T and @T member access are not flagged +issues <- check_src("f <- function(x) x$T + x@F") +expect_equal(length(issues), 0) + +# --- print()/cat() --- + +# Regression (#20): cat() inside a print.* S3 method is fine +issues <- check_src(c( + "print.myclass <- function(x, ...) {", + ' cat("a myclass object\\n")', + " invisible(x)", + "}" +)) +expect_equal(length(issues), 0) + +# format.* methods too +issues <- check_src(c( + "format.myclass <- function(x, ...) {", + ' cat("formatted\\n")', + "}" +)) +expect_equal(length(issues), 0) + +# cat() in a regular function is still flagged +issues <- check_src(c( + "describe <- function(x) {", + ' cat("x is", x, "\\n")', + "}" +)) +expect_equal(length(issues), 1) +expect_equal(issues[[1]]$line, 2) + +# Regression (#20): cat( inside a string literal is not a call +issues <- check_src(c( + "make_script <- function(dim) {", + ' sprintf("y = torch.cat([a, b], dim=%d)\\n", dim)', + "}" +)) +expect_equal(length(issues), 0) + +# Regression (#20): a dotted name like torch.cat() is not base cat() +issues <- check_src("f <- function(a, b) torch.cat(a, b)") +expect_equal(length(issues), 0) + +# --- other checks survive the rewrite --- + +issues <- check_src("f <- function() installed.packages()") +expect_equal(length(issues), 1) +expect_true(grepl("installed.packages", issues[[1]]$message)) + +issues <- check_src('f <- function() assign("x", 1, envir = .GlobalEnv)') +expect_equal(length(issues), 1) +expect_true(grepl("GlobalEnv", issues[[1]]$message)) + +issues <- check_src("f <- function() options(warn = -1)") +expect_equal(length(issues), 1) +expect_true(grepl("suppressWarnings", issues[[1]]$message)) + +issues <- check_src("f <- function() options(warn = 2)") +expect_equal(length(issues), 0) + +# setwd() without on.exit() is flagged +issues <- check_src(c( + "f <- function(d) {", + " setwd(d)", + "}" +)) +expect_equal(length(issues), 1) +expect_true(grepl("on.exit", issues[[1]]$message)) + +# setwd() restored with on.exit() is fine, even more than 5 lines away +# (the old line-window check missed this) +issues <- check_src(c( + "f <- function(d) {", + " old <- getwd()", + " x1 <- 1", " x2 <- 2", " x3 <- 3", " x4 <- 4", + " x5 <- 5", " x6 <- 6", " x7 <- 7", " x8 <- 8", + " setwd(d)", + " on.exit(setwd(old))", + "}" +)) +expect_equal(length(issues), 0) + +# Hardcoded set.seed() is flagged +issues <- check_src("f <- function(x) { set.seed(42); sample(x) }") +expect_equal(length(issues), 1) +expect_true(grepl("set.seed", issues[[1]]$message)) + +# set.seed() in a function with a seed formal is fine +issues <- check_src(c( + "f <- function(x, seed = 42) {", + " set.seed(42)", + " sample(x)", + "}" +)) +expect_equal(length(issues), 0) + +# set.seed(seed) with a non-literal argument is fine +issues <- check_src("f <- function(x, s) { set.seed(s); sample(x) }") +expect_equal(length(issues), 0) + +# --- file handling --- + +# A file that does not parse reports one issue instead of erroring +issues <- check_src("f <- function( {") +expect_equal(length(issues), 1) +expect_true(grepl("does not parse", issues[[1]]$message)) + +# Issues come back sorted by line +issues <- check_src(c( + "g <- function(x) sum(x, na.rm = F)", + "h <- function() installed.packages()" +)) +expect_equal(vapply(issues, function(x) x$line, numeric(1)), c(1, 2)) diff --git a/man/assigned_names.Rd b/man/assigned_names.Rd new file mode 100644 index 0000000..a8eda8c --- /dev/null +++ b/man/assigned_names.Rd @@ -0,0 +1,19 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{assigned_names} +\alias{assigned_names} +\title{Collect Assigned Names per Top-Level Expression} +\usage{ +assigned_names(terms) +} +\arguments{ +\item{terms}{Terminal tokens sorted by position, with a root column.} +} +\value{ +Named list mapping root id to character vector of names. +} +\description{ +A symbol counts as assigned if it is the target of <-, =, or ->, the +loop variable of a for(), or a function formal. Used to tell a local +variable named T or F apart from the logical shorthand. +} +\keyword{internal} diff --git a/man/call_expr_text.Rd b/man/call_expr_text.Rd new file mode 100644 index 0000000..674fe96 --- /dev/null +++ b/man/call_expr_text.Rd @@ -0,0 +1,22 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{call_expr_text} +\alias{call_expr_text} +\title{Reconstruct the Source Text of a Call} +\usage{ +call_expr_text(pd, id, parents) +} +\arguments{ +\item{pd}{Full parse data (srcfile attribute required).} + +\item{id}{Token id of the SYMBOL_FUNCTION_CALL.} + +\item{parents}{Named vector mapping token id to parent id.} +} +\value{ +Call source text, or "" if unavailable. +} +\description{ +Given the SYMBOL_FUNCTION_CALL token id, climbs two levels to the call +expression and returns its source text. +} +\keyword{internal} diff --git a/man/check_code_cran.Rd b/man/check_code_cran.Rd index 6110030..960d7b8 100644 --- a/man/check_code_cran.Rd +++ b/man/check_code_cran.Rd @@ -12,7 +12,10 @@ check_code_cran(path = ".") List with issues found } \description{ -Scans R files for common CRAN policy violations. +Scans R files for common CRAN policy violations. Files are parsed and +checked token-wise: comments and string literals are never flagged, +print()/cat() are allowed inside print./format. S3 methods, and T/F +shorthand is not confused with a local variable named T or F. } \examples{ \donttest{ diff --git a/man/check_code_file.Rd b/man/check_code_file.Rd new file mode 100644 index 0000000..41ed2bf --- /dev/null +++ b/man/check_code_file.Rd @@ -0,0 +1,18 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{check_code_file} +\alias{check_code_file} +\title{Check One R File for CRAN Issues} +\usage{ +check_code_file(file) +} +\arguments{ +\item{file}{Path to an R source file.} +} +\value{ +List of issues (each a list with line and message). +} +\description{ +Parses the file and runs the token-level checks. A file that does not +parse is reported as a single issue rather than stopping the whole check. +} +\keyword{internal} diff --git a/man/check_code_lines.Rd b/man/check_code_lines.Rd deleted file mode 100644 index ebe068a..0000000 --- a/man/check_code_lines.Rd +++ /dev/null @@ -1,18 +0,0 @@ -% tinyrox says don't edit this manually, but it can't stop you! -\name{check_code_lines} -\alias{check_code_lines} -\title{Check Code Lines for Issues} -\usage{ -check_code_lines(lines, filename) -} -\arguments{ -\item{lines}{Character vector of code lines} - -\item{filename}{Filename for reporting} -} -\value{ -List of issues -} -\description{ -Check Code Lines for Issues -} diff --git a/man/check_code_tokens.Rd b/man/check_code_tokens.Rd new file mode 100644 index 0000000..4d48df3 --- /dev/null +++ b/man/check_code_tokens.Rd @@ -0,0 +1,18 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{check_code_tokens} +\alias{check_code_tokens} +\title{Run Token-Level CRAN Checks on Parse Data} +\usage{ +check_code_tokens(pd) +} +\arguments{ +\item{pd}{Parse data from utils::getParseData() with source kept (the +srcfile attribute is needed to reconstruct call text).} +} +\value{ +List of issues, sorted by line. +} +\description{ +Run Token-Level CRAN Checks on Parse Data +} +\keyword{internal} diff --git a/man/get_maintainer_from_desc.Rd b/man/get_maintainer_from_desc.Rd deleted file mode 100644 index d47c759..0000000 --- a/man/get_maintainer_from_desc.Rd +++ /dev/null @@ -1,17 +0,0 @@ -% tinyrox says don't edit this manually, but it can't stop you! -\name{get_maintainer_from_desc} -\alias{get_maintainer_from_desc} -\title{Get Maintainer Info from DESCRIPTION} -\usage{ -get_maintainer_from_desc(path) -} -\arguments{ -\item{path}{Package root path.} -} -\value{ -Formatted author string or NULL. -} -\description{ -Reads Authors@R from DESCRIPTION and formats maintainer for Rd. -} -\keyword{internal} diff --git a/man/render_sections.Rd b/man/render_sections.Rd index 4ec1b33..1fd5e62 100644 --- a/man/render_sections.Rd +++ b/man/render_sections.Rd @@ -12,7 +12,7 @@ render_sections(sections) Character vector of Rd lines (empty when there are no sections). } \description{ -Emits one `\\section{title}{content}` per parsed `@section`. Content is +Emits one Rd section macro per parsed @section tag. Content is passed through verbatim as Rd (tinyrox does no markdown parsing), matching how the title-only macros elsewhere treat hand-written Rd markup. } diff --git a/man/root_function_name.Rd b/man/root_function_name.Rd new file mode 100644 index 0000000..aecce9d --- /dev/null +++ b/man/root_function_name.Rd @@ -0,0 +1,20 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{root_function_name} +\alias{root_function_name} +\title{Name of the Function a Top-Level Expression Defines} +\usage{ +root_function_name(root_id, terms) +} +\arguments{ +\item{root_id}{Top-level expression id.} + +\item{terms}{Terminal tokens sorted by position, with a root column.} +} +\value{ +Function name as a string, or NA. +} +\description{ +Recognizes the pattern name <- function(...) (also = and quoted or +backticked names). Anything else returns NA. +} +\keyword{internal} diff --git a/man/token_root.Rd b/man/token_root.Rd new file mode 100644 index 0000000..812e30f --- /dev/null +++ b/man/token_root.Rd @@ -0,0 +1,19 @@ +% tinyrox says don't edit this manually, but it can't stop you! +\name{token_root} +\alias{token_root} +\title{Find the Top-Level Expression Containing a Token} +\usage{ +token_root(id, parents) +} +\arguments{ +\item{id}{Token id from parse data.} + +\item{parents}{Named vector mapping token id to parent id.} +} +\value{ +Id of the top-level expression. +} +\description{ +Find the Top-Level Expression Containing a Token +} +\keyword{internal}