-
Notifications
You must be signed in to change notification settings - Fork 0
/
document.ts
133 lines (105 loc) · 3.71 KB
/
document.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
import fs from 'node:fs/promises'
import path from 'node:path'
import { Feature, IdGenerator, GherkinDocument } from '@cucumber/messages'
import Gherkin from '@cucumber/gherkin'
import { LintError, Location } from './types'
import { writeFileSync } from 'node:fs'
import Line from './line'
import Rule from './rule'
export default class Document {
public filename: string
public feature: Feature = new Feature()
public path: string
public gherkinDocument: GherkinDocument
// Sometimes, we need access to the raw lines since Gherkin has processing
// to trim line content in the AST
public lines: Array<Line> = []
public errors: Array<LintError> = []
// If true, this document has rule validation disabled
public disabled: boolean = false
// A list of lines that are disabled by the gherklin-disable-next-line comment
// Uses a map of line number => boolean for faster lookups
public linesDisabled: Map<number, boolean> = new Map()
// A list of rules that are disabled by the gherklin-disable rule-name comment
public rulesDisabled: Map<string, boolean> = new Map()
public constructor(filePath: string) {
this.filename = path.basename(filePath)
this.path = filePath
}
public load = async (): Promise<void> => {
const content = await fs.readFile(this.path).catch((): never => {
throw new Error(`Could not open the feature file at "${this.filename}". Does it exist?`)
})
this.parseGherkin(String(content))
if (!this.gherkinDocument) {
return
}
const lines = String(content).split(/\r\n|\r|\n/)
lines.forEach((line) => {
this.lines.push(new Line(line))
})
this.getDisabledRules()
}
private parseGherkin = (content: string): void => {
const builder = new Gherkin.AstBuilder(IdGenerator.uuid())
const matcher = new Gherkin.GherkinClassicTokenMatcher()
const parser = new Gherkin.Parser(builder, matcher)
this.gherkinDocument = parser.parse(content.toString())
if (this.gherkinDocument.feature) {
this.feature = this.gherkinDocument.feature
}
}
public addError = (rule: Rule, message: string, location: Location): void => {
// Don't add the error if the line has been disabled
if (this.linesDisabled.get(location.line)) {
return
}
if (this.rulesDisabled.get(rule.name) === true) {
return
}
this.errors.push({
message,
location,
severity: rule.schema.severity,
rule: rule.name,
} as LintError)
}
private getDisabledRules = (): void => {
this.gherkinDocument.comments.forEach((comment) => {
const text = comment.text.trim()
if (comment.location.line === 1) {
if (text === '# gherklin-disable') {
this.disabled = true
return
}
const disableRuleMatch = text.match(/^# gherklin-disable ([a-zA-Z0-9-,\s]+)$/)
if (disableRuleMatch) {
const rules = (disableRuleMatch[1] || '').split(',')
rules.forEach((rule) => {
this.rulesDisabled.set(rule.trim(), true)
})
}
}
if (text === '# gherklin-disable-next-line') {
this.linesDisabled.set(comment.location.line + 1, true)
}
})
}
/**
* Regenerates the file from the lines array, overwriting the existing file
*/
public regenerate = async (): Promise<void> => {
const lines = []
this.lines.forEach((l) => {
let padding = []
if (l.indentation > 0) {
padding = Array(Number(l.indentation)).fill(' ')
}
lines.push(padding.join('') + l.keyword + l.text)
})
const content = lines.join('\n')
writeFileSync(this.path, content)
// Need to regenerate the Gherkin AST since some rules rely on that
this.parseGherkin(content)
}
}