You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@skywalking.apache.org by wu...@apache.org on 2026/10/10 01:47:30 UTC
(skywalking-eyes) branch main updated: feat: add PowerShell license header support (#285)
This is an automated email from the ASF dual-hosted git repository.
wu-sheng pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/skywalking-eyes.git
The following commit(s) were added to refs/heads/main by this push:
new f6b7f44 feat: add PowerShell license header support (#285)
f6b7f44 is described below
commit f6b7f444be3b6d5dc790a9c9edb479892c7b0eaf
Author: Hengqian Ling <li...@outlook.com>
AuthorDate: Sat Oct 10 09:47:24 2026 +0800
feat: add PowerShell license header support (#285)
---
assets/languages.yaml | 1 +
assets/styles.yaml | 6 ++
go.mod | 2 +-
pkg/comments/config_test.go | 3 +
pkg/header/check.go | 23 ++++-
pkg/header/diff.go | 15 ++-
pkg/header/encoding.go | 125 +++++++++++++++++++++++
pkg/header/encoding_test.go | 238 ++++++++++++++++++++++++++++++++++++++++++++
pkg/header/fix.go | 10 ++
pkg/header/fix_test.go | 46 +++++++++
pkg/license/norm.go | 2 +
pkg/license/norm_test.go | 25 +++++
12 files changed, 487 insertions(+), 9 deletions(-)
diff --git a/assets/languages.yaml b/assets/languages.yaml
index 27c9663..8a11a43 100644
--- a/assets/languages.yaml
+++ b/assets/languages.yaml
@@ -4307,6 +4307,7 @@ PowerShell:
interpreters:
- pwsh
language_id: 293
+ comment_style_id: PowerShellStyle
Prisma:
type: data
color: "#0c344b"
diff --git a/assets/styles.yaml b/assets/styles.yaml
index d62f46a..79ee050 100644
--- a/assets/styles.yaml
+++ b/assets/styles.yaml
@@ -125,3 +125,9 @@
start: '/'
middle: '/'
end: '/'
+
+- id: PowerShellStyle
+ after: '(?m)\A#!.+$'
+ start: '<#'
+ middle: ~
+ end: '#>'
diff --git a/go.mod b/go.mod
index ed1d369..e04d9fb 100644
--- a/go.mod
+++ b/go.mod
@@ -16,6 +16,7 @@ require (
golang.org/x/net v0.56.0
golang.org/x/oauth2 v0.27.0
golang.org/x/sync v0.21.0
+ golang.org/x/text v0.39.0
golang.org/x/tools v0.47.0
gopkg.in/yaml.v3 v3.0.1
)
@@ -52,6 +53,5 @@ require (
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/sys v0.46.0 // indirect
- golang.org/x/text v0.39.0 // indirect
gopkg.in/warnings.v0 v0.1.2 // indirect
)
diff --git a/pkg/comments/config_test.go b/pkg/comments/config_test.go
index 9bfea88..7eb367f 100644
--- a/pkg/comments/config_test.go
+++ b/pkg/comments/config_test.go
@@ -53,6 +53,9 @@ func TestCommentStyle(t *testing.T) {
}{
{filename: "Test.java", commentStyleID: "SlashAsterisk"},
{filename: "Test.py", commentStyleID: "PythonStyle"},
+ {filename: "Test.ps1", commentStyleID: "PowerShellStyle"},
+ {filename: "Test.psm1", commentStyleID: "PowerShellStyle"},
+ {filename: "Test.psd1", commentStyleID: "PowerShellStyle"},
}
for _, test := range tests {
t.Run(test.filename, func(t *testing.T) {
diff --git a/pkg/header/check.go b/pkg/header/check.go
index c5008af..de3870b 100644
--- a/pkg/header/check.go
+++ b/pkg/header/check.go
@@ -255,18 +255,31 @@ func CheckFile(file string, config *ConfigHeader, result *Result) error {
if err != nil {
return err
}
- if t := http.DetectContentType(bs); !strings.HasPrefix(t, "text/") {
- logger.Log.Debugln("Ignoring file:", file, "; type:", t)
+
+ content, encoding, err := decodeContent(bs)
+ if err != nil {
+ // Report the file instead of aborting the whole check. Fixing it is
+ // rejected later by InsertComment, which leaves the file untouched.
+ logger.Log.Warnln("Failed to decode file:", file, "; error:", err)
+ result.Fail(file)
return nil
}
+ // A BOM already implies textual content, so only files without one need
+ // the MIME sniffing.
+ if encoding == encodingRaw {
+ if t := http.DetectContentType(bs); !strings.HasPrefix(t, "text/") {
+ logger.Log.Debugln("Ignoring file:", file, "; type:", t)
+ return nil
+ }
+ }
- content := lcs.NormalizeHeader(string(bs))
+ normalized := lcs.NormalizeHeader(string(content))
expected, pattern := config.NormalizedLicense(), config.NormalizedPattern()
- if satisfy(content, config, expected, pattern) {
+ if satisfy(normalized, config, expected, pattern) {
result.Succeed(file)
} else {
- logger.Log.Debugln("Content is:", content)
+ logger.Log.Debugln("Content is:", normalized)
result.Fail(file)
}
diff --git a/pkg/header/diff.go b/pkg/header/diff.go
index 51a91ea..2e1dec5 100644
--- a/pkg/header/diff.go
+++ b/pkg/header/diff.go
@@ -48,11 +48,20 @@ func DiffFile(file string, config *ConfigHeader) (string, error) {
if err != nil {
return "", err
}
- if t := http.DetectContentType(bs); !strings.HasPrefix(t, "text/") {
- return "", fmt.Errorf("not a text file: %v (%v)", file, t)
+
+ decoded, encoding, err := decodeContent(bs)
+ if err != nil {
+ return "", fmt.Errorf("failed to decode %v: %w", file, err)
+ }
+ // A BOM already implies textual content, so only files without one need
+ // the MIME sniffing.
+ if encoding == encodingRaw {
+ if t := http.DetectContentType(bs); !strings.HasPrefix(t, "text/") {
+ return "", fmt.Errorf("not a text file: %v (%v)", file, t)
+ }
}
- content := lcs.NormalizeHeader(string(bs))
+ content := lcs.NormalizeHeader(string(decoded))
if satisfy(content, config, expected, config.NormalizedPattern()) {
return "", nil
}
diff --git a/pkg/header/encoding.go b/pkg/header/encoding.go
new file mode 100644
index 0000000..5f7cc98
--- /dev/null
+++ b/pkg/header/encoding.go
@@ -0,0 +1,125 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package header
+
+import (
+ "bytes"
+ "fmt"
+
+ "golang.org/x/text/encoding/unicode"
+ "golang.org/x/text/transform"
+)
+
+// fileEncoding identifies the byte encoding of a file so that header fixes can
+// preserve it, including its byte-order mark (BOM).
+type fileEncoding int
+
+const (
+ encodingRaw fileEncoding = iota // UTF-8 without a BOM
+ encodingUTF8BOM
+ encodingUTF16LE
+ encodingUTF16BE
+)
+
+var (
+ utf8BOM = []byte{0xEF, 0xBB, 0xBF}
+ utf16LEBOM = []byte{0xFF, 0xFE}
+ utf16BEBOM = []byte{0xFE, 0xFF}
+ utf32LEBOM = []byte{0xFF, 0xFE, 0x00, 0x00}
+ utf32BEBOM = []byte{0x00, 0x00, 0xFE, 0xFF}
+)
+
+// decodeContent converts the file content into UTF-8 and reports the encoding
+// it was written in. Files without a BOM are returned unchanged, because their
+// encoding cannot be determined reliably (e.g. UTF-16 without a BOM).
+func decodeContent(content []byte) ([]byte, fileEncoding, error) {
+ switch {
+ case bytes.HasPrefix(content, utf32LEBOM), bytes.HasPrefix(content, utf32BEBOM):
+ // The UTF-32LE BOM shares the UTF-16LE prefix, so check it first.
+ return nil, encodingRaw, fmt.Errorf("unsupported encoding: UTF-32")
+ case bytes.HasPrefix(content, utf16LEBOM):
+ decoded, err := decodeWith(content[len(utf16LEBOM):], unicode.LittleEndian)
+ return decoded, encodingUTF16LE, err
+ case bytes.HasPrefix(content, utf16BEBOM):
+ decoded, err := decodeWith(content[len(utf16BEBOM):], unicode.BigEndian)
+ return decoded, encodingUTF16BE, err
+ case bytes.HasPrefix(content, utf8BOM):
+ return content[len(utf8BOM):], encodingUTF8BOM, nil
+ default:
+ return content, encodingRaw, nil
+ }
+}
+
+// decodeContentLossless is decodeContent for callers that rewrite the file. The
+// UTF-16 decoder replaces malformed input, such as an odd trailing byte or an
+// unpaired surrogate, with U+FFFD instead of failing, so content that does not
+// survive a round trip is rejected to keep the original bytes intact.
+func decodeContentLossless(content []byte) ([]byte, fileEncoding, error) {
+ decoded, encoding, err := decodeContent(content)
+ if err != nil {
+ return nil, encoding, err
+ }
+ encoded, err := encodeContent(decoded, encoding)
+ if err != nil {
+ return nil, encoding, err
+ }
+ // Only UTF-16 decoding is lossy, the other encodings always round trip.
+ if !bytes.Equal(encoded, content) {
+ return nil, encoding, fmt.Errorf("malformed UTF-16 content")
+ }
+ return decoded, encoding, nil
+}
+
+func decodeWith(content []byte, order unicode.Endianness) ([]byte, error) {
+ decoded, _, err := transform.Bytes(unicode.UTF16(order, unicode.IgnoreBOM).NewDecoder(), content)
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode UTF-16 content: %w", err)
+ }
+ return decoded, nil
+}
+
+// encodeContent converts UTF-8 content back into the given encoding, restoring
+// the BOM at byte 0.
+func encodeContent(content []byte, encoding fileEncoding) ([]byte, error) {
+ switch encoding {
+ case encodingUTF16LE:
+ encoded, err := encodeWith(content, unicode.LittleEndian)
+ if err != nil {
+ return nil, err
+ }
+ return append(append([]byte{}, utf16LEBOM...), encoded...), nil
+ case encodingUTF16BE:
+ encoded, err := encodeWith(content, unicode.BigEndian)
+ if err != nil {
+ return nil, err
+ }
+ return append(append([]byte{}, utf16BEBOM...), encoded...), nil
+ case encodingUTF8BOM:
+ return append(append([]byte{}, utf8BOM...), content...), nil
+ default:
+ return content, nil
+ }
+}
+
+func encodeWith(content []byte, order unicode.Endianness) ([]byte, error) {
+ encoded, _, err := transform.Bytes(unicode.UTF16(order, unicode.IgnoreBOM).NewEncoder(), content)
+ if err != nil {
+ return nil, fmt.Errorf("failed to encode UTF-16 content: %w", err)
+ }
+ return encoded, nil
+}
diff --git a/pkg/header/encoding_test.go b/pkg/header/encoding_test.go
new file mode 100644
index 0000000..b1ea937
--- /dev/null
+++ b/pkg/header/encoding_test.go
@@ -0,0 +1,238 @@
+// Licensed to the Apache Software Foundation (ASF) under one
+// or more contributor license agreements. See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership. The ASF licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License. You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied. See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package header
+
+import (
+ "bytes"
+ "encoding/binary"
+ "os"
+ "path/filepath"
+ "testing"
+ "unicode/utf16"
+
+ "github.com/stretchr/testify/require"
+
+ "github.com/apache/skywalking-eyes/pkg/comments"
+)
+
+// encodeUTF16 encodes s as UTF-16 with the given byte order, prefixed with bom.
+// It is independent of the implementation under test, which uses x/text.
+func encodeUTF16(s string, order binary.ByteOrder, bom []byte) []byte {
+ units := utf16.Encode([]rune(s))
+ out := make([]byte, 0, len(bom)+len(units)*2)
+ out = append(out, bom...)
+ var buf [2]byte
+ for _, unit := range units {
+ order.PutUint16(buf[:], unit)
+ out = append(out, buf[0], buf[1])
+ }
+ return out
+}
+
+func decodeUTF16(t *testing.T, content []byte, order binary.ByteOrder) string {
+ t.Helper()
+ require.Zero(t, len(content)%2, "UTF-16 content must have an even number of bytes")
+ units := make([]uint16, len(content)/2)
+ for i := range units {
+ units[i] = order.Uint16(content[i*2:])
+ }
+ return string(utf16.Decode(units))
+}
+
+// TestFixPreservesFileEncoding ensures that fixing a BOM encoded file keeps
+// its encoding and byte-order mark, see
+// https://github.com/apache/skywalking-eyes/pull/285#issuecomment-6081296040.
+func TestFixPreservesFileEncoding(t *testing.T) {
+ const source = "#!/usr/bin/env pwsh\nWrite-Host 'Hello World'\n"
+ tests := []struct {
+ name string
+ bom []byte
+ order binary.ByteOrder
+ }{
+ {name: "UTF-16LE with BOM", bom: []byte{0xFF, 0xFE}, order: binary.LittleEndian},
+ {name: "UTF-16BE with BOM", bom: []byte{0xFE, 0xFF}, order: binary.BigEndian},
+ {name: "UTF-8 with BOM", bom: []byte{0xEF, 0xBB, 0xBF}},
+ }
+
+ for _, test := range tests {
+ t.Run(test.name, func(t *testing.T) {
+ content := append(append([]byte{}, test.bom...), source...)
+ if test.order != nil {
+ content = encodeUTF16(source, test.order, test.bom)
+ }
+ file := filepath.Join(t.TempDir(), "test.ps1")
+ require.NoError(t, os.WriteFile(file, content, 0o600))
+
+ config := &ConfigHeader{
+ License: LicenseConfig{Content: "Apache License 2.0"},
+ Paths: []string{"**"},
+ }
+ require.NoError(t, config.Finalize())
+
+ var before Result
+ require.NoError(t, CheckFile(file, config, &before))
+ require.True(t, before.HasFailure(), "a headerless file must not pass the check")
+
+ var fixed Result
+ require.NoError(t, Fix(file, config, &fixed))
+ require.Equal(t, []string{file}, fixed.Fixed)
+
+ after, err := os.ReadFile(file)
+ require.NoError(t, err)
+ require.True(t, bytes.HasPrefix(after, test.bom), "the BOM must stay at byte 0")
+
+ text := string(after[len(test.bom):])
+ if test.order != nil {
+ text = decodeUTF16(t, after[len(test.bom):], test.order)
+ }
+ require.Contains(t, text, "#!/usr/bin/env pwsh\n")
+ require.Contains(t, text, "<#\n Apache License 2.0\n#>\n")
+ require.Contains(t, text, "Write-Host 'Hello World'")
+
+ var checked Result
+ require.NoError(t, CheckFile(file, config, &checked))
+ require.False(t, checked.HasFailure(), "the fixed file must pass the check")
+ require.Equal(t, []string{file}, checked.Success)
+
+ // Fixing an already valid file must not change it.
+ var again Result
+ require.NoError(t, Fix(file, config, &again))
+ unchanged, err := os.ReadFile(file)
+ require.NoError(t, err)
+ require.Equal(t, after, unchanged)
+ })
+ }
+}
+
+func TestHeaderOperationsRejectUTF32(t *testing.T) {
+ const source = "Write-Host 'Hello'\n"
+ encodings := []struct {
+ name string
+ bom []byte
+ order binary.ByteOrder
+ }{
+ {name: "UTF-32LE", bom: []byte{0xFF, 0xFE, 0x00, 0x00}, order: binary.LittleEndian},
+ {name: "UTF-32BE", bom: []byte{0x00, 0x00, 0xFE, 0xFF}, order: binary.BigEndian},
+ }
+ // The check reports the file as invalid without aborting the whole run,
+ // while the operations that would rewrite it fail and leave it untouched.
+ operations := []struct {
+ name string
+ run func(string, *ConfigHeader, *Result) error
+ wantErr bool
+ }{
+ {name: "check", run: CheckFile},
+ {name: "diff", run: func(file string, config *ConfigHeader, _ *Result) error {
+ _, err := DiffFile(file, config)
+ return err
+ }, wantErr: true},
+ {name: "fix", run: Fix, wantErr: true},
+ {name: "insert", run: func(file string, config *ConfigHeader, result *Result) error {
+ return InsertComment(file, comments.FileCommentStyle(file), config, result)
+ }, wantErr: true},
+ }
+
+ for _, encoding := range encodings {
+ t.Run(encoding.name, func(t *testing.T) {
+ content := append([]byte{}, encoding.bom...)
+ var buf [4]byte
+ for _, b := range []byte(source) {
+ encoding.order.PutUint32(buf[:], uint32(b))
+ content = append(content, buf[:]...)
+ }
+
+ for _, operation := range operations {
+ t.Run(operation.name, func(t *testing.T) {
+ file := filepath.Join(t.TempDir(), "test.ps1")
+ require.NoError(t, os.WriteFile(file, content, 0o600))
+ config := &ConfigHeader{
+ License: LicenseConfig{Content: "Apache License 2.0"},
+ Paths: []string{"**"},
+ }
+ require.NoError(t, config.Finalize())
+
+ var result Result
+ err := operation.run(file, config, &result)
+ if operation.wantErr {
+ require.ErrorContains(t, err, "unsupported encoding: UTF-32")
+ } else {
+ require.NoError(t, err)
+ require.Equal(t, []string{file}, result.Failure)
+ }
+ require.Empty(t, result.Success)
+ require.Empty(t, result.Fixed)
+ after, err := os.ReadFile(file)
+ require.NoError(t, err)
+ require.Equal(t, content, after, "unsupported files must remain unchanged")
+ })
+ }
+ })
+ }
+}
+
+// TestFixRejectsMalformedUTF16 ensures that fixing a UTF-16 file the decoder
+// cannot convert losslessly leaves it untouched instead of replacing the
+// malformed bytes with U+FFFD.
+func TestFixRejectsMalformedUTF16(t *testing.T) {
+ const source = "Write-Host 'Hello'\n"
+ orders := []struct {
+ name string
+ bom []byte
+ order binary.ByteOrder
+ }{
+ {name: "UTF-16LE", bom: []byte{0xFF, 0xFE}, order: binary.LittleEndian},
+ {name: "UTF-16BE", bom: []byte{0xFE, 0xFF}, order: binary.BigEndian},
+ }
+ malformations := []struct {
+ name string
+ tail func(binary.ByteOrder) []byte
+ }{
+ {name: "odd trailing byte", tail: func(binary.ByteOrder) []byte { return []byte{0x41} }},
+ {name: "unpaired surrogate", tail: func(order binary.ByteOrder) []byte {
+ var buf [2]byte
+ order.PutUint16(buf[:], 0xD800)
+ return append(buf[:], encodeUTF16("x\n", order, nil)...)
+ }},
+ }
+
+ for _, order := range orders {
+ for _, malformation := range malformations {
+ t.Run(order.name+" with "+malformation.name, func(t *testing.T) {
+ content := append(encodeUTF16(source, order.order, order.bom), malformation.tail(order.order)...)
+ file := filepath.Join(t.TempDir(), "test.ps1")
+ require.NoError(t, os.WriteFile(file, content, 0o600))
+ config := &ConfigHeader{
+ License: LicenseConfig{Content: "Apache License 2.0"},
+ Paths: []string{"**"},
+ }
+ require.NoError(t, config.Finalize())
+
+ var checked Result
+ require.NoError(t, CheckFile(file, config, &checked))
+ require.Equal(t, []string{file}, checked.Failure)
+
+ var fixed Result
+ require.ErrorContains(t, Fix(file, config, &fixed), "malformed UTF-16 content")
+ require.Empty(t, fixed.Fixed)
+ after, err := os.ReadFile(file)
+ require.NoError(t, err)
+ require.Equal(t, content, after, "malformed files must remain unchanged")
+ })
+ }
+ }
+}
diff --git a/pkg/header/fix.go b/pkg/header/fix.go
index 18cf067..da65b3c 100644
--- a/pkg/header/fix.go
+++ b/pkg/header/fix.go
@@ -56,6 +56,11 @@ func InsertComment(file string, style *comments.CommentStyle, config *ConfigHead
return err
}
+ content, encoding, err := decodeContentLossless(content)
+ if err != nil {
+ return fmt.Errorf("failed to decode %v: %w", file, err)
+ }
+
licenseHeader, err := GenerateLicenseHeader(style, config)
if err != nil {
return err
@@ -63,6 +68,11 @@ func InsertComment(file string, style *comments.CommentStyle, config *ConfigHead
content = rewriteContent(style, content, licenseHeader, config.LicensePattern(style))
+ content, err = encodeContent(content, encoding)
+ if err != nil {
+ return fmt.Errorf("failed to encode %v: %w", file, err)
+ }
+
if err := os.WriteFile(file, content, stat.Mode()); err != nil { //nolint:gosec // path from tool's own file scanner
return err
}
diff --git a/pkg/header/fix_test.go b/pkg/header/fix_test.go
index c877944..82a786e 100644
--- a/pkg/header/fix_test.go
+++ b/pkg/header/fix_test.go
@@ -82,6 +82,16 @@ func TestFix(t *testing.T) {
/ http://www.apache.org/licenses/LICENSE-2.0
/ Apache License 2.0
+`,
+ },
+ {
+ filename: "test.ps1",
+ comments: `<#
+ Apache License 2.0
+ http://www.apache.org/licenses/LICENSE-2.0
+ Apache License 2.0
+#>
+
`,
},
}
@@ -133,6 +143,42 @@ if __name__ == '__main__':
if __name__ == '__main__':
print('Hello World')
+`},
+ {
+ name: "PowerShell with interpreter binary",
+ style: comments.FileCommentStyle("test.ps1"),
+ content: `#!/usr/bin/env pwsh
+Write-Host 'Hello World'
+`,
+ licenseHeader: getLicenseHeader("test.ps1", t.Error),
+ expectedContent: `#!/usr/bin/env pwsh
+<#
+ Apache License 2.0
+ http://www.apache.org/licenses/LICENSE-2.0
+ Apache License 2.0
+#>
+
+Write-Host 'Hello World'
+`},
+ {
+ name: "PowerShell with shebang-like string mid file",
+ style: comments.FileCommentStyle("test.ps1"),
+ content: `function Invoke-Native {
+ $script = '#!/usr/bin/env pwsh'
+ Write-Host 'Hello World'
+}
+`,
+ licenseHeader: getLicenseHeader("test.ps1", t.Error),
+ expectedContent: `<#
+ Apache License 2.0
+ http://www.apache.org/licenses/LICENSE-2.0
+ Apache License 2.0
+#>
+
+function Invoke-Native {
+ $script = '#!/usr/bin/env pwsh'
+ Write-Host 'Hello World'
+}
`},
{
name: "Python with shebang-like string mid file",
diff --git a/pkg/license/norm.go b/pkg/license/norm.go
index 5a7dac9..04136a0 100644
--- a/pkg/license/norm.go
+++ b/pkg/license/norm.go
@@ -43,6 +43,8 @@ var (
// 6. Code Comment Indicators (https://spdx.dev/license-list/matching-guidelines.)
commentIndicators = []*regexp.Regexp{
+ regexp.MustCompile(`(?m)^\s*<#+`), // <#
+ regexp.MustCompile(`(?m)^\s*#+>`), // #>, must come before the # pattern
regexp.MustCompile(`(?m)^\s*#+`), // #
regexp.MustCompile(`(?m)^\s*//+`), // //
regexp.MustCompile(`(?m)^\s*"""+`), // """
diff --git a/pkg/license/norm_test.go b/pkg/license/norm_test.go
index 7f0d33f..4e8c556 100644
--- a/pkg/license/norm_test.go
+++ b/pkg/license/norm_test.go
@@ -190,6 +190,31 @@ func TestCommentLeadingCharsNormalizer(t *testing.T) {
@REM See the License for the specific language governing permissions and
@REM limitations under the License.
@REM
+`,
+ },
+ {
+ name: "PowerShell",
+ // Like PythonTripleQuotes below, the block delimiters leave a
+ // leading newline: the middle lines carry no comment marker
+ // that would consume it.
+ want: "\n" + want,
+ text: `
+<#
+ Licensed to the Apache Software Foundation (ASF) under one or more
+ contributor license agreements. See the NOTICE file distributed with
+ this work for additional information regarding copyright ownership.
+ The ASF licenses this file to You under the Apache License, Version 2.0
+ (the "License"); you may not use this file except in compliance with
+ the License. You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+#>
`,
},
{