Skip to content

Commit ce042ea

Browse files
authored
Load related collections from YAML files (#1902)
1 parent 8175dd2 commit ce042ea

16 files changed

Lines changed: 95 additions & 21 deletions

‎.github/workflows/ci.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
REPO: ${{ github.repository }}
3939
run: |
4040
gh api "repos/$REPO/issues/$PR/comments" \
41-
-f body="README.md was edited directly. The README is generated from the YAML files in \`data/tools/\`. Please add or edit the corresponding file in \`data/tools/\` instead and do not touch README.md." \
41+
-f body="README.md was edited directly. Please edit tool entries in \`data/tools/\`, related collections in \`data/collections/\`, or text and structure in \`ci/render/templates/README.md\` instead. Leave the generated README.md out of your pull request." \
4242
--silent
4343
echo "README.md must not be edited directly." >&2
4444
exit 1

‎CONTRIBUTING.md‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,20 @@ Finally, create a pull request with all your changes. You can call `make
5757
render` to check for errors before. This is optional, because it will also be
5858
done when creating a pull request.
5959

60+
### Related collections
61+
62+
To add or update a link in “More collections”, edit a YAML file in
63+
`data/collections/`, not the README template. Each file contains `name`,
64+
`homepage`, and `description`:
65+
66+
```yaml
67+
name: Example collection
68+
homepage: https://example.com/collection
69+
description: A collection of static analysis tools for a specific language.
70+
```
71+
72+
Collections are listed alphabetically and are separate from individual tool entries.
73+
6074
### How to mark a tool as unmaintained/deprecated
6175
6276
Sometimes a tool becomes unmaintained and there's nothing wrong with that.

‎Makefile‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ help:
1616

1717
# Main rendering targets
1818
render:
19-
cargo run --manifest-path ci/Cargo.toml -p render -- --tags data/tags.yml --tools data/tools --md-out README.md --json-out data/api
19+
cargo run --manifest-path ci/Cargo.toml -p render -- --tags data/tags.yml --tools data/tools --collections data/collections --md-out README.md --json-out data/api
2020

2121
render-skip-deprecated:
22-
cargo run --manifest-path ci/Cargo.toml -p render -- --tags data/tags.yml --tools data/tools --md-out README.md --json-out data/api --skip-deprecated
22+
cargo run --manifest-path ci/Cargo.toml -p render -- --tags data/tags.yml --tools data/tools --collections data/collections --md-out README.md --json-out data/api --skip-deprecated
2323

2424
# Development targets
2525
check:

‎ci/render/src/bin/main.rs‎

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
use anyhow::{Context, Result};
22
use askama::Template;
33
use pico_args::Arguments;
4-
use render::types::{Entry, ParsedEntry, Tag, Tags, Type};
4+
use render::types::{Collection, Entry, ParsedEntry, Tag, Tags, Type};
55
use render::{check_deprecated, create_api, create_catalog};
6+
use serde::de::DeserializeOwned;
67
use slug::slugify;
78
use std::collections::BTreeMap;
89
use std::env;
@@ -14,6 +15,7 @@ use std::path::PathBuf;
1415
struct Args {
1516
tags: PathBuf,
1617
tools: PathBuf,
18+
collections: PathBuf,
1719
md_out: PathBuf,
1820
json_out: PathBuf,
1921
skip_deprecated: bool,
@@ -30,7 +32,7 @@ fn read_tags(path: PathBuf) -> Result<Tags> {
3032
Ok(serde_saphyr::from_reader(f)?)
3133
}
3234

33-
fn read_tools(path: PathBuf) -> Result<Vec<ParsedEntry>> {
35+
fn read_entries<T: DeserializeOwned>(path: PathBuf) -> Result<Vec<T>> {
3436
let dir: std::fs::ReadDir = std::fs::read_dir(path)?;
3537

3638
let files = dir
@@ -47,10 +49,11 @@ fn read_tools(path: PathBuf) -> Result<Vec<ParsedEntry>> {
4749
.inspect(|p| println!("Checking {}", p.display()))
4850
.map(|p| {
4951
let file = std::fs::File::open(p)?;
50-
let entry: ParsedEntry = serde_saphyr::from_reader(file)?;
52+
let entry = serde_saphyr::from_reader(file)
53+
.with_context(|| format!("Cannot parse {}", p.display()))?;
5154
Ok(entry)
5255
})
53-
.collect::<Result<Vec<ParsedEntry>, _>>()
56+
.collect()
5457
}
5558

5659
/// Backfills the deprecated field in the tools data from the old tools data.
@@ -81,14 +84,18 @@ async fn main() -> Result<()> {
8184
let args = Args {
8285
tags: args.value_from_os_str("--tags", parse_path)?,
8386
tools: args.value_from_os_str("--tools", parse_path)?,
87+
collections: args.value_from_os_str("--collections", parse_path)?,
8488
md_out: args.value_from_os_str("--md-out", parse_path)?,
8589
json_out: args.value_from_os_str("--json-out", parse_path)?,
8690
skip_deprecated: args.contains("--skip-deprecated"),
8791
};
8892

8993
let tags = read_tags(args.tags)?;
9094

91-
let parsed_tools = read_tools(args.tools)?;
95+
let mut collections: Vec<Collection> = read_entries(args.collections)?;
96+
collections.sort_by_cached_key(|collection| collection.name.to_lowercase());
97+
98+
let parsed_tools: Vec<ParsedEntry> = read_entries(args.tools)?;
9299
let tools: Result<Vec<Entry>> = parsed_tools
93100
.into_iter()
94101
.map(|t| Entry::from_parsed(t, &tags))
@@ -119,7 +126,7 @@ async fn main() -> Result<()> {
119126

120127
let other_tags: Vec<Tag> = tags.into_iter().filter(|t| t.kind == Type::Other).collect();
121128

122-
let catalog = create_catalog(&tools, &languages, &other_tags);
129+
let catalog = create_catalog(&tools, &languages, &other_tags, collections);
123130
fs::write(&args.md_out, catalog.render()?).context(format!(
124131
"Cannot write Markdown output to {}",
125132
args.md_out.display()
@@ -168,7 +175,20 @@ mod tests {
168175
fn parses_catalog() -> Result<()> {
169176
let data = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../data");
170177
let tags = read_tags(data.join("tags.yml"))?;
171-
let tools = read_tools(data.join("tools"))?;
178+
let tools: Vec<ParsedEntry> = read_entries(data.join("tools"))?;
179+
let collections: Vec<Collection> = read_entries(data.join("collections"))?;
180+
assert!(!collections.is_empty());
181+
let catalog = create_catalog(&[], &[], &[], collections);
182+
let markdown = catalog.render()?;
183+
for collection in &catalog.collections {
184+
assert!(!collection.name.is_empty());
185+
reqwest::Url::parse(&collection.homepage)?;
186+
assert!(!collection.description.is_empty());
187+
assert!(markdown.contains(&format!(
188+
"- [{}]({}) — {}",
189+
collection.name, collection.homepage, collection.description
190+
)));
191+
}
172192
assert!(!tags.is_empty());
173193
assert!(!tools.is_empty());
174194
for tool in tools {

‎ci/render/src/lib.rs‎

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ pub mod stats;
1010
pub mod types;
1111

1212
use std::collections::BTreeMap;
13-
use types::{Api, ApiEntry, Catalog, Entry, ParsedEntry, Tag, Type};
13+
use types::{Api, ApiEntry, Catalog, Collection, Entry, ParsedEntry, Tag, Type};
1414

1515
fn valid(entry: &ParsedEntry, tags: &[Tag]) -> Result<()> {
1616
let lints = [lints::name, lints::min_one_tag];
@@ -113,7 +113,12 @@ pub async fn check_deprecated(token: &str, entries: &mut [Entry]) -> Result<()>
113113

114114
/// Groups normalized entries for the generated README.
115115
#[must_use]
116-
pub fn create_catalog(entries: &[Entry], languages: &[Tag], other_tags: &[Tag]) -> Catalog {
116+
pub fn create_catalog(
117+
entries: &[Entry],
118+
languages: &[Tag],
119+
other_tags: &[Tag],
120+
collections: Vec<Collection>,
121+
) -> Catalog {
117122
// Multi-language tools get their own primary section instead of being repeated under
118123
// every language. They still belong in applicable non-language tag sections.
119124
let (multi, single_language): (Vec<Entry>, Vec<Entry>) =
@@ -159,6 +164,7 @@ pub fn create_catalog(entries: &[Entry], languages: &[Tag], other_tags: &[Tag])
159164
linters,
160165
others,
161166
multi,
167+
collections,
162168
}
163169
}
164170

@@ -292,6 +298,7 @@ mod tests {
292298
linters: BTreeMap::from([(tag("Rust", "rust", Type::Language), tools.clone())]),
293299
others: BTreeMap::from([(tag("Security", "security", Type::Other), tools.clone())]),
294300
multi: tools,
301+
collections: vec![],
295302
};
296303
let markdown = catalog.render()?;
297304

@@ -343,6 +350,7 @@ mod tests {
343350
linters: BTreeMap::new(),
344351
others: BTreeMap::new(),
345352
multi: tools,
353+
collections: vec![],
346354
}
347355
.render()?;
348356
assert!(!markdown.contains("Show Deprecated"));
@@ -358,6 +366,7 @@ mod tests {
358366
linters: BTreeMap::new(),
359367
others: BTreeMap::new(),
360368
multi: vec![tool],
369+
collections: vec![],
361370
}
362371
.render()?;
363372
assert_eq!(
@@ -427,7 +436,7 @@ mod tests {
427436
let languages = [python, rust];
428437
let other_tags = [ai_generated.clone()];
429438

430-
let catalog = create_catalog(std::slice::from_ref(&tool), &languages, &other_tags);
439+
let catalog = create_catalog(std::slice::from_ref(&tool), &languages, &other_tags, vec![]);
431440

432441
assert!(catalog.linters.is_empty());
433442
assert_eq!(catalog.multi.len(), 1);
@@ -451,6 +460,7 @@ mod tests {
451460
std::slice::from_ref(&tool),
452461
&[c.clone(), cpp.clone()],
453462
std::slice::from_ref(&security),
463+
vec![],
454464
);
455465

456466
assert!(catalog.multi.is_empty());

‎ci/render/src/types.rs‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,12 +212,22 @@ impl Ord for Entry {
212212

213213
pub type EntryMap = BTreeMap<Tag, Vec<Entry>>;
214214

215+
/// A related collection listed separately from individual tools.
216+
#[derive(Debug, Serialize, Deserialize)]
217+
#[serde(deny_unknown_fields)]
218+
pub struct Collection {
219+
pub name: String,
220+
pub homepage: String,
221+
pub description: String,
222+
}
223+
215224
#[derive(Debug, Serialize, Deserialize, Template)]
216225
#[template(path = "README.md")]
217226
pub struct Catalog {
218227
pub linters: EntryMap,
219228
pub others: EntryMap,
220229
pub multi: Vec<Entry>,
230+
pub collections: Vec<Collection>,
221231
}
222232

223233
impl Catalog {

‎ci/render/templates/README.md‎

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,14 +109,9 @@ Also check out the sister project, [awesome-dynamic-analysis](https://github.com
109109

110110
## More collections
111111

112-
- [Clean code linters](https://github.com/collections/clean-code-linters) — A collection of linters in github collections
113-
- [Code Quality Checker Tools For PHP Projects](https://github.com/collections/code-quality-in-php) — A collection of PHP linters in github collections
114-
- [go-tools](https://github.com/dominikh/go-tools) — A collection of tools and libraries for working with Go code, including linters and static analysis
115-
- [linters](https://github.com/mcandre/linters) — An introduction to static code analysis
116-
- [OWASP Source Code Analysis Tools](https://owasp.org/www-community/Source_Code_Analysis_Tools) — List of tools maintained by the Open Web Application Security Project
117-
- [php-static-analysis-tools](https://github.com/exakat/php-static-analysis-tools) — A reviewed list of useful PHP static analysis tools
118-
- [AppSec Santa — SAST Tools](https://appsecsanta.com/sast-tools) — Independent comparison of 30+ static analysis security testing tools with features, pricing, and alternatives
119-
- [Wikipedia](http://en.wikipedia.org/wiki/List_of_tools_for_static_code_analysis) — A list of tools for static code analysis.
112+
{% for collection in collections -%}
113+
- [{{ collection.name }}]({{ collection.homepage }}) — {{ collection.description }}
114+
{% endfor %}
120115

121116
## License
122117

‎data/README.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
This folder contains the source data for the list.
22

33
- `tools/` - one YAML file per tool
4+
- `collections/` - one YAML file per related collection in the README's “More collections” section
45
- `tags.yml` - all valid tags that can be used in tool entries
56
- `api/` - generated JSON output (do not edit manually)
67

‎data/collections/appsec-santa.yml‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: AppSec Santa — SAST Tools
2+
homepage: https://appsecsanta.com/sast-tools
3+
description: Independent comparison of 30+ static analysis security testing tools with features, pricing, and alternatives
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
name: Clean code linters
2+
homepage: https://github.com/collections/clean-code-linters
3+
description: A collection of linters in github collections

0 commit comments

Comments
 (0)