1// Copyright (c) 2020 Denis Tingajkin
2//
3// SPDX-License-Identifier: Apache-2.0
4//
5// Licensed under the Apache License, Version 2.0 (the "License");
6// you may not use this file except in compliance with the License.
7// You may obtain a copy of the License at:
8//
9//     http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing, software
12// distributed under the License is distributed on an "AS IS" BASIS,
13// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14// See the License for the specific language governing permissions and
15// limitations under the License.
16
17package goheader
18
19import (
20	"fmt"
21	"go/ast"
22	"os"
23	"os/exec"
24	"strings"
25	"time"
26)
27
28type Target struct {
29	Path string
30	File *ast.File
31}
32
33const iso = "2006-01-02 15:04:05 -0700"
34
35func (t *Target) ModTime() (time.Time, error) {
36	diff, err := exec.Command("git", "diff", t.Path).CombinedOutput()
37	if err == nil && len(diff) == 0 {
38		line, err := exec.Command("git", "log", "-1", "--pretty=format:%cd", "--date=iso", "--", t.Path).CombinedOutput()
39		if err == nil {
40			return time.Parse(iso, string(line))
41		}
42	}
43	info, err := os.Stat(t.Path)
44	if err != nil {
45		return time.Time{}, err
46	}
47	return info.ModTime(), nil
48}
49
50type Analyzer struct {
51	values   map[string]Value
52	template string
53}
54
55func (a *Analyzer) Analyze(target *Target) Issue {
56	if a.template == "" {
57		return NewIssue("Missed template for check")
58	}
59	if t, err := target.ModTime(); err == nil {
60		if t.Year() != time.Now().Year() {
61			return nil
62		}
63	}
64	file := target.File
65	var header string
66	var offset = Location{
67		Position: 1,
68	}
69	if len(file.Comments) > 0 && file.Comments[0].Pos() < file.Package {
70		if strings.HasPrefix(file.Comments[0].List[0].Text, "/*") {
71			header = (&ast.CommentGroup{List: []*ast.Comment{file.Comments[0].List[0]}}).Text()
72		} else {
73			header = file.Comments[0].Text()
74			offset.Position += 3
75		}
76	}
77	header = strings.TrimSpace(header)
78	if header == "" {
79		return NewIssue("Missed header for check")
80	}
81	s := NewReader(header)
82	s.SetOffset(offset)
83	t := NewReader(a.template)
84	for !s.Done() && !t.Done() {
85		templateCh := t.Peek()
86		if templateCh == '{' {
87			name := a.readField(t)
88			if a.values[name] == nil {
89				return NewIssue(fmt.Sprintf("Template has unknown value: %v", name))
90			}
91			if i := a.values[name].Read(s); i != nil {
92				return i
93			}
94			continue
95		}
96		sourceCh := s.Peek()
97		if sourceCh != templateCh {
98			l := s.Location()
99			notNextLine := func(r rune) bool {
100				return r != '\n'
101			}
102			actual := s.ReadWhile(notNextLine)
103			expected := t.ReadWhile(notNextLine)
104			return NewIssueWithLocation(fmt.Sprintf("Actual: %v\nExpected:%v", actual, expected), l)
105		}
106		s.Next()
107		t.Next()
108	}
109	if !s.Done() {
110		l := s.Location()
111		return NewIssueWithLocation(fmt.Sprintf("Unexpected string: %v", s.Finish()), l)
112	}
113	if !t.Done() {
114		l := s.Location()
115		return NewIssueWithLocation(fmt.Sprintf("Missed string: %v", t.Finish()), l)
116	}
117	return nil
118}
119
120func (a *Analyzer) readField(reader *Reader) string {
121	_ = reader.Next()
122	_ = reader.Next()
123
124	r := reader.ReadWhile(func(r rune) bool {
125		return r != '}'
126	})
127
128	_ = reader.Next()
129	_ = reader.Next()
130
131	return strings.ToLower(strings.TrimSpace(r))
132}
133
134func New(options ...Option) *Analyzer {
135	a := &Analyzer{}
136	for _, o := range options {
137		o.apply(a)
138	}
139	for _, v := range a.values {
140		err := v.Calculate(a.values)
141		if err != nil {
142			panic(err.Error())
143		}
144	}
145	return a
146}
147