Skip to content
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,10 @@ Linters which are not language-specific:
| Kotlin | [ktfmt] | [ktlint] |
| Markdown | [Prettier] | [Vale] |
| Pkl | [pkl] | |
| PowerShell | | [PSScriptAnalyzer] |
| Protocol Buffer | [buf] | [buf lint] |
| Python | [ruff] | [bandit], [flake8], [pydoclint], [pylint], [ruff], [ty] |
| QML | [qmlformat] | [qmllint] |
| QML | [qmlformat] | [qmllint] |
| Ruby | | [RuboCop], [Standard] |
| Rust | [rustfmt] | [clippy] |
| SQL | [prettier-plugin-sql] | |
Expand Down Expand Up @@ -115,6 +116,7 @@ Linters which are not language-specific:
[vale]: https://vale.sh/
[yamlfmt]: https://github.com/google/yamlfmt
[yamllint]: https://yamllint.readthedocs.io/en/stable/
[psscriptanalyzer]: https://github.com/PowerShell/PSScriptAnalyzer
[rustfmt]: https://rust-lang.github.io/rustfmt
[stylelint]: https://stylelint.io
[clippy]: https://github.com/rust-lang/rust-clippy
Expand Down
1 change: 1 addition & 0 deletions examples/powershell/.bazelrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build:lint --aspects=//tools/lint:linters.bzl%ps_script_analyzer
3 changes: 3 additions & 0 deletions examples/powershell/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package(default_visibility = ["//visibility:public"])

exports_files(["PSScriptAnalyzerSettings.psd1"])
9 changes: 9 additions & 0 deletions examples/powershell/MODULE.aspect
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Aspect Extension setup for CLI "lint" command.
# This example is in rules_lint, so we use a local dependency.
# See https://github.com/bazel-starters/python/blob/main/MODULE.aspect
# for a real-world example.
axl_local_dep(
name = "rules_lint",
path = "../..",
auto_use_tasks = True,
)
10 changes: 10 additions & 0 deletions examples/powershell/MODULE.bazel
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"Bazel dependencies for PowerShell linting example"

bazel_dep(name = "aspect_rules_lint")
local_path_override(
module_name = "aspect_rules_lint",
path = "../..",
)

repos = use_extension("//tools:repositories.bzl", "repos")
use_repo(repos, "converttosarif", "psscriptanalyzer", "pwsh")
3 changes: 3 additions & 0 deletions examples/powershell/PSScriptAnalyzerSettings.psd1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
@{
Severity = @('Error', 'Warning', 'Information')
}
58 changes: 58 additions & 0 deletions examples/powershell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
# PowerShell Linting with PSScriptAnalyzer

This example demonstrates running [PSScriptAnalyzer](https://github.com/PowerShell/PSScriptAnalyzer)
as a Bazel lint aspect via `aspect_rules_lint`.

## Prerequisites

PSScriptAnalyzer has no Bazel ruleset, so you supply three archives via a module extension
in `tools/repositories.bzl` (bzlmod doesn't allow `http_archive` directly in `MODULE.bazel`):

- `pwsh` — [PowerShell releases](https://github.com/PowerShell/PowerShell/releases) (`linux-x64.tar.gz`)
- `psscriptanalyzer` — [PowerShell Gallery](https://www.powershellgallery.com/packages/PSScriptAnalyzer)
- `converttosarif` — [PowerShell Gallery](https://www.powershellgallery.com/packages/ConvertToSARIF)

For the sha256: download the `.nupkg` and run `sha256sum` on it (the Gallery URL redirects, so
`curl -L -o pkg.nupkg "<url>" && sha256sum pkg.nupkg`).

## Setup

See `tools/repositories.bzl` for the `http_archive` declarations and `tools/lint/linters.bzl`
for the aspect instantiation.

Key points:

- `type = "zip"` is required for Gallery packages (the redirect URL has no file extension)
- `patch_cmds = ["chmod +x pwsh"]` restores the execute bit stripped by `http_archive`
- Pass a `PSScriptAnalyzerSettings.psd1` label as `config` to customise rules (optional)

Reference the extension from `MODULE.bazel`:

```starlark
repos = use_extension("//tools:repositories.bzl", "repos")
use_repo(repos, "converttosarif", "psscriptanalyzer", "pwsh")
```

Register the aspect in `.bazelrc`:

```
build:lint --aspects=//tools/lint:linters.bzl%ps_script_analyzer
```

Opt PowerShell files into linting with a tagged filegroup (only `.ps1` and `.psm1` are linted):

```starlark
filegroup(
name = "scripts",
srcs = glob(["**/*.ps1"]),
tags = ["lint-with-psscriptanalyzer"],
)
```

## Running

```bash
bazel build --config=lint --output_groups=rules_lint_human //src:all
bazel build --config=lint --output_groups=rules_lint_machine //src:all
bazel build --config=lint --@aspect_rules_lint//lint:fail_on_violation //src:all
```
29 changes: 29 additions & 0 deletions examples/powershell/src/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
load("//tools/lint:linters.bzl", "ps_script_analyzer_test")

package(default_visibility = ["//visibility:public"])

filegroup(
name = "scripts",
srcs = glob(["**/*.ps1"]),
tags = ["lint-with-psscriptanalyzer"],
)

filegroup(
name = "clean_scripts",
srcs = ["hello.ps1"],
tags = ["lint-with-psscriptanalyzer"],
)

# bad.ps1 triggers violations — expect exit code 1
ps_script_analyzer_test(
name = "ps_script_analyzer_violations_test",
srcs = [":scripts"],
expected_exit_code = 1,
)

# hello.ps1 is clean — expect exit code 0, empty output, empty SARIF
ps_script_analyzer_test(
name = "ps_script_analyzer_clean_test",
srcs = [":clean_scripts"],
expected_exit_code = 0,
)
3 changes: 3 additions & 0 deletions examples/powershell/src/bad.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
Write-Host "Starting script"
$unused = "never read"
ls C:\Windows
6 changes: 6 additions & 0 deletions examples/powershell/src/hello.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
function Get-Greeting {
param([string]$Name = "World")
return "Hello, $Name!"
}

Write-Output (Get-Greeting)
1 change: 1 addition & 0 deletions examples/powershell/tools/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# Makes tools/ a Bazel package so MODULE.bazel can reference //tools:repositories.bzl
1 change: 1 addition & 0 deletions examples/powershell/tools/lint/BUILD
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
package(default_visibility = ["//:__subpackages__"])
13 changes: 13 additions & 0 deletions examples/powershell/tools/lint/linters.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""PSScriptAnalyzer linter aspect for the PowerShell example workspace."""

load("@aspect_rules_lint//lint:lint_test.bzl", "lint_test")
load("@aspect_rules_lint//lint:ps_script_analyzer.bzl", "lint_ps_script_analyzer_aspect")

ps_script_analyzer = lint_ps_script_analyzer_aspect(
binary = Label("@pwsh//:pwsh"),
psscriptanalyzer = Label("@psscriptanalyzer//:files"),
converttosarif = Label("@converttosarif//:files"),
config = Label("//:PSScriptAnalyzerSettings.psd1"),
)

ps_script_analyzer_test = lint_test(aspect = ps_script_analyzer)
36 changes: 36 additions & 0 deletions examples/powershell/tools/repositories.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Module extension that declares external tool archives for the PowerShell linting example."""

load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive")

def _repos_impl(_mctx):
http_archive(
name = "pwsh",
url = "https://github.com/PowerShell/PowerShell/releases/download/v7.6.1/powershell-7.6.1-linux-x64.tar.gz",
sha256 = "dfc94229767921603f7c3e1cb1ac5aa931448af7496ccf657723b6278057c415",
build_file_content = """exports_files(["pwsh"])""",
patch_cmds = ["chmod +x pwsh"],
)
http_archive(
name = "psscriptanalyzer",
url = "https://www.powershellgallery.com/api/v2/package/PSScriptAnalyzer/1.25.0",
sha256 = "14e634c828eb98efb9f40b2918ba90f139ed5eccdf663a2a747736d996995d60",
type = "zip",
build_file_content = """
package(default_visibility = ["//visibility:public"])
filegroup(name = "files", srcs = glob(["**"]))
""",
)
http_archive(
name = "converttosarif",
url = "https://www.powershellgallery.com/api/v2/package/ConvertToSARIF/1.0.0",
sha256 = "b1bdf60f029f12284dd78b2c2edc34705d4475079b4cb3d50d669da464bf3d35",
type = "zip",
build_file_content = """
package(default_visibility = ["//visibility:public"])
filegroup(name = "files", srcs = glob(["**"]))
""",
)

repos = module_extension(
implementation = _repos_impl,
)
11 changes: 10 additions & 1 deletion lint/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@ load("//lint/private:lint_aspect.bzl", "lint_options")

package(default_visibility = ["//visibility:public"])

exports_files(glob(["*.bzl"]) + ["lint_test.sh"])
exports_files(glob(["*.bzl"]) + [
"lint_test.sh",
"ps_script_analyzer_wrapper.ps1",
])

# Aliases for the built-in linters
alias(
Expand Down Expand Up @@ -183,6 +186,12 @@ bzl_library(
deps = ["//lint/private:lint_aspect"],
)

bzl_library(
name = "ps_script_analyzer",
srcs = ["ps_script_analyzer.bzl"],
deps = ["//lint/private:lint_aspect"],
)

bzl_library(
name = "keep_sorted",
srcs = ["keep_sorted.bzl"],
Expand Down
167 changes: 167 additions & 0 deletions lint/ps_script_analyzer.bzl
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"""API for declaring a PSScriptAnalyzer lint aspect that visits filegroups tagged
lint-with-psscriptanalyzer (or any rule kinds provided by the caller).

Typical usage in tools/lint/linters.bzl:

```starlark
load("@aspect_rules_lint//lint:ps_script_analyzer.bzl", "lint_ps_script_analyzer_aspect")

ps_script_analyzer = lint_ps_script_analyzer_aspect(
binary = Label("@pwsh//:pwsh"),
psscriptanalyzer = Label("@psscriptanalyzer//:files"),
converttosarif = Label("@converttosarif//:files"),
config = Label("//:PSScriptAnalyzerSettings.psd1"),
)
```

Opt PowerShell sources into linting by tagging a filegroup:

```starlark
filegroup(
name = "scripts",
srcs = glob(["**/*.ps1"]),
tags = ["lint-with-psscriptanalyzer"],
)
```
"""

load("//lint/private:lint_aspect.bzl", "LintOptionsInfo", "filter_srcs", "noop_lint_action", "output_files", "should_visit")

_MNEMONIC = "AspectRulesLintPSScriptAnalyzer"

_PS_EXTENSIONS = (".ps1", ".psm1")

def _ps_files(files):
return [f for f in files if f.basename.endswith(_PS_EXTENSIONS)]

def _ps_script_analyzer_aspect_impl(target, ctx):
if not should_visit(ctx.rule, ctx.attr._rule_kinds, ctx.attr._filegroup_tags):
return []

files_to_lint = _ps_files(filter_srcs(ctx.rule))
outputs, info = output_files(_MNEMONIC, target, ctx)

if len(files_to_lint) == 0:
noop_lint_action(ctx, outputs)
return [info]

wrapper = ctx.file._wrapper
pwsh = ctx.file._pwsh
pssa_files = ctx.files._psscriptanalyzer
sarif_files = ctx.files._converttosarif

pssa_matches = [f for f in pssa_files if f.basename == "PSScriptAnalyzer.psd1"]
if not pssa_matches:
fail("PSScriptAnalyzer.psd1 not found in the psscriptanalyzer filegroup. " +
"Ensure the http_archive for @psscriptanalyzer points to a PSScriptAnalyzer nupkg.")
pssa_psd1 = pssa_matches[0]

sarif_matches = [f for f in sarif_files if f.basename == "ConvertToSARIF.psd1"]
if not sarif_matches:
fail("ConvertToSARIF.psd1 not found in the converttosarif filegroup. " +
"Ensure the http_archive for @converttosarif points to a ConvertToSARIF nupkg.")
sarif_psd1 = sarif_matches[0]

args = ctx.actions.args()
args.add_all(files_to_lint)

# Use $PWD/ to make psd1 paths absolute: Import-Module doesn't recognise
# relative paths that don't start with ./ or / (e.g. "external/repo/foo.psd1"
# is treated as a module name search, not a file path).
cmd = (
"{pwsh} -NonInteractive -File {wrapper}" +
" -PssaPsd1 $PWD/{pssa} -SarifPsd1 $PWD/{sarif}" +
" -OutFile {outfile} -SarifFile {sariffile}"
).format(
pwsh = pwsh.path,
wrapper = wrapper.path,
pssa = pssa_psd1.path,
sarif = sarif_psd1.path,
outfile = outputs.human.out.path,
sariffile = outputs.machine.out.path,
)

action_outputs = [outputs.human.out, outputs.machine.out]

if outputs.human.exit_code:
cmd += " -HumanExitCodeFile {hec} -MachineExitCodeFile {mec}".format(
hec = outputs.human.exit_code.path,
mec = outputs.machine.exit_code.path,
)
action_outputs += [outputs.human.exit_code, outputs.machine.exit_code]

if ctx.file._config_file:
cmd += " -Settings {settings}".format(settings = ctx.file._config_file.path)

cmd += " $@"

action_inputs = files_to_lint + pssa_files + sarif_files + [wrapper]
if ctx.file._config_file:
action_inputs = action_inputs + [ctx.file._config_file]

ctx.actions.run_shell(
inputs = action_inputs,
outputs = action_outputs,
arguments = [args],
command = cmd,
tools = [pwsh],
mnemonic = _MNEMONIC,
progress_message = "Linting %{label} with PSScriptAnalyzer",
)

return [info]

def lint_ps_script_analyzer_aspect(
binary,
psscriptanalyzer,
converttosarif,
config = None,
rule_kinds = [],
filegroup_tags = ["lint-with-psscriptanalyzer"]):
"""A factory function to create a linter aspect for PSScriptAnalyzer.

Args:
binary: label of the pwsh executable (e.g. Label("@pwsh//:pwsh"))
psscriptanalyzer: label of the PSScriptAnalyzer filegroup from http_archive
converttosarif: label of the ConvertToSARIF filegroup from http_archive
config: optional label of a .psd1 settings file
rule_kinds: Bazel rule kinds to lint (default: none, use filegroup_tags instead)
filegroup_tags: filegroup tags that opt targets into linting
"""
return aspect(
implementation = _ps_script_analyzer_aspect_impl,
attrs = {
"_options": attr.label(
default = "//lint:options",
providers = [LintOptionsInfo],
),
"_pwsh": attr.label(
default = binary,
allow_single_file = True,
cfg = "exec",
),
"_psscriptanalyzer": attr.label(
default = psscriptanalyzer,
allow_files = True,
),
"_converttosarif": attr.label(
default = converttosarif,
allow_files = True,
),
"_config_file": attr.label(
default = config,
allow_single_file = True,
),
"_wrapper": attr.label(
default = "@aspect_rules_lint//lint:ps_script_analyzer_wrapper.ps1",
allow_single_file = True,
),
"_rule_kinds": attr.string_list(
default = rule_kinds,
),
"_filegroup_tags": attr.string_list(
default = filegroup_tags,
),
},
toolchains = [],
)
Loading