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
24 changes: 19 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ jobs:
name: Lint
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
Expand All @@ -40,7 +42,7 @@ jobs:
go-version: 'stable'
cache: true
- name: golangci-lint
uses: golangci/golangci-lint-action@971e284b6050e8a5805b1b15d3b97ce06ab88ea8 # v6.1.1
uses: golangci/golangci-lint-action@v6
with:
version: latest
args: --timeout=5m
Expand All @@ -50,6 +52,8 @@ jobs:
name: Lint Shell Scripts
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
Expand All @@ -60,7 +64,8 @@ jobs:
with:
scandir: '.'
severity: warning
ignore_paths: 'tests/integration_test.zsh' # Ignore specific files if needed
ignore_paths: 'pkg/katas/testdata examples scripts completions' # Ignore directories
ignore_names: 'install.sh integration_test.zsh' # Ignore specific files

test:
name: Tests
Expand All @@ -69,11 +74,16 @@ jobs:
os: [ubuntu-latest, windows-latest, macos-latest]
runs-on: ${{ matrix.os }}
timeout-minutes: 20
permissions:
contents: read
steps:
- uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
egress-policy: audit
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.1.7
- name: Install Zsh (Ubuntu)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y zsh
- name: Set up Go
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.0.1
with:
Expand All @@ -83,7 +93,7 @@ jobs:
- name: Build
run: go build -v -o zshellcheck ./cmd/zshellcheck
- name: Test (Unit)
run: go test -v -coverprofile=coverage.out -covermode=atomic ./...
run: go test -v -coverprofile="coverage.txt" -covermode=atomic ./...
- name: Verify go.mod is tidy
run: |
go mod tidy
Expand All @@ -99,12 +109,12 @@ jobs:
chmod +x tests/integration_test.zsh
./tests/integration_test.zsh
- name: Upload coverage to Codecov
uses: codecov/codecov-action@1e68e06f1dbfde0f2ce0c13999cd13d260d60d6e # v5.1.1
uses: codecov/codecov-action@v5
with:
token: ${{ secrets.CODECOV_TOKEN }}
fail_ci_if_error: true
- name: Upload Binary Artifact
uses: actions/upload-artifact@65c4c4a1ddee5b7c888b470a09886fa126ba21b8 # v4.6.0 (pinned)
uses: actions/upload-artifact@v4
with:
name: zshellcheck-${{ runner.os }}-${{ runner.arch }}
path: zshellcheck${{ runner.os == 'Windows' && '.exe' || '' }}
Expand All @@ -114,6 +124,8 @@ jobs:
name: Fuzzing
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
Expand All @@ -134,6 +146,8 @@ jobs:
name: Security Scan
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
steps:
- uses: step-security/harden-runner@c6295a65d1254861815972266d5933fd6e532bdf # v2.11.1
with:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ on:
- 'v*'

permissions:
contents: write
contents: read

jobs:
goreleaser:
Expand All @@ -27,6 +27,8 @@ jobs:
uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5.0.1
with:
go-version: '1.23.0'
- name: Install Cosign
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 # v3.5.0
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0
with:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,4 @@ zshellcheck-windows-amd64.exe
# Gemini CLI specific private files
GEMINI.md
bin/
coverage.txt
2 changes: 1 addition & 1 deletion pkg/katas/katatests/zc1071_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ func TestZC1071(t *testing.T) {
}{
{
name: "invalid append self reference single",
input: `arr=($arr)`,
input: `arr=($arr)`,
expected: []katas.Violation{
{
KataID: "ZC1071",
Expand Down
93 changes: 34 additions & 59 deletions pkg/katas/zc1071.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package katas

import (
// "fmt"
"strings"

"github.com/afadesigns/zshellcheck/pkg/ast"
)

func init() {
RegisterKata(ast.SimpleCommandNode, Kata{
RegisterKata(ast.ExpressionStatementNode, Kata{
ID: "ZC1071",
Title: "Use `+=` for appending to arrays",
Description: "Appending to an array using `arr=($arr ...)` is verbose and slower. Use `arr+=(...)` instead.",
Expand All @@ -14,89 +17,61 @@ func init() {
}

func checkZC1071(node ast.Node) []Violation {
cmd, ok := node.(*ast.SimpleCommand)
exprStmt, ok := node.(*ast.ExpressionStatement)
if !ok {
return nil
}

if len(cmd.Arguments) == 0 {
infixExpr, ok := exprStmt.Expression.(*ast.InfixExpression)
if !ok || infixExpr.Operator != "=" {
return nil
}

varName := cmd.Name.String()
var rhs ast.Expression
leftIdent, ok := infixExpr.Left.(*ast.Identifier)
if !ok {
return nil
}

arg0 := cmd.Arguments[0]
varName := leftIdent.Value
valueExpr := infixExpr.Right

if concat, ok := arg0.(*ast.ConcatenatedExpression); ok {
if len(concat.Parts) >= 2 {
if str, ok := concat.Parts[0].(*ast.StringLiteral); ok && str.Value == "=" {
rhs = concat.Parts[1]
}
}
} else if len(cmd.Arguments) >= 2 {
if str, ok := arg0.(*ast.StringLiteral); ok && str.Value == "=" {
rhs = cmd.Arguments[1]
}
if checkSelfReference(varName, valueExpr) {
return []Violation{{
KataID: "ZC1071",
Message: "Appending to an array using `arr=($arr ...)` is verbose and slower. Use `arr+=(...)` instead.",
Line: exprStmt.Token.Line,
Column: exprStmt.Token.Column,
}}
}

if rhs == nil {
return nil
}
return nil
}

func checkSelfReference(varName string, expr ast.Expression) bool {
found := false

checkNode := func(n ast.Node) bool {
// Check ArrayAccess (for ${var})
if aa, ok := n.(*ast.ArrayAccess); ok {
if ident, ok := aa.Left.(*ast.Identifier); ok && ident.Value == varName {
if ident, ok := n.(*ast.Identifier); ok {
if ident.Value == varName || (strings.HasPrefix(ident.Value, "$") && strings.TrimPrefix(ident.Value, "$") == varName) {
found = true
return false
}
}
// Check Identifier with value "$var" or "${var}"
if ident, ok := n.(*ast.Identifier); ok {
if ident.Value == "$"+varName || ident.Value == "${"+varName+"}" {
if aa, ok := n.(*ast.ArrayAccess); ok {
if ident, ok := aa.Left.(*ast.Identifier); ok && ident.Value == varName {
found = true
return false
}
}
// Check PrefixExpression like `$var`
if prefix, ok := n.(*ast.PrefixExpression); ok {
if prefix.Operator == "$" {
if ident, ok := prefix.Right.(*ast.Identifier); ok && ident.Value == varName {
found = true
return false
}
}
}
return true
}

// Handle GroupedExpression (legacy/single element)
if grouped, ok := rhs.(*ast.GroupedExpression); ok {
ast.Walk(grouped.Expression, checkNode)
}

// Handle ArrayLiteral (multiple elements)
if arrayLit, ok := rhs.(*ast.ArrayLiteral); ok {
for _, elem := range arrayLit.Elements {
if found {
break
if prefix, ok := n.(*ast.PrefixExpression); ok && prefix.Operator == "$" {
if ident, ok := prefix.Right.(*ast.Identifier); ok && ident.Value == varName {
found = true
return false
}
ast.Walk(elem, checkNode)
}
}

if found {
return []Violation{{
KataID: "ZC1071",
Message: "Appending to an array using `arr=($arr ...)` is verbose and slower. " +
"Use `arr+=(...)` instead.",
Line: cmd.Token.Line,
Column: cmd.Token.Column,
}}
return true
}

return nil
ast.Walk(expr, checkNode)
return found
}
3 changes: 3 additions & 0 deletions pkg/lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -365,6 +365,9 @@ func (l *Lexer) readString(quote byte) string {
}
if l.ch == '\\' {
l.readChar() // skip escaped char
if l.ch == 0 {
break
}
}
}
if l.ch == 0 {
Expand Down
Loading