Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .Rbuildignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
^CODEOWNERS
^vignettes.*
^release_testing*
^CLAUDE.md$

# History files
^\.Rhistory*
Expand Down
1 change: 0 additions & 1 deletion .github/workflows/smoke-tests.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ jobs:
- fontawesome
- formatR
- fs
- futile.logger
- futile.options
- glue
- highr
Expand Down
190 changes: 190 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Project Overview

pkgnet is an R package for analyzing R packages using graph theory. It builds network representations of packages to:
- Analyze function interdependencies within a package
- Map recursive package dependencies
- Trace class inheritance structures (S4, R5/Reference Classes, R6)
- Prioritize functions for unit testing based on centrality metrics

The core functionality is `CreatePackageReport()` which generates HTML reports analyzing package structure.

## Architecture

### Reporter System (R6-based)

The package uses an object-oriented architecture built on R6 classes with a hierarchical reporter system:

**Base Classes:**
- `AbstractPackageReporter` (R/AbstractPackageReporter.R): Base class for all reporters. Handles package setup via `set_package()` method.
- `AbstractGraphReporter` (R/AbstractGraphReporter.R): Extends AbstractPackageReporter for network-based reporters. Provides `nodes`, `edges`, `network_measures`, and `pkg_graph` active bindings.

**Concrete Reporters:**
- `DependencyReporter` (R/DependencyReporter.R): Analyzes recursive package dependencies
- `FunctionReporter` (R/FunctionReporter.R): Maps function call networks and test coverage
- `InheritanceReporter` (R/InheritanceReporter.R): Traces S4/R5/R6 class inheritance

**Report Generation:**
- `PackageReport` (R/CreatePackageReport.R): Aggregates multiple reporters and renders HTML via rmarkdown
- `CreatePackageReport()`: Convenience function that instantiates PackageReport with default reporters

### Graph Models

`DirectedGraph` class (R/GraphClasses.R) wraps igraph functionality and provides:
- Node and graph measure calculations
- Network visualization via visNetwork
- Integration with reporter system

### Key Patterns

1. **Reporter Lifecycle**: Instantiate → `set_package()` → extract nodes/edges → calculate measures → render
2. **Active Bindings**: Reporters use R6 active bindings for lazy evaluation of `nodes`, `edges`, and `network_measures`
3. **Namespacing**: All non-base function calls must use `::` namespace operator (e.g., `data.table::data.table()`)
4. **data.table convention**: data.table objects named with `DT` suffix (e.g., `nodesDT`)

## Commands

### Testing

Run full test suite (builds tarball and runs R CMD check):
```bash
./test.sh
```

This script:
- Builds package tarball with `R CMD build`
- Runs `R CMD check --as-cran` in isolated directory
- Fails if any WARNINGs found
- Fails if NOTEs exceed allowed count (currently 0)

Run tests interactively in R:
```R
# Set to run NOT_CRAN tests
Sys.setenv(NOT_CRAN = "true")

# Run all tests
devtools::test()

# Run specific test file
devtools::test(filter = "FunctionReporter")
```

### Test Environment

Tests use a temporary package library (`PKGNET_TEST_LIB`) with fake test packages (baseballstats, sartre, milne). Setup/teardown in:
- `tests/testthat/setup-setTestEnv.R`
- `tests/testthat/teardown-setTestEnv.R`

On CRAN, only `test-cran-true.R` runs due to complications with temporary package handling.

### Building

```bash
# Build tarball
R CMD build .

# Install locally
R CMD INSTALL pkgnet_*.tar.gz

# Build documentation
Rscript -e "devtools::document()"
```

### Coverage

```R
covr::package_coverage()
```

## Code Style

### Naming Conventions

- **R6 classes**: UpperCamelCase (e.g., `FunctionReporter`)
- **Exported functions**: UpperCamelCase (e.g., `CreatePackageReport`)
- **Methods/fields**: snake_case (e.g., `set_package`, `pkg_name`)
- **data.table objects**: camelCase ending in `DT` (e.g., `nodesDT`)

### Dependencies

- Always use `::` namespacing for non-base calls
- Add `#' @importFrom package function` in roxygen docs
- Exceptions: operators like `%>%` and `:=` don't need namespacing
- New package dependencies must be added to DESCRIPTION `Imports`

### Indentation

- Use 4 spaces (never tabs)
- Comma-first style for multi-line lists:
```r
sample_list <- list(
norm_sample = rnorm(100)
, unif_sample = runif(100)
, t_sample = rt(100, df = 100)
)
```

### Comments

- All comments above code, never beside
- Avoid comments where code is self-evident

### Roxygen Documentation

**Functions** require:
- `#' @title`
- `#' @name`
- `#' @description`
- `#' @param` (for each parameter)
- `#' @export` (if public API)

**R6 Classes** require sections:
- `#' @section Class Constructor:`
- `#' @section Public Methods:`
- `#' @section Public Members:`
- Document all public methods and active bindings

## R6 Method Support

**FunctionReporter** treats R6 methods as functions with naming convention:
- Format: `<classname>$<methodtype>$<methodname>`
- Example: `FunctionReporter$private$extract_nodes`
- Uses **generator object name** from namespace, not `classname` attribute

**InheritanceReporter** naming:
- **Reference Classes**: Uses `Class` arg from `setRefClass()`
- **R6 Classes**: Uses generator object name in namespace (not `classname` arg)

## Known Limitations

1. **FunctionReporter**:
- Non-standard evaluation can cause false positives when column names match function names
- Functions stored in lists (not namespace) are invisible
- Instantiated R6/reference object method calls not recognized
- Reference class methods not yet supported

2. **InheritanceReporter**:
- S3 classes not supported (no formal class definitions)

## Package Versioning

Follows semantic versioning (MAJOR.MINOR.PATCH):
- Development versions append `.9999` (e.g., `0.5.0.9999`)
- Release versions remove `.9999` for CRAN submission

## CI/CD

GitHub Actions workflows:
- `.github/workflows/ci.yml`: Tests on Ubuntu/macOS with R release
- `.github/workflows/release.yml`: Tests on R-devel for CRAN submission
- `.github/workflows/smoke-tests.yaml`: Runs `CreatePackageReport()` on many packages
- `.github/workflows/website.yaml`: Builds pkgdown site

## Rendering Reports

Reports use rmarkdown templates from `inst/package_report/`:
- Work done in temp directory to avoid writing to package repo
- See `PackageReport$render_report()` in R/CreatePackageReport.R:67-98
5 changes: 2 additions & 3 deletions DESCRIPTION
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,8 @@ Imports:
covr,
data.table,
DT,
futile.logger,
glue,
igraph(>= 1.3),
igraph,
knitr,
magrittr,
methods,
Expand All @@ -38,4 +37,4 @@ Suggests:
License: BSD_3_clause + file LICENSE
URL: https://github.com/uptake/pkgnet, https://uptake.github.io/pkgnet/
BugReports: https://github.com/uptake/pkgnet/issues
RoxygenNote: 7.3.1
RoxygenNote: 7.3.3
11 changes: 2 additions & 9 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -26,27 +26,20 @@ importFrom(data.table,copy)
importFrom(data.table,data.table)
importFrom(data.table,rbindlist)
importFrom(data.table,setkeyv)
importFrom(futile.logger,INFO)
importFrom(futile.logger,flog.fatal)
importFrom(futile.logger,flog.info)
importFrom(futile.logger,flog.threshold)
importFrom(futile.logger,flog.warn)
importFrom(futile.logger,logger.options)
importFrom(glue,glue)
importFrom(grDevices,colorRamp)
importFrom(grDevices,colorRampPalette)
importFrom(grDevices,rgb)
importFrom(igraph,V)
importFrom(igraph,authority_score)
importFrom(igraph,betweenness)
importFrom(igraph,centr_betw_tmax)
importFrom(igraph,centr_clo_tmax)
importFrom(igraph,centr_degree_tmax)
importFrom(igraph,centralize)
importFrom(igraph,closeness)
importFrom(igraph,degree)
importFrom(igraph,graph.edgelist)
importFrom(igraph,hub_score)
importFrom(igraph,graph_from_edgelist)
importFrom(igraph,hits_scores)
importFrom(igraph,make_empty_graph)
importFrom(igraph,neighborhood.size)
importFrom(igraph,page_rank)
Expand Down
1 change: 1 addition & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

## BUGFIXES
* Moved `rmarkdown::render` interium files to occur within a temp directory, not the installed package directory (#329 Thanks @jcarbaut!)
* Removed `futile.logger` dependency (#338)

# pkgnet 0.5.0
## NEW FEATURES
Expand Down
8 changes: 4 additions & 4 deletions R/AbstractGraphReporter.R
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,9 @@ AbstractGraphReporter <- R6::R6Class(
#' column acts the identifier. Read-only.
nodes = function(){
if (is.null(private$cache$nodes)){
private$extract_nodes()
invisible(private$extract_nodes())
}
return(private$cache$nodes)
invisible(private$cache$nodes)
},

#' @field edges A data.table, containing information about
Expand All @@ -83,9 +83,9 @@ AbstractGraphReporter <- R6::R6Class(
#' specify the node identifiers. Read-only.
edges = function(){
if (is.null(private$cache$edges)) {
private$extract_edges()
invisible(private$extract_edges())
}
return(private$cache$edges)
invisible(private$cache$edges)
},

#' @field network_measures A list, containing any measures
Expand Down
2 changes: 1 addition & 1 deletion R/FunctionReporter.R
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,7 @@ FunctionReporter <- R6::R6Class(

private$cache$nodes <- nodes

log_info(sprintf('... done extracting functions as nodes.'
log_info(sprintf('%s ... done extracting functions as nodes.'
, self$pkg_name))

return(invisible(nodes))
Expand Down
24 changes: 12 additions & 12 deletions R/GraphClasses.R
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
#' @concept Graph Classes
#' @keywords internal
#' @importFrom R6 R6Class
#' @importFrom igraph graph.edgelist make_empty_graph vertex
#' @importFrom igraph graph_from_edgelist make_empty_graph vertex
#' @importFrom data.table data.table
#' @importFrom assertthat assert_that
AbstractGraph <- R6::R6Class(
Expand Down Expand Up @@ -172,7 +172,7 @@ AbstractGraph <- R6::R6Class(
# Connected graph
if (nrow(self$edges) > 0) {
# A graph with edges
connectedGraph <- igraph::graph.edgelist(
connectedGraph <- igraph::graph_from_edgelist(
as.matrix(self$edges[,list(SOURCE,TARGET)])
, directed = directed
)
Expand Down Expand Up @@ -227,8 +227,8 @@ AbstractGraph <- R6::R6Class(
#' @concept Graph Classes
#' @importFrom R6 R6Class
#' @importFrom igraph degree closeness betweenness
#' @importFrom igraph page_rank hub_score authority_score
#' @importFrom igraph neighborhood.size vcount V
#' @importFrom igraph page_rank hits_scores
#' @importFrom igraph ego_size vcount V
#' @importFrom igraph centralize centr_degree_tmax
#' @importFrom igraph centr_clo_tmax centr_betw_tmax
#' @seealso DirectedGraphMeasures
Expand Down Expand Up @@ -324,7 +324,7 @@ DirectedGraph <- R6::R6Class(
, numRecursiveDeps = function(self){
# Calculate using out-neighborhood size with order of longest
# possible path
result <- igraph::neighborhood.size(
result <- igraph::ego_size(
graph = self$igraph
, order = igraph::vcount(self$igraph)
, mode = "out"
Expand All @@ -340,7 +340,7 @@ DirectedGraph <- R6::R6Class(
, numRecursiveRevDeps = function(self){
# Calculate using in-neighborhood size with order of longest
# possible path
result <- igraph::neighborhood.size(
result <- igraph::ego_size(
graph = self$igraph
, order = igraph::vcount(self$igraph)
, mode = "in"
Expand Down Expand Up @@ -370,18 +370,18 @@ DirectedGraph <- R6::R6Class(

# Hub Score
, hubScore = function(self){
igraph::hub_score(
igraph::hits_scores(
graph = self$igraph
, scale = TRUE
)$vector
)$hub
}

# Authority Score
, authorityScore = function(self){
igraph::authority_score(
igraph::hits_scores(
graph = self$igraph
, scale = TRUE
)$vector
)$authority
}

) #/node_measure_functions
Expand Down Expand Up @@ -477,11 +477,11 @@ DirectedGraph <- R6::R6Class(
#' [\href{https://en.wikipedia.org/wiki/Closeness_centrality}{Wikipedia}]}
#' \item{\bold{\code{numRecursiveDeps}}}{number recursive dependencies, i.e., count of all nodes reachable by following edges
#' out from this node.
#' Calculated by \code{\link[igraph:neighborhood.size]{igraph::neighborhood.size}}.
#' Calculated by \code{\link[igraph:ego_size]{igraph::ego_size}}.
#' [\href{https://en.wikipedia.org/wiki/Rooted_graph}{Wikipedia}]}
#' \item{\bold{\code{numRecursiveRevDeps}}}{number of recursive reverse dependencies (dependents), i.e., count all nodes reachable by following edges
#' into this node in reverse direction.
#' Calculated by \code{\link[igraph:neighborhood.size]{igraph::neighborhood.size}}.
#' Calculated by \code{\link[igraph:ego_size]{igraph::ego_size}}.
#' [\href{https://en.wikipedia.org/wiki/Rooted_graph}{Wikipedia}]}
#' \item{\bold{\code{betweenness}}}{betweenness centrality, a measure of
#' the number of shortest paths in graph passing through this node
Expand Down
Loading