Skip to content

Commit e1409e8

Browse files
Unify the linters a bit (#7077)
* Introduce and document .ci/lint.R For use in both .github/workflows/code-quality.yaml and manually, especially when developing the linters. * code-quality: switch lint-c, lint-po to .ci/lint.R * code-quality: switch lint-md to .ci/lint.R * lint-md: make sure to use Rscript * Update .ci/lint.R Use str2lang(). Co-authored-by: Michael Chirico <[email protected]> * Update .ci/linters/md/heading_id_linter.R Co-authored-by: Michael Chirico <[email protected]> * Update .ci/linters/md/news_linter.R Co-authored-by: Michael Chirico <[email protected]> * lint.R: move preprocessing to a special function The preprocessing functions were too unwieldy to leave them on the command line. This makes it possible to run any lint check directly from the command line before committing code. Co-authored-by: Michael Chirico <[email protected]> --------- Co-authored-by: Michael Chirico <[email protected]>
1 parent 71734ad commit e1409e8

File tree

7 files changed

+81
-49
lines changed

7 files changed

+81
-49
lines changed

.ci/README.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@ Base R implemented helper script, [originally proposed to base R](https://svn.r-
5050

5151
Base R implemented helper script to orchestrate generation of most artifacts and to arrange them nicely. It is being used only in [_integration_ stage in GitLab CI pipeline](./../.gitlab-ci.yml).
5252

53+
### [`lint.R`](./lint.R)
54+
55+
Base R runner for the manual (non-`lintr`) lint checks to be run from GitHub Actions during the code quality check. The command line arguments are as follows:
56+
1. Path to the directory containing files defining the linters. A linter is a function that accepts one argument (typically the path to the file) and signals an error if it fails the lint check.
57+
2. Path to the directory containing files to check.
58+
3. A regular expression matching the files to check.
59+
60+
One of the files in the linter directory may define the `.preprocess` function, which must accept one file path and return a value that other linter functions will understand. The function may also return `NULL` to indicate that the file must be skipped.
61+
62+
Example command lines:
63+
64+
```sh
65+
Rscript .ci/lint.R .ci/linters/c src '[.][ch]$'
66+
Rscript .ci/lint.R .ci/linters/po po '[.]po$'
67+
Rscript .ci/lint.R .ci/linters/md . '[.]R?md$'
68+
```
69+
5370
## GitLab Open Source Program
5471

5572
We are currently part of the [GitLab for Open Source Program](https://about.gitlab.com/solutions/open-source/). This gives us 50,000 compute minutes per month for our GitLab CI. Our license needs to be renewed yearly (around July) and is currently managed by @ben-schwen.

.ci/lint.R

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
#!/usr/bin/Rscript
2+
# Runner for the manual lint checks in .ci/linters
3+
args = commandArgs(TRUE)
4+
if (identical(args, '--help')) {
5+
writeLines(c(
6+
'Usage: Rscript .ci/lint.R .ci/linters/<KIND> <WHERE> <WHAT> [PREPROCESS]',
7+
'KIND must name the directory containing the *.R files defining the linter functions.',
8+
'WHERE must name the directory containing the files to lint, e.g. "po", or "src".',
9+
"WHAT must contain the regular expression matching the files to lint, e.g., '[.]po$', or '[.][ch]$'.",
10+
))
11+
q('no')
12+
}
13+
stopifnot(`Invalid arguments, see .ci/lint.R --help` = length(args) == 3)
14+
15+
linter_env = list2env(list(.preprocess = identity))
16+
for (f in list.files(args[[1]], full.names=TRUE)) sys.source(f, linter_env)
17+
if (!length(ls(linter_env))) stop(
18+
"No linters found after sourcing files in ", dQuote(args[[1]])
19+
)
20+
21+
sources = list.files(args[[2]], pattern = args[[3]], full.names = TRUE, recursive = TRUE)
22+
if (!length(sources)) stop(
23+
"No files to lint found in directory ", dQuote(args[[2]]), " for mask ", dQuote(args[[3]])
24+
)
25+
sources = Filter(Negate(is.null), lapply(setNames(nm = sources), linter_env$.preprocess))
26+
27+
okay = TRUE
28+
for (src in names(sources))
29+
for (linter in ls(linter_env)) tryCatch(
30+
linter_env[[linter]](sources[[src]]),
31+
error = function(e) {
32+
message('Source file ', dQuote(src), ' failed lint check ', dQuote(linter), ': ', conditionMessage(e))
33+
okay <<- FALSE
34+
}
35+
)
36+
stopifnot(`Please fix the issues above.` = okay)

.ci/linters/c/00preprocess.R

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.preprocess = function (f) list(
2+
c_obj = f, lines = readLines(f),
3+
preprocessed = system2(
4+
"gcc", shQuote(c("-fpreprocessed", "-E", f)),
5+
stdout = TRUE, stderr = FALSE
6+
)
7+
)

.ci/linters/md/heading_id_linter.R

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
any_mismatch = FALSE
2-
31
# ensure that ids are limited to alphanumerics and dashes
42
# (in particular, dots and underscores break the links)
53
check_header_ids = function(md) {
4+
if (!grepl('[.]Rmd$', md)) return(invisible())
5+
md = readLines(md)
66
# A bit surprisingly, some headings don't start with a letter.
77
# We're interested in those that set an id to link to, i.e., end with {#id}.
88
heading_captures = regmatches(md, regexec("^#+ \\S.*[{]#([^}]*)[}]$", md))
@@ -14,13 +14,5 @@ check_header_ids = function(md) {
1414
"On line %d, bad heading id '%s':\n%s\n",
1515
line, heading_captures[[line]][2], heading_captures[[line]][1]
1616
))
17-
!all(good_ids)
17+
stopifnot('Please fix the vignette issues above' = all(good_ids))
1818
}
19-
20-
any_error = FALSE
21-
for (vignette in list.files('vignettes', pattern = "[.]Rmd$", recursive = TRUE, full.name = TRUE)) {
22-
cat(sprintf("Checking vignette file %s...\n", vignette))
23-
rmd_lines = readLines(vignette)
24-
any_error = check_header_ids(rmd_lines) || any_error
25-
}
26-
if (any_error) stop("Please fix the vignette issues above.")

.ci/linters/md/news_linter.R

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
1-
any_mismatch = FALSE
2-
31
# ensure that numbered list in each section is in sequence
42
check_section_numbering = function(news) {
3+
if (!grepl("NEWS", news)) return(invisible())
4+
news = readLines(news)
55
# plain '#' catches some examples; 'd' for 'data.table'
66
sections = grep("^#+ [A-Zd]", news)
77
entries = grep("^[0-9]+[.]", news)
88
entry_value = as.integer(gsub("^([0-9]+)[.].*", "\\1", news[entries]))
99
section_id = findInterval(entries, sections)
10-
10+
1111
any_mismatch = FALSE
1212
for (id in unique(section_id)) {
1313
section_entries = entry_value[section_id == id]
@@ -22,11 +22,12 @@ check_section_numbering = function(news) {
2222
paste0(" [", section_entries[!matched], " --> ", intended_value[!matched], "]", collapse="\n")
2323
))
2424
}
25-
return(any_mismatch)
25+
stopifnot("Please fix the NEWS issues above" = !any_mismatch)
2626
}
2727

2828
# ensure that GitHub link text & URL actually agree
2929
check_gh_links = function(news) {
30+
news = readLines(news)
3031
gh_links_info = gregexpr(
3132
"\\[#(?<md_number>[0-9]+)\\]\\(https://github.com/Rdatatable/data.table/(?<link_type>[^/]+)/(?<link_number>[0-9]+)\\)",
3233
news,
@@ -48,14 +49,5 @@ check_gh_links = function(news) {
4849
"In line %d, link pointing to %s %s is written #%s\n",
4950
line_number, link_type, link_number, md_number
5051
)))
51-
return(TRUE)
52-
}
53-
54-
any_error = FALSE
55-
for (news in list.files(pattern = "NEWS")) {
56-
cat(sprintf("Checking NEWS file %s...\n", news))
57-
news_lines = readLines(news)
58-
any_error = check_section_numbering(news_lines) || any_error
59-
any_error = check_gh_links(news_lines) || any_error
52+
stop("Please fix the NEWS issues above.")
6053
}
61-
if (any_error) stop("Please fix the NEWS issues above.")

.ci/linters/po/00preprocess.R

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
.preprocess = function (f) {
2+
diff_v_master = system2('git', c('diff', 'master', f), stdout=TRUE)
3+
if (length(diff_v_master)) f
4+
}

.github/workflows/code-quality.yaml

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -40,40 +40,24 @@ jobs:
4040
run: /usr/bin/sudo apt-get -y install coccinelle
4141
- name: Lint
4242
run: |
43-
linter_env = new.env()
44-
for (f in list.files('.ci/linters/c', full.names=TRUE)) sys.source(f, linter_env)
45-
for (f in list.files('src', pattern='[.][ch]$', full.names=TRUE)) {
46-
c_obj = list(path = f, lines = readLines(f))
47-
c_obj$preprocessed = system2("gcc", c("-fpreprocessed", "-E", f), stdout=TRUE, stderr=FALSE)
48-
for (linter in ls(linter_env)) linter_env[[linter]](c_obj)
49-
# TODO(#6272): Incorporate more checks from CRAN_Release
50-
}
51-
shell: Rscript {0}
43+
Rscript .ci/lint.R .ci/linters/c src '[.][ch]$'
5244
lint-po:
5345
runs-on: ubuntu-latest
5446
steps:
5547
- uses: actions/checkout@v4
5648
- uses: r-lib/actions/setup-r@v2
5749
- name: Check translations
50+
# only pay attention to files edited in the current PR, otherwise we can get
51+
# a situation like after #6424 where some untranslated messages were added
52+
# as part of non-translation maintenance, but this GHA would go red repeatedly
53+
# until a translation is added or the blank/fuzzy translations removed. We'd
54+
# rather only have the failure on one PR, then ignore these files later.
5855
run: |
59-
linter_env = new.env()
60-
for (f in list.files('.ci/linters/po', full.names=TRUE)) sys.source(f, linter_env)
61-
for (po_file in list.files(pattern = "[.]po$", full.names=TRUE)) {
62-
# only pay attention to files edited in the current PR, otherwise we can get
63-
# a situation like after #6424 where some untranslated messages were added
64-
# as part of non-translation maintenance, but this GHA would go red repeatedly
65-
# until a translation is added or the blank/fuzzy translations removed. We'd
66-
# rather only have the failure on one PR, then ignore these files later.
67-
diff_v_master = system2("git", c("diff", "master", po_file), stdout=TRUE)
68-
if (!length(diff_v_master)) next
69-
for (linter in ls(linter_env)) linter_env[[linter]](po_file)
70-
}
71-
shell: Rscript {0}
56+
Rscript .ci/lint.R .ci/linters/po po '[.]po$'
7257
lint-md:
7358
runs-on: ubuntu-latest
7459
steps:
7560
- uses: actions/checkout@v4
7661
- uses: r-lib/actions/setup-r@v2
7762
- name: Lint
78-
run: for (f in list.files('.ci/linters/md', full.names=TRUE)) source(f)
79-
shell: Rscript {0}
63+
run: Rscript .ci/lint.R .ci/linters/md . '[.]R?md$'

0 commit comments

Comments
 (0)