mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-04 23:56:30 +00:00
c28f7cb29f
Initial part of #435
349 lines
8.3 KiB
Go
349 lines
8.3 KiB
Go
package ruleguard
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/printer"
|
|
"io/ioutil"
|
|
"path/filepath"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/quasilyte/go-ruleguard/internal/gogrep"
|
|
"github.com/quasilyte/go-ruleguard/nodetag"
|
|
"github.com/quasilyte/go-ruleguard/ruleguard/goutil"
|
|
)
|
|
|
|
type rulesRunner struct {
|
|
state *engineState
|
|
|
|
ctx *RunContext
|
|
rules *goRuleSet
|
|
|
|
importer *goImporter
|
|
|
|
filename string
|
|
src []byte
|
|
|
|
filterParams filterParams
|
|
}
|
|
|
|
func newRulesRunner(ctx *RunContext, state *engineState, rules *goRuleSet) *rulesRunner {
|
|
importer := newGoImporter(state, goImporterConfig{
|
|
fset: ctx.Fset,
|
|
debugImports: ctx.DebugImports,
|
|
debugPrint: ctx.DebugPrint,
|
|
})
|
|
rr := &rulesRunner{
|
|
ctx: ctx,
|
|
importer: importer,
|
|
rules: rules,
|
|
filterParams: filterParams{
|
|
env: state.env.GetEvalEnv(),
|
|
importer: importer,
|
|
ctx: ctx,
|
|
},
|
|
}
|
|
rr.filterParams.nodeText = rr.nodeText
|
|
return rr
|
|
}
|
|
|
|
func (rr *rulesRunner) nodeText(n ast.Node) []byte {
|
|
if gogrep.IsEmptyNodeSlice(n) {
|
|
return nil
|
|
}
|
|
|
|
from := rr.ctx.Fset.Position(n.Pos()).Offset
|
|
to := rr.ctx.Fset.Position(n.End()).Offset
|
|
src := rr.fileBytes()
|
|
if (from >= 0 && from < len(src)) && (to >= 0 && to < len(src)) {
|
|
return src[from:to]
|
|
}
|
|
|
|
// Go printer would panic on comments.
|
|
if n, ok := n.(*ast.Comment); ok {
|
|
return []byte(n.Text)
|
|
}
|
|
|
|
// Fallback to the printer.
|
|
var buf bytes.Buffer
|
|
if err := printer.Fprint(&buf, rr.ctx.Fset, n); err != nil {
|
|
panic(err)
|
|
}
|
|
return buf.Bytes()
|
|
}
|
|
|
|
func (rr *rulesRunner) fileBytes() []byte {
|
|
if rr.src != nil {
|
|
return rr.src
|
|
}
|
|
|
|
// TODO(quasilyte): re-use src slice?
|
|
src, err := ioutil.ReadFile(rr.filename)
|
|
if err != nil || src == nil {
|
|
// Assign a zero-length slice so rr.src
|
|
// is never nil during the second fileBytes call.
|
|
rr.src = make([]byte, 0)
|
|
} else {
|
|
rr.src = src
|
|
}
|
|
return rr.src
|
|
}
|
|
|
|
func (rr *rulesRunner) run(f *ast.File) error {
|
|
// TODO(quasilyte): run local rules as well.
|
|
|
|
rr.filename = rr.ctx.Fset.Position(f.Pos()).Filename
|
|
rr.filterParams.filename = rr.filename
|
|
rr.collectImports(f)
|
|
|
|
if rr.rules.universal.categorizedNum != 0 {
|
|
ast.Inspect(f, func(n ast.Node) bool {
|
|
if n == nil {
|
|
return false
|
|
}
|
|
rr.runRules(n)
|
|
return true
|
|
})
|
|
}
|
|
|
|
if len(rr.rules.universal.commentRules) != 0 {
|
|
for _, commentGroup := range f.Comments {
|
|
for _, comment := range commentGroup.List {
|
|
rr.runCommentRules(comment)
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (rr *rulesRunner) runCommentRules(comment *ast.Comment) {
|
|
// We'll need that file to create a token.Pos from the artificial offset.
|
|
file := rr.ctx.Fset.File(comment.Pos())
|
|
|
|
for _, rule := range rr.rules.universal.commentRules {
|
|
var m commentMatchData
|
|
if rule.captureGroups {
|
|
result := rule.pat.FindStringSubmatchIndex(comment.Text)
|
|
if result == nil {
|
|
continue
|
|
}
|
|
for i, name := range rule.pat.SubexpNames() {
|
|
if i == 0 || name == "" {
|
|
continue
|
|
}
|
|
resultIndex := i * 2
|
|
beginPos := result[resultIndex+0]
|
|
endPos := result[resultIndex+1]
|
|
// Negative index a special case when named group captured nothing.
|
|
// Consider this pattern: `(?P<x>foo)|(bar)`.
|
|
// If we have `bar` input string, <x> will remain empty.
|
|
if beginPos < 0 || endPos < 0 {
|
|
m.capture = append(m.capture, gogrep.CapturedNode{
|
|
Name: name,
|
|
Node: &ast.Comment{Slash: comment.Pos()},
|
|
})
|
|
continue
|
|
}
|
|
m.capture = append(m.capture, gogrep.CapturedNode{
|
|
Name: name,
|
|
Node: &ast.Comment{
|
|
Slash: file.Pos(beginPos + file.Offset(comment.Pos())),
|
|
Text: comment.Text[beginPos:endPos],
|
|
},
|
|
})
|
|
}
|
|
m.node = &ast.Comment{
|
|
Slash: file.Pos(result[0] + file.Offset(comment.Pos())),
|
|
Text: comment.Text[result[0]:result[1]],
|
|
}
|
|
} else {
|
|
// Fast path: no need to save any submatches.
|
|
result := rule.pat.FindStringIndex(comment.Text)
|
|
if result == nil {
|
|
continue
|
|
}
|
|
m.node = &ast.Comment{
|
|
Slash: file.Pos(result[0] + file.Offset(comment.Pos())),
|
|
Text: comment.Text[result[0]:result[1]],
|
|
}
|
|
}
|
|
|
|
accept := rr.handleCommentMatch(rule, m)
|
|
if accept {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rr *rulesRunner) runRules(n ast.Node) {
|
|
tag := nodetag.FromNode(n)
|
|
for _, rule := range rr.rules.universal.rulesByTag[tag] {
|
|
matched := false
|
|
rule.pat.MatchNode(n, func(m gogrep.MatchData) {
|
|
matched = rr.handleMatch(rule, m)
|
|
})
|
|
if matched {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func (rr *rulesRunner) reject(rule goRule, reason string, m matchData) {
|
|
if rule.group != rr.ctx.Debug {
|
|
return // This rule is not being debugged
|
|
}
|
|
|
|
pos := rr.ctx.Fset.Position(m.Node().Pos())
|
|
rr.ctx.DebugPrint(fmt.Sprintf("%s:%d: [%s:%d] rejected by %s",
|
|
pos.Filename, pos.Line, filepath.Base(rule.filename), rule.line, reason))
|
|
|
|
values := make([]gogrep.CapturedNode, len(m.CaptureList()))
|
|
copy(values, m.CaptureList())
|
|
sort.Slice(values, func(i, j int) bool {
|
|
return values[i].Name < values[j].Name
|
|
})
|
|
|
|
for _, v := range values {
|
|
name := v.Name
|
|
node := v.Node
|
|
|
|
if comment, ok := node.(*ast.Comment); ok {
|
|
s := strings.ReplaceAll(comment.Text, "\n", `\n`)
|
|
rr.ctx.DebugPrint(fmt.Sprintf(" $%s: %s", name, s))
|
|
continue
|
|
}
|
|
|
|
var expr ast.Expr
|
|
switch node := node.(type) {
|
|
case ast.Expr:
|
|
expr = node
|
|
case *ast.ExprStmt:
|
|
expr = node.X
|
|
default:
|
|
continue
|
|
}
|
|
|
|
typ := rr.ctx.Types.TypeOf(expr)
|
|
typeString := "<unknown>"
|
|
if typ != nil {
|
|
typeString = typ.String()
|
|
}
|
|
s := strings.ReplaceAll(goutil.SprintNode(rr.ctx.Fset, expr), "\n", `\n`)
|
|
rr.ctx.DebugPrint(fmt.Sprintf(" $%s %s: %s", name, typeString, s))
|
|
}
|
|
}
|
|
|
|
func (rr *rulesRunner) handleCommentMatch(rule goCommentRule, m commentMatchData) bool {
|
|
if rule.base.filter.fn != nil {
|
|
rr.filterParams.match = m
|
|
filterResult := rule.base.filter.fn(&rr.filterParams)
|
|
if !filterResult.Matched() {
|
|
rr.reject(rule.base, filterResult.RejectReason(), m)
|
|
return false
|
|
}
|
|
}
|
|
|
|
message := rr.renderMessage(rule.base.msg, m, true)
|
|
node := m.Node()
|
|
if rule.base.location != "" {
|
|
node, _ = m.CapturedByName(rule.base.location)
|
|
}
|
|
var suggestion *Suggestion
|
|
if rule.base.suggestion != "" {
|
|
suggestion = &Suggestion{
|
|
Replacement: []byte(rr.renderMessage(rule.base.suggestion, m, false)),
|
|
From: node.Pos(),
|
|
To: node.End(),
|
|
}
|
|
}
|
|
info := GoRuleInfo{
|
|
Group: rule.base.group,
|
|
Filename: rule.base.filename,
|
|
Line: rule.base.line,
|
|
}
|
|
rr.ctx.Report(info, node, message, suggestion)
|
|
return true
|
|
}
|
|
|
|
func (rr *rulesRunner) handleMatch(rule goRule, m gogrep.MatchData) bool {
|
|
if rule.filter.fn != nil {
|
|
rr.filterParams.match = astMatchData{match: m}
|
|
filterResult := rule.filter.fn(&rr.filterParams)
|
|
if !filterResult.Matched() {
|
|
rr.reject(rule, filterResult.RejectReason(), astMatchData{match: m})
|
|
return false
|
|
}
|
|
}
|
|
|
|
message := rr.renderMessage(rule.msg, astMatchData{match: m}, true)
|
|
node := m.Node
|
|
if rule.location != "" {
|
|
node, _ = m.CapturedByName(rule.location)
|
|
}
|
|
var suggestion *Suggestion
|
|
if rule.suggestion != "" {
|
|
suggestion = &Suggestion{
|
|
Replacement: []byte(rr.renderMessage(rule.suggestion, astMatchData{match: m}, false)),
|
|
From: node.Pos(),
|
|
To: node.End(),
|
|
}
|
|
}
|
|
info := GoRuleInfo{
|
|
Group: rule.group,
|
|
Filename: rule.filename,
|
|
Line: rule.line,
|
|
}
|
|
rr.ctx.Report(info, node, message, suggestion)
|
|
return true
|
|
}
|
|
|
|
func (rr *rulesRunner) collectImports(f *ast.File) {
|
|
rr.filterParams.imports = make(map[string]struct{}, len(f.Imports))
|
|
for _, spec := range f.Imports {
|
|
s, err := strconv.Unquote(spec.Path.Value)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
rr.filterParams.imports[s] = struct{}{}
|
|
}
|
|
}
|
|
|
|
func (rr *rulesRunner) renderMessage(msg string, m matchData, truncate bool) string {
|
|
var buf strings.Builder
|
|
if strings.Contains(msg, "$$") {
|
|
buf.Write(rr.nodeText(m.Node()))
|
|
msg = strings.ReplaceAll(msg, "$$", buf.String())
|
|
}
|
|
if len(m.CaptureList()) == 0 {
|
|
return msg
|
|
}
|
|
|
|
capture := make([]gogrep.CapturedNode, len(m.CaptureList()))
|
|
copy(capture, m.CaptureList())
|
|
sort.Slice(capture, func(i, j int) bool {
|
|
return len(capture[i].Name) > len(capture[j].Name)
|
|
})
|
|
|
|
for _, c := range capture {
|
|
n := c.Node
|
|
key := "$" + c.Name
|
|
if !strings.Contains(msg, key) {
|
|
continue
|
|
}
|
|
buf.Reset()
|
|
buf.Write(rr.nodeText(n))
|
|
// Don't interpolate strings that are too long.
|
|
var replacement string
|
|
if truncate && buf.Len() > 60 {
|
|
replacement = key
|
|
} else {
|
|
replacement = buf.String()
|
|
}
|
|
msg = strings.ReplaceAll(msg, key, replacement)
|
|
}
|
|
return msg
|
|
}
|