diff --git a/docs/projects/regal/architecture.md b/docs/projects/regal/architecture.md index d90ab50159..a84a9df2c6 100644 --- a/docs/projects/regal/architecture.md +++ b/docs/projects/regal/architecture.md @@ -20,8 +20,8 @@ When running Regal against a directory, like `regal lint my-policies/`, Regal do - Each linter rule (and there are almost 40 of them at the time of writing this) uses the **input**, which contains information such as the package name, what imports are used, and all the rules and the expressions they contain, to determine whether the Rego policy linted contains any violations against the rule. An example could be a rule that - [forbids shadowing](https://openpolicyagent.org/projects/regal/rules/bugs/rule-shadows-builtin) (i.e. using the same name as) - built-in functions and operators. + [forbids shadowing](https://openpolicyagent.org/projects/regal/rules/bugs/rule-shadows-builtin) + (i.e. using the same name as) built-in functions and operators. - Since rule bodies aren’t necessarily flat, but may contain nested bodies of constructs such as [comprehensions](https://www.openpolicyagent.org/docs/policy-language/#comprehensions) or [every](https://www.openpolicyagent.org/docs/policy-language/#every-keyword) blocks, many linter rules need to diff --git a/docs/projects/regal/configuration/capabilities.md b/docs/projects/regal/configuration/capabilities.md index fc1310d2c6..974657b152 100644 --- a/docs/projects/regal/configuration/capabilities.md +++ b/docs/projects/regal/configuration/capabilities.md @@ -9,11 +9,14 @@ By default, Regal will lint your policies using the [capabilities](https://www.openpolicyagent.org/docs/deployments/#capabilities) of the latest version of OPA known to Regal (i.e. the latest version of OPA at the time Regal was released). Sometimes you might want to tell Regal that some rules aren't applicable to your project (yet!). As an example, if you're running OPA v0.46.0, you likely won't -be helped by the [custom-has-key](https://openpolicyagent.org/projects/regal/rules/idiomatic/custom-has-key-construct) rule, as it -suggests using the `object.keys` built-in function introduced in OPA v0.47.0. The opposite could also be true — +be helped by the +[custom-has-key](https://openpolicyagent.org/projects/regal/rules/idiomatic/custom-has-key-construct) +rule, as it suggests using the `object.keys` built-in function introduced in OPA +v0.47.0. The opposite could also be true — sometimes new versions of OPA will invalidate rules that applied to older versions. An example of this is the upcoming introduction of `import rego.v1`, which will make -[implicit-future-keywords](https://openpolicyagent.org/projects/regal/rules/imports/implicit-future-keywords) obsolete, as importing +[implicit-future-keywords](https://openpolicyagent.org/projects/regal/rules/imports/implicit-future-keywords) +obsolete, as importing `rego.v1` automatically imports all "future" functions. Capabilities help you tell Regal which features to take into account, and rules with dependencies to capabilities diff --git a/docs/projects/regal/fixing.md b/docs/projects/regal/fixing.md index 4e33d20623..f8706bacaf 100644 --- a/docs/projects/regal/fixing.md +++ b/docs/projects/regal/fixing.md @@ -6,7 +6,8 @@ sidebar_position: 3 # Fixing Violations For each violation Regal is able to detect, there is a documentation page explaining the issue in detail and how to fix -it. For example, here's the one for the [`prefer-some-in-iteration`](https://openpolicyagent.org/projects/regal/rules/style/prefer-some-in-iteration) rule. +it. For example, here's the one for the +[`prefer-some-in-iteration`](https://openpolicyagent.org/projects/regal/rules/style/prefer-some-in-iteration) rule. Some rules are **automatically** fixable, meaning that Regal can fix the violation for you. Note that while most fixes will make minor changes to the code, some fixes make more significant modifications. As an example, the diff --git a/docs/src/components/projects/regal/RulesTable/index.js b/docs/src/components/projects/regal/RulesTable/index.js index d3a0ecdcd8..64eea8ad5d 100644 --- a/docs/src/components/projects/regal/RulesTable/index.js +++ b/docs/src/components/projects/regal/RulesTable/index.js @@ -1,69 +1,151 @@ -import React, { useState } from "react"; - import Link from "@docusaurus/Link"; +import React, { useMemo, useState } from "react"; +import ReactMarkdown from "react-markdown"; +import styles from "./styles.module.css"; import rules from "@generated/regal/default/rules.json"; -import styles from "./styles.module.css"; +export default function RegalRulesTable({ category = "" }) { + const [searchTerm, setSearchTerm] = useState(""); + const [selectedCategory, setSelectedCategory] = useState(""); -export default function RulesTable({ category }) { - const [searchQuery, setSearchQuery] = useState(""); + const preselectedCategory = category || null; - let predicates = []; + const effectiveCategory = preselectedCategory || selectedCategory; - let basePath = "./rules/"; - if (category !== undefined && category !== "") { - predicates.push((rule) => rule.id.startsWith(category + "/")); - basePath = "./"; - } + const allRows = useMemo(() => { + return Object.values(rules) + .map((rule) => { + const filePathMatch = rule.filePath.match(/\/regal\/rules\/([^/]+)\//); + const pathCategory = filePathMatch ? filePathMatch[1] : "unknown"; - if (searchQuery !== "") { - predicates.push((rule) => rule.id.includes(searchQuery.toLowerCase())); - } + const content = rule.content || ""; - const filteredRules = rules.filter(rule => { - if (predicates.length === 0) return true; + const summaryMatch = content.match(/\*\*Summary\*\*:\s*(.*?)\n/); + const typeMatch = content.match(/\*\*Type\*\*:\s*(.*?)\n/); + const fixableMatch = content.match(/\*\*Automatically fixable\*\*:\s*\[(.*?)\]\((.*?)\)/); - return predicates.map((predicate) => predicate(rule)) - .every((e) => e == true); - }); + const summaryParts = []; + + const summary = summaryMatch?.[1]?.trim(); + if (summary) summaryParts.push(summary); + + const type = typeMatch?.[1]?.trim(); + if (type) summaryParts.push(`**Type**: ${type}`); + + if (fixableMatch) { + const label = fixableMatch[1]; + const link = fixableMatch[2]; + summaryParts.push(`**Automatically fixable**: [${label}](${link})`); + } + + if (summaryParts.length === 0) return null; + + const contentCategoryMatch = content.match(/\*\*Category\*\*:\s*(.*?)\n/); + + return { + pathCategory, + displayCategory: contentCategoryMatch?.[1] || pathCategory, + id: rule.id, + content, + summary: summaryParts.join("\n\n"), + }; + }) + .filter(Boolean); + }, []); + + const categories = useMemo(() => { + const set = new Set(allRows.map((r) => r.pathCategory)); + return Array.from(set).sort(); + }, [allRows]); + + const filteredRows = useMemo(() => { + const term = searchTerm.toLowerCase(); + return allRows + .map((row) => { + if (!term) return { ...row, contentOnlyMatch: false }; + + const idMatch = row.id.toLowerCase().includes(term); + const categoryMatch = row.displayCategory.toLowerCase().includes(term); + const contentMatch = row.content.toLowerCase().includes(term); + + const contentOnlyMatch = contentMatch && !idMatch && !categoryMatch; + const matches = idMatch || categoryMatch || contentMatch; + + return matches ? { ...row, contentOnlyMatch } : null; + }) + .filter(Boolean) + .filter((row) => effectiveCategory ? row.pathCategory === effectiveCategory : true) + .sort((a, b) => a.displayCategory.localeCompare(b.displayCategory)); + }, [allRows, searchTerm, effectiveCategory]); return ( -
-
+ <> +
+ {!preselectedCategory && ( + + )} setSearchQuery(e.target.value)} - className={styles.searchInput} + placeholder="Search rules..." + value={searchTerm} + onChange={(e) => setSearchTerm(e.target.value)} />
- {filteredRules.length === 0 - ?

No matching rules

- : ( - - - - - + {filteredRows.length === 0 && ( +
+

No rules found.

+
+ )} + + {filteredRows.length > 0 && ( +
RuleSummary
+ + + {!preselectedCategory && } + + + + + + {filteredRows.map((row) => ( + + {!preselectedCategory && ( + + )} + + - - - {filteredRules.map((rule) => { - return ( - - - - - ); - })} - -
CategoryIDSummary
+ {row.displayCategory} + + + {row.id} + + {row.contentOnlyMatch && ( + + Content Matches Search + + )} + + {row.summary} +
- {rule.id} - {rule.summary}
- )} -
+ ))} + + + )} + ); } diff --git a/docs/src/components/projects/regal/RulesTable/styles.module.css b/docs/src/components/projects/regal/RulesTable/styles.module.css index f4de3d6f87..c109bc3dd6 100644 --- a/docs/src/components/projects/regal/RulesTable/styles.module.css +++ b/docs/src/components/projects/regal/RulesTable/styles.module.css @@ -1,24 +1,64 @@ -.searchContainer { - margin: 1.5rem 0; -} - -.searchInput { - padding: 0.5rem; - width: 100%; - max-width: 40rem; - font-size: 1rem; -} - .table { width: 100%; border-collapse: collapse; table-layout: auto; } -.table tbody td:first-child { - width: 1; +.th { + text-align: left; + padding: 0.5rem; + border-bottom: 0.125rem solid #ccc; } -.table tbody td:last-child { +.td { + padding: 0.5rem; + border-bottom: 0.0625rem solid #eee; + vertical-align: top; +} + +.categoryCell { + padding: 0.5rem; + border-bottom: 0.0625rem solid #eee; + vertical-align: top; + white-space: nowrap; + width: 1px; +} + +.idCell { + padding: 0.5rem; + border-bottom: 0.0625rem solid #eee; + vertical-align: top; + white-space: nowrap; + width: 1px; +} + +.summaryCell { + padding: 0.5rem; + border-bottom: 0.0625rem solid #eee; + vertical-align: top; width: 100%; } + +.controls { + margin-bottom: 1rem; + display: flex; + gap: 1rem; + align-items: center; +} + +.input { + padding: 0.5rem; + font-size: 1rem; + width: 100%; +} + +.select { + padding: 0.5rem; + font-size: 1rem; +} + +.contentMatchIndicator { + margin-left: 0.5rem; + font-style: italic; + color: #666; +} diff --git a/docs/src/lib/projects/regal/loadRules.js b/docs/src/lib/projects/regal/loadRules.js index 64432992c3..db2fb1aaf4 100644 --- a/docs/src/lib/projects/regal/loadRules.js +++ b/docs/src/lib/projects/regal/loadRules.js @@ -29,7 +29,8 @@ export async function loadRules() { const id = filePath.replace(rootPath + "/", "").replace(".md", ""); acc.push({ - summary, + filePath, + content, id, });