You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@skywalking.apache.org by ke...@apache.org on 2020/12/22 13:53:47 UTC

[skywalking-eyes] branch main updated: Enhance checking/fixing command, more tests, more comment styles (#2)

This is an automated email from the ASF dual-hosted git repository.

kezhenxu94 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 d7f8ed6  Enhance checking/fixing command, more tests, more comment styles (#2)
d7f8ed6 is described below

commit d7f8ed6c0a3b40e6fe2124b2f31045b3693ba0d1
Author: Zhenxu Ke <ke...@apache.org>
AuthorDate: Tue Dec 22 21:53:39 2020 +0800

    Enhance checking/fixing command, more tests, more comment styles (#2)
    
    - `fix` command should properly tackle shebang and xml declaration;
    - Add more known comment styles;
    - Add more test cases;
    - Support checking a single file for debugging;
    - Append .gitignore content automatically, add `LicenseLocationThreshold`
---
 .github/workflows/license-eye-check.yaml           |   3 +
 .gitignore                                         |   1 +
 .golangci.yml                                      |   2 +-
 .licenserc.yaml                                    |  33 ++-
 license-eye/Makefile                               |   3 +-
 license-eye/assets/languages.yaml                  |   9 +
 license-eye/assets/styles.yaml                     |  31 ++-
 license-eye/commands/header/check.go               |   5 +
 license-eye/internal/logger/log.go                 |   2 +-
 license-eye/pkg/comments/config.go                 |  11 +-
 license-eye/pkg/config/{Config.go => config.go}    |   0
 license-eye/pkg/header/check.go                    |  34 ++-
 license-eye/pkg/header/check_test.go               | 143 +++++++++++++
 license-eye/pkg/header/config.go                   |  41 +++-
 license-eye/pkg/header/fix.go                      |  28 ++-
 license-eye/pkg/header/fix_test.go                 | 236 ++++++++++++++++-----
 .../test/testdata/.licenserc_for_test_check.yaml   |   2 +-
 .../test/testdata/.licenserc_for_test_fix.yaml     |   2 +-
 .../testdata/include_test/with_license/testcase.go |   3 +-
 .../include_test/with_license/testcase.java        |  11 +-
 .../testdata/include_test/with_license/testcase.ml |  19 ++
 .../include_test/without_license/testcase.py       |   1 +
 22 files changed, 515 insertions(+), 105 deletions(-)

diff --git a/.github/workflows/license-eye-check.yaml b/.github/workflows/license-eye-check.yaml
index cd40f8c..2bcfd23 100644
--- a/.github/workflows/license-eye-check.yaml
+++ b/.github/workflows/license-eye-check.yaml
@@ -50,3 +50,6 @@ jobs:
 
       - name: Build
         run: make build
+
+      - name: Build Docker Image
+        run: make docker
diff --git a/.gitignore b/.gitignore
index a559c8f..a90f1dd 100644
--- a/.gitignore
+++ b/.gitignore
@@ -19,3 +19,4 @@
 bin/
 license-eye/assets/assets.gen.go
 .DS_Store
+coverage.txt
diff --git a/.golangci.yml b/.golangci.yml
index ca0227f..f4a7795 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -51,7 +51,7 @@ linters-settings:
     disabled-checks:
       - ifElseChain
   funlen:
-    lines: 100
+    lines: 150
     statements: 50
   whitespace:
     multi-if: false
diff --git a/.licenserc.yaml b/.licenserc.yaml
index 049ce1b..dc3b932 100644
--- a/.licenserc.yaml
+++ b/.licenserc.yaml
@@ -1,3 +1,21 @@
+#
+# Licensed to 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. Apache Software Foundation (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.
+#
 header: # `header` section is configurations for source codes license header.
   license: | # `license` will be used as the content when `fix` command needs to insert a license header.
     Licensed to Apache Software Foundation (ASF) under one or more contributor
@@ -16,7 +34,10 @@ header: # `header` section is configurations for source codes license header.
     KIND, either express or implied.  See the License for the
     specific language governing permissions and limitations
     under the License.
-  pattern: | # `pattern` is optional regexp if all the file headers are the same as `license` (linebreaks doesn't matter).
+
+  # `pattern` is optional regexp if all the file headers are the same as `license` (linebreaks doesn't matter);
+  # In the `pattern`, all punctuations should be removed unless they are part of the regex;
+  pattern: |
     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
@@ -34,14 +55,12 @@ header: # `header` section is configurations for source codes license header.
     specific language governing permissions and limitations
     under the License.
 
-  paths: # `paths` are the path list that will be checked (and fixed) by license-eye.
+  paths: # `paths` are the path list that will be checked (and fixed) by license-eye, default is ['**'].
     - '**'
 
   paths-ignore: # `paths-ignore` are the path list that will be ignored by license-eye.
-    - '.git/**'
-    - '.idea/**'
-    - 'dist/**'
-    - 'licenses/**'
+    - 'dist'
+    - 'licenses'
     - '**/bin/**'
     - '**/*.md'
     - '**/.DS_Store'
@@ -50,5 +69,5 @@ header: # `header` section is configurations for source codes license header.
     - '**/go.sum'
     - 'LICENSE'
     - 'NOTICE'
-    - '**/assets.gen.go'
     - '**/assets/languages.yaml'
+    - '**/assets/assets.gen.go'
diff --git a/license-eye/Makefile b/license-eye/Makefile
index 0629e37..09f9fca 100644
--- a/license-eye/Makefile
+++ b/license-eye/Makefile
@@ -61,7 +61,7 @@ codegen: clean
 
 .PHONY: test
 test: clean codegen
-	$(GO_TEST) ./...
+	$(GO_TEST) ./... -coverprofile=coverage.txt -covermode=atomic
 	@>&2 echo "Great, all tests passed."
 
 .PHONY: $(PLATFORMS)
@@ -80,3 +80,4 @@ docker:
 clean:
 	-rm -rf bin
 	-rm -rf assets/*.gen.go
+	-rm -rf coverage.txt
diff --git a/license-eye/assets/languages.yaml b/license-eye/assets/languages.yaml
index e4af70e..2d179ae 100644
--- a/license-eye/assets/languages.yaml
+++ b/license-eye/assets/languages.yaml
@@ -2232,6 +2232,7 @@ Haskell:
   codemirror_mode: haskell
   codemirror_mime_type: text/x-haskell
   language_id: 157
+  comment_style_id: CurlyBracketDash
 Haxe:
   type: programming
   ace_mode: haxe
@@ -3708,6 +3709,7 @@ OCaml:
     - ocamlscript
   tm_scope: source.ocaml
   language_id: 255
+  comment_style_id: RoundBracketAsterisk
 ObjDump:
   type: data
   extensions:
@@ -3977,6 +3979,7 @@ PLSQL:
     - ".trg"
     - ".vw"
   language_id: 273
+  comment_style_id: DoubleDash
 PLpgSQL:
   type: programming
   ace_mode: pgsql
@@ -3987,6 +3990,7 @@ PLpgSQL:
     - ".pgsql"
     - ".sql"
   language_id: 274
+  comment_style_id: DoubleDash
 POV-Ray SDL:
   type: programming
   aliases:
@@ -5026,6 +5030,7 @@ SQL:
     - ".udf"
     - ".viw"
   language_id: 333
+  comment_style_id: DoubleDash
 SQLPL:
   type: programming
   ace_mode: sql
@@ -5036,6 +5041,7 @@ SQLPL:
     - ".sql"
     - ".db2"
   language_id: 334
+  comment_style_id: DoubleDash
 SRecode Template:
   type: markup
   color: "#348a34"
@@ -5549,6 +5555,7 @@ TSQL:
   ace_mode: sql
   tm_scope: source.tsql
   language_id: 918334941
+  comment_style_id: DoubleDash
 TSV:
   type: data
   ace_mode: text
@@ -5969,6 +5976,7 @@ Vim script:
     - vimrc
   ace_mode: text
   language_id: 388
+  comment_style_id: Quotes
 Visual Basic .NET:
   type: programming
   color: "#945db7"
@@ -6393,6 +6401,7 @@ YAML:
   codemirror_mode: yaml
   codemirror_mime_type: text/x-yaml
   language_id: 407
+  comment_style_id: Hashtag
 YANG:
   type: data
   extensions:
diff --git a/license-eye/assets/styles.yaml b/license-eye/assets/styles.yaml
index deb64a5..216232f 100644
--- a/license-eye/assets/styles.yaml
+++ b/license-eye/assets/styles.yaml
@@ -18,16 +18,17 @@
 
 - id: DoubleSlash
   start: '//'
-  middle: ~
-  end: ~
+  middle: '//'
+  end: '//'
 
 - id: Hashtag
+  after: '(?m)^*#!.+$'
   start: '#'
-  middle: ~
-  end: ~
-  skip: '^#!'
+  middle: '#'
+  end: '#'
 
 - id: AngleBracket
+  after: '(?ms)^<\?.+?\?>$'
   start: '<!--'
   middle: '  ~'
   end: '-->'
@@ -38,3 +39,23 @@
   middle: ' *'          # <2>
   end: ' */'            # <3>
 # end::SlashAsterisk[]
+
+- id: RoundBracketAsterisk
+  start: '(*'
+  middle: '(*'
+  end: '(*'
+
+- id: CurlyBracketDash
+  start: '{-'
+  middle: ~
+  end: '-}'
+
+- id: DoubleDash
+  start: '--'
+  middle: '--'
+  end: '--'
+
+- id: Quotes
+  start: '"'
+  middle: '"'
+  end: '"'
diff --git a/license-eye/commands/header/check.go b/license-eye/commands/header/check.go
index 1de9115..c349ace 100644
--- a/license-eye/commands/header/check.go
+++ b/license-eye/commands/header/check.go
@@ -37,6 +37,11 @@ var CheckCommand = &cobra.Command{
 			return err
 		}
 
+		if len(args) > 0 {
+			logger.Log.Debugln("Overriding paths with command line args.")
+			config.Header.Paths = args
+		}
+
 		if err := header.Check(&config.Header, &result); err != nil {
 			return err
 		}
diff --git a/license-eye/internal/logger/log.go b/license-eye/internal/logger/log.go
index 7fc102e..1c641e8 100644
--- a/license-eye/internal/logger/log.go
+++ b/license-eye/internal/logger/log.go
@@ -29,7 +29,7 @@ func init() {
 	if Log == nil {
 		Log = logrus.New()
 	}
-	Log.Level = logrus.DebugLevel
+	Log.Level = logrus.InfoLevel
 	Log.SetOutput(os.Stdout)
 	Log.SetFormatter(&logrus.TextFormatter{
 		DisableTimestamp:       true,
diff --git a/license-eye/pkg/comments/config.go b/license-eye/pkg/comments/config.go
index 8b2db6f..28f4462 100644
--- a/license-eye/pkg/comments/config.go
+++ b/license-eye/pkg/comments/config.go
@@ -27,6 +27,7 @@ import (
 
 type CommentStyle struct {
 	ID     string `yaml:"id"`
+	After  string `yaml:"after"`
 	Start  string `yaml:"start"`
 	Middle string `yaml:"middle"`
 	End    string `yaml:"end"`
@@ -39,15 +40,6 @@ func (style *CommentStyle) Validate() error {
 	return nil
 }
 
-func (style *CommentStyle) Finalize() {
-	if style.Middle == "" {
-		style.Middle = style.Start
-	}
-	if style.End == "" {
-		style.End = style.Start
-	}
-}
-
 type Language struct {
 	Type           string   `yaml:"type"`
 	Extensions     []string `yaml:"extensions"`
@@ -107,7 +99,6 @@ func initCommentStyles() {
 	}
 
 	for _, style := range styles {
-		style.Finalize()
 		comments[style.ID] = style
 	}
 }
diff --git a/license-eye/pkg/config/Config.go b/license-eye/pkg/config/config.go
similarity index 100%
rename from license-eye/pkg/config/Config.go
rename to license-eye/pkg/config/config.go
diff --git a/license-eye/pkg/header/check.go b/license-eye/pkg/header/check.go
index a0541e6..5325692 100644
--- a/license-eye/pkg/header/check.go
+++ b/license-eye/pkg/header/check.go
@@ -29,7 +29,13 @@ import (
 	"github.com/bmatcuk/doublestar/v2"
 )
 
-const CommentChars = "/*#- !~'\"(){}"
+// TODO: also trim stop words
+var (
+	// LicenseLocationThreshold specifies the index threshold where the license header can be located,
+	// after all, a "header" cannot be TOO far from the file start.
+	LicenseLocationThreshold = 80
+	Punctuations             = regexp.MustCompile("[\\[\\]/*:;\\s#\\-!~'\"(){}?]+")
+)
 
 // Check checks the license headers of the specified paths/globs.
 func Check(config *ConfigHeader, result *Result) error {
@@ -111,31 +117,45 @@ func CheckFile(file string, config *ConfigHeader, result *Result) error {
 	reader, err := os.Open(file)
 
 	if err != nil {
-		return nil
+		return err
 	}
 
 	var lines []string
 
 	scanner := bufio.NewScanner(reader)
 	for scanner.Scan() {
-		line := strings.ToLower(strings.Trim(scanner.Text(), CommentChars))
-		line = regexp.MustCompile("[ '\"]+").ReplaceAllString(line, " ")
+		line := strings.ToLower(Punctuations.ReplaceAllString(scanner.Text(), " "))
 		if len(line) > 0 {
 			lines = append(lines, line)
 		}
 	}
 
-	content := strings.Join(lines, " ")
+	content := Punctuations.ReplaceAllString(strings.Join(lines, " "), " ")
 	license, pattern := config.NormalizedLicense(), config.NormalizedPattern()
 
-	if strings.Contains(content, license) || (pattern != nil && pattern.MatchString(content)) {
+	if satisfy(content, license, pattern) {
 		result.Succeed(file)
 	} else {
 		logger.Log.Debugln("Content is:", content)
-		logger.Log.Debugln("Pattern is:", pattern)
+		if pattern != nil {
+			logger.Log.Debugln("Pattern is:", pattern)
+		}
 
 		result.Fail(file)
 	}
 
 	return nil
 }
+
+func satisfy(content, license string, pattern *regexp.Regexp) bool {
+	if index := strings.Index(content, license); index >= 0 {
+		return index < LicenseLocationThreshold
+	}
+
+	if pattern == nil {
+		return false
+	}
+	index := pattern.FindStringIndex(content)
+
+	return len(index) == 2 && index[0] < LicenseLocationThreshold
+}
diff --git a/license-eye/pkg/header/check_test.go b/license-eye/pkg/header/check_test.go
new file mode 100644
index 0000000..2cc2de7
--- /dev/null
+++ b/license-eye/pkg/header/check_test.go
@@ -0,0 +1,143 @@
+//
+// Licensed to 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. Apache Software Foundation (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 (
+	"io/ioutil"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"gopkg.in/yaml.v3"
+)
+
+var c struct {
+	Header ConfigHeader `yaml:"header"`
+}
+
+func init() {
+	content, err := ioutil.ReadFile("../../test/testdata/.licenserc_for_test_check.yaml")
+	if err != nil {
+		panic(err)
+	}
+	if err := yaml.Unmarshal(content, &c); err != nil {
+		panic(err)
+	}
+	if err := c.Header.Finalize(); err != nil {
+		panic(err)
+	}
+}
+
+func TestCheckFile(t *testing.T) {
+	type args struct {
+		name       string
+		file       string
+		result     *Result
+		wantErr    bool
+		hasFailure bool
+	}
+	tests := func() []args {
+		files, err := filepath.Glob("../../test/testdata/include_test/with_license/*")
+		if err != nil {
+			t.Error(err)
+		}
+
+		var cases []args
+
+		for _, file := range files {
+			cases = append(cases, args{
+				name:       file,
+				file:       file,
+				result:     &Result{},
+				wantErr:    false,
+				hasFailure: false,
+			})
+		}
+
+		return cases
+	}()
+
+	if len(tests) == 0 {
+		t.Errorf("Tests should not be empty")
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if strings.TrimSpace(c.Header.License) == "" {
+				t.Errorf("License should not be empty")
+			}
+			if err := CheckFile(tt.file, &c.Header, tt.result); (err != nil) != tt.wantErr {
+				t.Errorf("CheckFile() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if len(tt.result.Ignored) > 0 {
+				t.Errorf("Should not ignore any file, %v", tt.result.Ignored)
+			}
+			if tt.result.HasFailure() != tt.hasFailure {
+				t.Errorf("CheckFile() result has failure = %v, wanted = %v", tt.result.Failure, tt.hasFailure)
+			}
+		})
+	}
+}
+
+func TestCheckFileFailure(t *testing.T) {
+	type args struct {
+		name       string
+		file       string
+		result     *Result
+		wantErr    bool
+		hasFailure bool
+	}
+	tests := func() []args {
+		files, err := filepath.Glob("../../test/testdata/include_test/without_license/*")
+		if err != nil {
+			panic(err)
+		}
+
+		var cases []args
+
+		for _, file := range files {
+			cases = append(cases, args{
+				name:       file,
+				file:       file,
+				result:     &Result{},
+				wantErr:    false,
+				hasFailure: true,
+			})
+		}
+
+		return cases
+	}()
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if strings.TrimSpace(c.Header.License) == "" {
+				t.Errorf("License should not be empty")
+			}
+			if err := CheckFile(tt.file, &c.Header, tt.result); (err != nil) != tt.wantErr {
+				t.Errorf("CheckFile() error = %v, wantErr %v", err, tt.wantErr)
+			}
+			if len(tt.result.Ignored) > 0 {
+				t.Errorf("Should not ignore any file, %v", tt.result.Ignored)
+			}
+			if tt.result.HasFailure() != tt.hasFailure {
+				t.Errorf("CheckFile() result has failure = %v, wanted = %v", tt.result.Failure, tt.hasFailure)
+			}
+		})
+	}
+}
diff --git a/license-eye/pkg/header/config.go b/license-eye/pkg/header/config.go
index 0293d1f..8911cfc 100644
--- a/license-eye/pkg/header/config.go
+++ b/license-eye/pkg/header/config.go
@@ -18,7 +18,9 @@
 package header
 
 import (
+	"bufio"
 	"io/ioutil"
+	"os"
 	"regexp"
 	"strings"
 
@@ -36,17 +38,15 @@ type ConfigHeader struct {
 }
 
 // NormalizedLicense returns the normalized string of the license content,
-// "normalized" means the linebreaks and CommentChars are all trimmed.
+// "normalized" means the linebreaks and Punctuations are all trimmed.
 func (config *ConfigHeader) NormalizedLicense() string {
 	var lines []string
 	for _, line := range strings.Split(config.License, "\n") {
 		if len(line) > 0 {
-			line = strings.ToLower(strings.Trim(line, CommentChars))
-			line = regexp.MustCompile(" +").ReplaceAllString(line, " ")
-			lines = append(lines, line)
+			lines = append(lines, Punctuations.ReplaceAllString(line, " "))
 		}
 	}
-	return strings.Join(lines, " ")
+	return strings.ToLower(regexp.MustCompile("(?m)[\\s\"']+").ReplaceAllString(strings.Join(lines, " "), " "))
 }
 
 func (config *ConfigHeader) NormalizedPattern() *regexp.Regexp {
@@ -57,11 +57,11 @@ func (config *ConfigHeader) NormalizedPattern() *regexp.Regexp {
 	var lines []string
 	for _, line := range strings.Split(config.Pattern, "\n") {
 		if len(line) > 0 {
-			line = regexp.MustCompile("[ \"']+").ReplaceAllString(line, " ")
-			lines = append(lines, strings.TrimSpace(line))
+			lines = append(lines, line)
 		}
 	}
-	return regexp.MustCompile("(?i).*" + strings.Join(lines, " ") + ".*")
+	content := regexp.MustCompile("(?m)[\\s\"':;/\\-]+").ReplaceAllString(strings.Join(lines, " "), " ")
+	return regexp.MustCompile("(?i).*" + content + ".*")
 }
 
 // Parse reads and parses the header check configurations in config file.
@@ -89,6 +89,16 @@ func (config *ConfigHeader) ShouldIgnore(path string) (bool, error) {
 			return matched, err
 		}
 	}
+
+	if stat, err := os.Stat(path); err == nil {
+		for _, ignorePattern := range config.PathsIgnore {
+			ignorePattern = strings.TrimRight(ignorePattern, "/")
+			if strings.HasPrefix(path, ignorePattern+"/") || stat.Name() == ignorePattern {
+				return true, nil
+			}
+		}
+	}
+
 	return false, nil
 }
 
@@ -99,5 +109,20 @@ func (config *ConfigHeader) Finalize() error {
 		config.Paths = []string{"**"}
 	}
 
+	config.PathsIgnore = append(config.PathsIgnore, ".git")
+
+	if file, err := os.Open(".gitignore"); err == nil {
+		defer func() { _ = file.Close() }()
+
+		for scanner := bufio.NewScanner(file); scanner.Scan(); {
+			line := scanner.Text()
+			if strings.HasPrefix(line, "#") || strings.TrimSpace(line) == "" {
+				continue
+			}
+			logger.Log.Debugln("Add ignore path from .gitignore:", line)
+			config.PathsIgnore = append(config.PathsIgnore, strings.TrimSpace(line))
+		}
+	}
+
 	return nil
 }
diff --git a/license-eye/pkg/header/fix.go b/license-eye/pkg/header/fix.go
index a710106..40acde7 100644
--- a/license-eye/pkg/header/fix.go
+++ b/license-eye/pkg/header/fix.go
@@ -22,6 +22,7 @@ import (
 	"fmt"
 	"io/ioutil"
 	"os"
+	"regexp"
 	"strings"
 
 	"github.com/apache/skywalking-eyes/license-eye/internal/logger"
@@ -60,12 +61,14 @@ func InsertComment(file string, style *comments.CommentStyle, config *ConfigHead
 		return err
 	}
 
-	lines, err := generateLicenseHeader(style, config)
+	licenseHeader, err := generateLicenseHeader(style, config)
 	if err != nil {
 		return err
 	}
 
-	if err := ioutil.WriteFile(file, append([]byte(lines), content...), stat.Mode()); err != nil {
+	content = rewriteContent(style, content, licenseHeader)
+
+	if err := ioutil.WriteFile(file, content, stat.Mode()); err != nil {
 		return err
 	}
 
@@ -74,7 +77,22 @@ func InsertComment(file string, style *comments.CommentStyle, config *ConfigHead
 	return nil
 }
 
-// TODO: tackle with shebang and xml declaration
+func rewriteContent(style *comments.CommentStyle, content []byte, licenseHeader string) []byte {
+	if style.After == "" {
+		return append([]byte(licenseHeader), content...)
+	}
+
+	content = []byte(strings.TrimLeft(string(content), " \n"))
+	afterPattern := regexp.MustCompile(style.After)
+	location := afterPattern.FindIndex(content)
+	if location == nil || len(location) != 2 {
+		return append([]byte(licenseHeader), content...)
+	}
+	return append(content[0:location[1]],
+		append(append([]byte("\n"), []byte(licenseHeader)...), content[location[1]+1:]...)...,
+	)
+}
+
 func generateLicenseHeader(style *comments.CommentStyle, config *ConfigHeader) (string, error) {
 	if err := style.Validate(); err != nil {
 		return "", err
@@ -82,7 +100,7 @@ func generateLicenseHeader(style *comments.CommentStyle, config *ConfigHeader) (
 
 	middleLines := strings.Split(config.License, "\n")
 	for i, line := range middleLines {
-		middleLines[i] = fmt.Sprintf("%v %v", style.Middle, line)
+		middleLines[i] = strings.TrimRight(fmt.Sprintf("%v %v", style.Middle, line), " ")
 	}
 
 	lines := fmt.Sprintf("%v\n%v\n", style.Start, strings.Join(middleLines, "\n"))
@@ -90,5 +108,5 @@ func generateLicenseHeader(style *comments.CommentStyle, config *ConfigHeader) (
 		lines += style.End
 	}
 
-	return lines, nil
+	return strings.TrimSpace(lines) + "\n", nil
 }
diff --git a/license-eye/pkg/header/fix_test.go b/license-eye/pkg/header/fix_test.go
index a3ea449..c6f307f 100644
--- a/license-eye/pkg/header/fix_test.go
+++ b/license-eye/pkg/header/fix_test.go
@@ -1,85 +1,63 @@
+//
+// Licensed to 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. Apache Software Foundation (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 (
+	"reflect"
 	"testing"
 
 	"github.com/apache/skywalking-eyes/license-eye/pkg/comments"
 )
 
 var config = &ConfigHeader{
-	License: `
-Licensed to 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.
-Apache Software Foundation (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.
-`,
+	License: `Apache License 2.0
+  http://www.apache.org/licenses/LICENSE-2.0
+Apache License 2.0`,
 }
 
-func Test(t *testing.T) {
+func TestFix(t *testing.T) {
 	tests := []struct {
 		filename string
 		comments string
 	}{
 		{
 			filename: "Test.java",
-			comments: `/* 
- * Licensed to 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.
- * Apache Software Foundation (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.
- * 
+			comments: `/*
+ * Apache License 2.0
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ * Apache License 2.0
  */
 `,
 		},
 		{
 			filename: "Test.py",
 			comments: `#
-# Licensed to 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.
-# Apache Software Foundation (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.
-# 
-#
+# Apache License 2.0
+#   http://www.apache.org/licenses/LICENSE-2.0
+# Apache License 2.0
 `,
 		},
 	}
 	for _, test := range tests {
 		t.Run(test.filename, func(t *testing.T) {
 			style := comments.FileCommentStyle(test.filename)
-			if c, err := generateLicenseHeader(style, config); err != nil && c != test.comments {
+			if c, err := generateLicenseHeader(style, config); err != nil || c != test.comments {
 				t.Log("Actual:", c)
 				t.Log("Expected:", test.comments)
 				t.Logf("Middle:'%v'\n", style.Middle)
@@ -89,3 +67,155 @@ func Test(t *testing.T) {
 		})
 	}
 }
+
+func TestRewriteContent(t *testing.T) {
+	tests := []struct {
+		name            string
+		style           *comments.CommentStyle
+		content         string
+		licenseHeader   string
+		expectedContent string
+	}{
+		{
+			name:  "Ocaml",
+			style: comments.FileCommentStyle("test.ml"),
+			content: `print_string "hello worlds!\n";;
+`,
+			licenseHeader: getLicenseHeader("test.ml", t.Error),
+			expectedContent: `(*
+(* Apache License 2.0
+(*   http://www.apache.org/licenses/LICENSE-2.0
+(* Apache License 2.0
+print_string "hello worlds!\n";;
+`},
+		{
+			name:  "Python with Shebang",
+			style: comments.FileCommentStyle("test.py"),
+			content: `
+#!/usr/bin/env python3
+if __name__ == '__main__':
+    print('Hello World')
+`,
+			licenseHeader: getLicenseHeader("test.py", t.Error),
+			expectedContent: `#!/usr/bin/env python3
+#
+# Apache License 2.0
+#   http://www.apache.org/licenses/LICENSE-2.0
+# Apache License 2.0
+if __name__ == '__main__':
+    print('Hello World')
+`},
+		{
+			name:  "Python",
+			style: comments.FileCommentStyle("test.py"),
+			content: `
+if __name__ == '__main__':
+    print('Hello World')
+`,
+			licenseHeader: getLicenseHeader("test.py", t.Error),
+			expectedContent: `#
+# Apache License 2.0
+#   http://www.apache.org/licenses/LICENSE-2.0
+# Apache License 2.0
+if __name__ == '__main__':
+    print('Hello World')
+`},
+		{
+			name:  "XML one line declaration",
+			style: comments.FileCommentStyle("test.xml"),
+			content: `
+<?xml version="1.0" encoding="UTF-8"?>
+<project>
+  <modelVersion>4.0.0</modelVersion>
+</project>
+`,
+			licenseHeader: getLicenseHeader("test.xml", t.Error),
+			expectedContent: `<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  ~ Apache License 2.0
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~ Apache License 2.0
+-->
+<project>
+  <modelVersion>4.0.0</modelVersion>
+</project>
+`},
+		{
+			name:  "XML multi-line declaration",
+			style: comments.FileCommentStyle("test.xml"),
+			content: `
+<?xml
+  version="1.0"
+  encoding="UTF-8"
+?>
+<project>
+  <modelVersion>4.0.0</modelVersion>
+</project>
+`,
+			licenseHeader: getLicenseHeader("test.xml", t.Error),
+			expectedContent: `<?xml
+  version="1.0"
+  encoding="UTF-8"
+?>
+<!--
+  ~ Apache License 2.0
+  ~   http://www.apache.org/licenses/LICENSE-2.0
+  ~ Apache License 2.0
+-->
+<project>
+  <modelVersion>4.0.0</modelVersion>
+</project>
+`},
+		{
+			name:          "SQL",
+			style:         comments.FileCommentStyle("test.sql"),
+			content:       `select * from user;`,
+			licenseHeader: getLicenseHeader("test.sql", t.Error),
+			expectedContent: `--
+-- Apache License 2.0
+--   http://www.apache.org/licenses/LICENSE-2.0
+-- Apache License 2.0
+select * from user;`},
+		{
+			name:          "Haskell",
+			style:         comments.FileCommentStyle("test.hs"),
+			content:       `import Foundation.Hashing.Hashable`,
+			licenseHeader: getLicenseHeader("test.hs", t.Error),
+			expectedContent: `{-
+ Apache License 2.0
+   http://www.apache.org/licenses/LICENSE-2.0
+ Apache License 2.0
+-}
+import Foundation.Hashing.Hashable`},
+		{
+			name:  "Vim",
+			style: comments.FileCommentStyle("test.vim"),
+			content: `echo 'Hello' | echo 'world!'
+`,
+			licenseHeader: getLicenseHeader("test.vim", t.Error),
+			expectedContent: `"
+" Apache License 2.0
+"   http://www.apache.org/licenses/LICENSE-2.0
+" Apache License 2.0
+echo 'Hello' | echo 'world!'
+`},
+	}
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			content := rewriteContent(test.style, []byte(test.content), test.licenseHeader)
+			if !reflect.DeepEqual(content, []byte(test.expectedContent)) {
+				t.Log("Actual\n" + string(content))
+				t.Log("Expected\n" + test.expectedContent)
+				t.Fail()
+			}
+		})
+	}
+}
+
+func getLicenseHeader(filename string, tError func(args ...interface{})) string {
+	s, err := generateLicenseHeader(comments.FileCommentStyle(filename), config)
+	if err != nil {
+		tError(err)
+	}
+	return s
+}
diff --git a/license-eye/test/testdata/.licenserc_for_test_check.yaml b/license-eye/test/testdata/.licenserc_for_test_check.yaml
index 1ae329e..eee7d39 100644
--- a/license-eye/test/testdata/.licenserc_for_test_check.yaml
+++ b/license-eye/test/testdata/.licenserc_for_test_check.yaml
@@ -18,7 +18,7 @@ header:
     under the License.
 
   paths:
-    - 'testdata/**'
+    - 'test/testdata/**'
 
   paths-ignore:
     - '**/.DS_Store'
diff --git a/license-eye/test/testdata/.licenserc_for_test_fix.yaml b/license-eye/test/testdata/.licenserc_for_test_fix.yaml
index 008e9f4..facb12f 100644
--- a/license-eye/test/testdata/.licenserc_for_test_fix.yaml
+++ b/license-eye/test/testdata/.licenserc_for_test_fix.yaml
@@ -18,7 +18,7 @@ header:
     under the License.
 
   paths:
-    - 'testdata/include_test/**'
+    - 'test/testdata/include_test/**'
 
   paths-ignore:
     - '**/.DS_Store'
diff --git a/license-eye/test/testdata/include_test/with_license/testcase.go b/license-eye/test/testdata/include_test/with_license/testcase.go
index c382b60..9b47627 100644
--- a/license-eye/test/testdata/include_test/with_license/testcase.go
+++ b/license-eye/test/testdata/include_test/with_license/testcase.go
@@ -2,7 +2,8 @@
 // license agreements. See the NOTICE file distributed with
 // this work for additional information regarding copyright
 // ownership. Apache Software Foundation (ASF) licenses this file to you under
-// the Apache License, Version 2.0 (the "License"); you may
+// 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
 //
diff --git a/license-eye/test/testdata/include_test/with_license/testcase.java b/license-eye/test/testdata/include_test/with_license/testcase.java
index a388cbc..c24ce1f 100644
--- a/license-eye/test/testdata/include_test/with_license/testcase.java
+++ b/license-eye/test/testdata/include_test/with_license/testcase.java
@@ -1,9 +1,12 @@
 /**
- * Licensed to Apache Software Foundation (ASF) under one or more contributor
+ * Licensed to 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. Apache Software Foundation (ASF) licenses this file to you under
- * the Apache License, Version 2.0 (the "License"); you may
+ * ownership. Apache Software Foundation
+ * (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
  *
@@ -12,7 +15,7 @@
  * 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
+ * KIND, either express or implied.       See the License for the
  * specific language governing permissions and limitations
  * under the License.
  */
diff --git a/license-eye/test/testdata/include_test/with_license/testcase.ml b/license-eye/test/testdata/include_test/with_license/testcase.ml
new file mode 100644
index 0000000..246ff88
--- /dev/null
+++ b/license-eye/test/testdata/include_test/with_license/testcase.ml
@@ -0,0 +1,19 @@
+(* Licensed to 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. Apache Software Foundation (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.
+
+let to_string = function Left -> "Left" | Non -> "Non" | Right -> "Right"
diff --git a/license-eye/test/testdata/include_test/without_license/testcase.py b/license-eye/test/testdata/include_test/without_license/testcase.py
index 5030e1c..ce55ff9 100644
--- a/license-eye/test/testdata/include_test/without_license/testcase.py
+++ b/license-eye/test/testdata/include_test/without_license/testcase.py
@@ -1,3 +1,4 @@
+#!/usr/bin/env python
 # Esse enim dolore adipisicing in cillum eiusmod excepteur quis nisi sit dolor anim anim id id nostrud nostrud tempor.
 # Elit sit enim cillum adipisicing non magna aute nostrud ullamco dolor dolore consequat ut ea occaecat veniam incididunt
 #  occaecat consectetur eiusmod sint eiusmod aute eu duis fugiat dolore in laboris enim eiusmod aliquip nisi aliqua irure