github.com/errata-ai/vale/v3@v3.4.2/internal/check/conditional.go (about)

     1  package check
     2  
     3  import (
     4  	"github.com/errata-ai/regexp2"
     5  
     6  	"github.com/errata-ai/vale/v3/internal/core"
     7  	"github.com/errata-ai/vale/v3/internal/nlp"
     8  )
     9  
    10  // Conditional ensures that the present of First ensures the present of Second.
    11  type Conditional struct {
    12  	Definition `mapstructure:",squash"`
    13  	Exceptions []string
    14  	patterns   []*regexp2.Regexp
    15  	First      string
    16  	Second     string
    17  	exceptRe   *regexp2.Regexp
    18  	Ignorecase bool
    19  	Vocab      bool
    20  }
    21  
    22  // NewConditional creates a new `conditional`-based rule.
    23  func NewConditional(cfg *core.Config, generic baseCheck, path string) (Conditional, error) {
    24  	var expression []*regexp2.Regexp
    25  	rule := Conditional{Vocab: true}
    26  
    27  	err := decodeRule(generic, &rule)
    28  	if err != nil {
    29  		return rule, readStructureError(err, path)
    30  	}
    31  
    32  	err = checkScopes(rule.Scope, path)
    33  	if err != nil {
    34  		return rule, err
    35  	}
    36  
    37  	re, err := updateExceptions(rule.Exceptions, cfg.AcceptedTokens, rule.Vocab)
    38  	if err != nil {
    39  		return rule, core.NewE201FromPosition(err.Error(), path, 1)
    40  	}
    41  	rule.exceptRe = re
    42  
    43  	re, err = regexp2.CompileStd(rule.Second)
    44  	if err != nil {
    45  		return rule, core.NewE201FromPosition(err.Error(), path, 1)
    46  	}
    47  	expression = append(expression, re)
    48  
    49  	re, err = regexp2.CompileStd(rule.First)
    50  	if err != nil {
    51  		return rule, core.NewE201FromPosition(err.Error(), path, 1)
    52  	}
    53  	expression = append(expression, re)
    54  
    55  	// TODO: How do we support multiple patterns?
    56  	rule.patterns = expression
    57  	return rule, nil
    58  }
    59  
    60  // Run evaluates the given conditional statement.
    61  func (c Conditional) Run(blk nlp.Block, f *core.File) ([]core.Alert, error) {
    62  	alerts := []core.Alert{}
    63  
    64  	txt := blk.Text
    65  	// We first look for the consequent of the conditional statement.
    66  	// For example, if we're ensuring that abbreviations have been defined
    67  	// parenthetically, we'd have something like:
    68  	//
    69  	//     "WHO" [antecedent], "World Health Organization (WHO)" [consequent]
    70  	//
    71  	// In other words: if "WHO" exists, it must also have a definition -- which
    72  	// we're currently looking for.
    73  	matches := c.patterns[0].FindAllStringSubmatch(txt, -1)
    74  	for _, mat := range matches {
    75  		if len(mat) > 1 {
    76  			// If we find one, we store it in a slice associated with this
    77  			// particular file.
    78  			for _, m := range mat[1:] {
    79  				if len(m) > 0 {
    80  					f.Sequences = append(f.Sequences, m)
    81  				}
    82  			}
    83  		}
    84  	}
    85  
    86  	// Now we look for the antecedent.
    87  	locs := c.patterns[1].FindAllStringIndex(txt, -1)
    88  	for _, loc := range locs {
    89  		s, err := re2Loc(txt, loc)
    90  		if err != nil {
    91  			return alerts, err
    92  		}
    93  
    94  		if !core.StringInSlice(s, f.Sequences) && !isMatch(c.exceptRe, s) {
    95  			// If we've found one (e.g., "WHO") and we haven't marked it as
    96  			// being defined previously, send an Alert.
    97  			a, erra := makeAlert(c.Definition, loc, txt)
    98  			if erra != nil {
    99  				return alerts, erra
   100  			}
   101  			alerts = append(alerts, a)
   102  		}
   103  	}
   104  
   105  	return alerts, nil
   106  }
   107  
   108  // Fields provides access to the internal rule definition.
   109  func (c Conditional) Fields() Definition {
   110  	return c.Definition
   111  }
   112  
   113  // Pattern is the internal regex pattern used by this rule.
   114  func (c Conditional) Pattern() string {
   115  	return ""
   116  }