Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/bench.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ jobs:
cache: true

- name: Install benchstat
env:
GOTOOLCHAIN: auto
run: go install golang.org/x/perf/cmd/benchstat@latest

- name: Resolve base
Expand Down
13 changes: 11 additions & 2 deletions internal/core/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,17 @@ var commentControlMatchesRE = regexp.MustCompile(`^vale (.+\..+)(\[.+\]) = (YES|

// A File represents a linted text file.
type File struct {
NLP nlp.Info // -
Summary bytes.Buffer // holds content to be included in summarization checks
NLP nlp.Info // -
Summary bytes.Buffer // holds content to be included in summarization checks

// SummaryRuns place the summary's text in Content: each piece of prose
// written to Summary records where it came from, so that a match found in
// the summary can be reported at the text that was counted. Without them
// the match is located by searching Content for its text, and a copy in
// front matter, a code block or a heading -- none of which the summary
// holds -- takes the alert. See #1191.
SummaryRuns []nlp.Run

Alerts []Alert // all alerts associated with this file
BaseStyles []string // base style assigned in .vale
Lines []string // the File's Content split into lines
Expand Down
29 changes: 24 additions & 5 deletions internal/lint/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -355,9 +355,7 @@ func (l *Linter) lintScope(f *core.File, state *walker, txt string) error {
for _, tag := range state.tagHistory {
scope, match := tagToScope[tag]
if (match && !core.StringInSlice(tag, inlineTags)) || heading.MatchString(tag) {
if scope == "text.blockquote" || scope == "text.list" {
f.Summary.WriteString(txt + "\n\n")
}
summarized := scope == "text.blockquote" || scope == "text.list"

if !match {
scope = "text.heading." + tag
Expand All @@ -370,6 +368,9 @@ func (l *Linter) lintScope(f *core.File, state *walker, txt string) error {
shift -= len(txt)

b := state.block(txt, withClasses(scope, state)+f.MetaScope+f.RealExt, shift)
if summarized {
summarize(f, strings.Repeat(" ", shift)+txt, b, shift)
}
b.Inline = inlineRuns(state.inline, b.Text, shift)
state.gather(txt, b.Line, metric)

Expand Down Expand Up @@ -398,14 +399,13 @@ func (l *Linter) lintScope(f *core.File, state *walker, txt string) error {
}
}

f.Summary.WriteString(txt + "\n\n")

// Counted here, and not from the summary: quotes and list items share the
// summary so that readability metrics see all of a document's prose, but
// `paragraphs` means what the `paragraph` scope reaches -- this branch.
f.Metrics["paragraphs"]++

b := state.block(txt, withClasses("text", state)+f.MetaScope+f.RealExt, 0)
summarize(f, txt, b, 0)
b.Inline = inlineRuns(state.inline, b.Text, 0)
state.gather(txt, b.Line, "paragraphs")
if err := l.lintProse(f, b, state.lines, true); err != nil {
Expand All @@ -414,6 +414,24 @@ func (l *Linter) lintScope(f *core.File, state *walker, txt string) error {
return l.lintInline(f, state, b, state.lines, 0)
}

// summarize adds a block's text to the file's summary, recording where in
// the source each part of it sits. txt is what the summary receives and blk
// the block made from it, whose text begins shift bytes in.
func summarize(f *core.File, txt string, blk nlp.Block, shift int) {
at := f.Summary.Len() + shift
f.Summary.WriteString(txt + "\n\n")

if blk.Offset >= 0 {
f.SummaryRuns = append(f.SummaryRuns,
nlp.Run{At: at, Src: blk.Offset, N: len(blk.Text)})
return
}
for _, r := range blk.Runs {
f.SummaryRuns = append(f.SummaryRuns,
nlp.Run{At: at + r.At, Src: r.Src, N: r.N})
}
}

// lintInline lints the inline elements captured inside blk -- link text, bold
// and so on -- as scopes of their own.
//
Expand Down Expand Up @@ -523,6 +541,7 @@ func (l *Linter) lintSizedScopes(f *core.File) error {
// TODO: is this the most efficient place to assign tagging?
summary := nlp.NewLinedBlock(f.Content, f.Summary.String(),
"summary"+f.RealExt, 0)
summary.Runs = f.SummaryRuns
summary.Metrics = f.Metrics

for _, blk := range []nlp.Block{summary} {
Expand Down
52 changes: 52 additions & 0 deletions testdata/e2e/checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,58 @@ cases:
want: |
test.md:2:30:T.SentenceLength:Try to keep sentences short (< 30 words).

- name: occurrence-summary-ancho
about: "#1191 -- a summary-scoped `occurrence` alert lands on the prose it
counted. The summary holds neither front matter, code, nor headings, so a
copy of the match in any of them is not what was counted; searching the
file for the match text handed it the alert"
files:
.vale.ini: |
StylesPath = styles
MinAlertLevel = suggestion

[*.md]
BasedOnStyles = T
styles/T/Density.yml: |
extends: occurrence
message: "'finally' appears %d times"
level: suggestion
scope: summary
max: 1
token: 'finally'
front.md: |
---
summary: finally a value
---

# Title

It works finally. It stops finally.
code.md: |
# Title

```sh
echo finally
```

It works finally. It stops finally.
heading.md: |
# Finally, finally

It works finally. It stops finally.
list.md: |
# Finally

- It works finally.
- It stops finally.
args: --output=line front.md code.md heading.md list.md
exit: 0
want: |
code.md:7:10:T.Density:'finally' appears 2 times
front.md:7:10:T.Density:'finally' appears 2 times
heading.md:3:10:T.Density:'finally' appears 2 times
list.md:3:12:T.Density:'finally' appears 2 times

- name: existence-eol-anchor
about: "#892"
dir: EndOfLine
Expand Down
Loading