diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5adc18..5474d31 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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: @@ -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 @@ -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: @@ -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 @@ -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: @@ -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 @@ -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' || '' }} @@ -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: @@ -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: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5998bf2..d2b9eb6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,7 +6,7 @@ on: - 'v*' permissions: - contents: write + contents: read jobs: goreleaser: @@ -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: diff --git a/.gitignore b/.gitignore index 6b5af04..d85ec37 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,4 @@ zshellcheck-windows-amd64.exe # Gemini CLI specific private files GEMINI.md bin/ +coverage.txt diff --git a/pkg/katas/katatests/zc1071_test.go b/pkg/katas/katatests/zc1071_test.go index 36c8767..0b8acf6 100644 --- a/pkg/katas/katatests/zc1071_test.go +++ b/pkg/katas/katatests/zc1071_test.go @@ -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", diff --git a/pkg/katas/zc1071.go b/pkg/katas/zc1071.go index 7364403..14de41f 100644 --- a/pkg/katas/zc1071.go +++ b/pkg/katas/zc1071.go @@ -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.", @@ -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 } diff --git a/pkg/lexer/lexer.go b/pkg/lexer/lexer.go index 76288b1..bd3e85e 100644 --- a/pkg/lexer/lexer.go +++ b/pkg/lexer/lexer.go @@ -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 {