///|
pub(all) struct SourcePosition {
line : Int
column : Int
offset : Int
} derive(Debug, Eq)
///|
pub(all) struct SourceRange {
start : SourcePosition
end : SourcePosition
} derive(Debug, Eq)
///|
pub(all) struct LineSlice {
line : Int
start_offset : Int
end_offset : Int
text : String
} derive(Debug, Eq)
///|
pub(all) struct LineIndex {
source : String
lines : Array[LineSlice]
chars : Int
} derive(Debug, Eq)
///|
pub(all) struct LocatedFinding {
finding : Finding
position : SourcePosition
line_text : String
caret : String
} derive(Debug, Eq)
///|
fn clamp_position_value(value : Int, low : Int, high : Int) -> Int {
if value < low {
low
} else if value > high {
high
} else {
value
}
}
///|
fn count_chars_for_index(input : String) -> Int {
input.iter().count()
}
///|
fn make_line_slice(
line : Int,
start_offset : Int,
end_offset : Int,
chars : Array[Char],
) -> LineSlice {
{ line, start_offset, end_offset, text: String::from_array(chars) }
}
///|
pub fn build_line_index(source : String) -> LineIndex {
let lines : Array[LineSlice] = []
let current : Array[Char] = []
let mut line = 1
let mut line_start = 0
let mut offset = 0
for c in source {
if c == '\n' {
lines.push(make_line_slice(line, line_start, offset, current))
current.clear()
offset += 1
line += 1
line_start = offset
} else {
current.push(c)
offset += 1
}
}
lines.push(make_line_slice(line, line_start, offset, current))
{ source, lines, chars: offset }
}
///|
fn fallback_empty_line() -> LineSlice {
{ line: 1, start_offset: 0, end_offset: 0, text: "" }
}
///|
pub fn LineIndex::line_count(self : LineIndex) -> Int {
self.lines.length()
}
///|
pub fn LineIndex::is_empty(self : LineIndex) -> Bool {
self.chars == 0
}
///|
pub fn LineIndex::line_at(self : LineIndex, line : Int) -> LineSlice? {
if line <= 0 {
return None
}
let index = line - 1
if index >= self.lines.length() {
None
} else {
Some(self.lines[index])
}
}
///|
pub fn LineIndex::line_for_offset(self : LineIndex, offset : Int) -> LineSlice {
if self.lines.length() == 0 {
return fallback_empty_line()
}
let target = clamp_position_value(offset, 0, self.chars)
for slice in self.lines {
if target >= slice.start_offset && target <= slice.end_offset {
return slice
}
}
self.lines[self.lines.length() - 1]
}
///|
pub fn LineIndex::position_at(self : LineIndex, offset : Int) -> SourcePosition {
let target = clamp_position_value(offset, 0, self.chars)
let slice = self.line_for_offset(target)
let raw_column = target - slice.start_offset + 1
let max_column = count_chars_for_index(slice.text) + 1
{
line: slice.line,
column: clamp_position_value(raw_column, 1, max_column),
offset: target,
}
}
///|
pub fn SourcePosition::format(self : SourcePosition) -> String {
"line \{self.line}, column \{self.column}"
}
///|
pub fn SourcePosition::compact(self : SourcePosition) -> String {
"\{self.line}:\{self.column}"
}
///|
pub fn format_position(position : SourcePosition) -> String {
position.format()
}
///|
pub fn compact_position(position : SourcePosition) -> String {
position.compact()
}
///|
fn repeat_index_char(c : Char, count : Int) -> String {
let out = StringBuilder::new()
let mut i = 0
while i < count {
out.write_char(c)
i += 1
}
out.to_string()
}
///|
pub fn caret_for_column(column : Int) -> String {
repeat_index_char(' ', clamp_position_value(column - 1, 0, column)) + "^"
}
///|
pub fn LineIndex::caret_at(self : LineIndex, offset : Int) -> String {
caret_for_column(self.position_at(offset).column)
}
///|
fn line_number_width(max_line : Int) -> Int {
let mut value = max_line
let mut width = 1
while value >= 10 {
value = value / 10
width += 1
}
width
}
///|
fn pad_left_number(value : Int, width : Int) -> String {
let text = value.to_string()
let missing = width - text.length()
if missing <= 0 {
text
} else {
repeat_index_char(' ', missing) + text
}
}
///|
fn prefix_for_line(line : Int, width : Int) -> String {
pad_left_number(line, width) + " | "
}
///|
pub fn LineIndex::line_preview(self : LineIndex, line : Int) -> String {
match self.line_at(line) {
Some(slice) => visible(slice.text)
None => ""
}
}
///|
pub fn LineIndex::line_with_number(self : LineIndex, line : Int) -> String {
let width = line_number_width(self.line_count())
match self.line_at(line) {
Some(slice) => prefix_for_line(line, width) + visible(slice.text)
None => prefix_for_line(line, width) + ""
}
}
///|
fn bounded_context_start(line : Int, context : Int) -> Int {
let start = line - context
if start < 1 {
1
} else {
start
}
}
///|
fn bounded_context_end(line : Int, context : Int, max_line : Int) -> Int {
let end = line + context
if end > max_line {
max_line
} else {
end
}
}
///|
pub fn LineIndex::window_at(
self : LineIndex,
offset : Int,
context? : Int = 1,
) -> String {
let position = self.position_at(offset)
let safe_context = if context < 0 { 0 } else { context }
let start = bounded_context_start(position.line, safe_context)
let end = bounded_context_end(position.line, safe_context, self.line_count())
let width = line_number_width(self.line_count())
let out = StringBuilder::new()
let mut line = start
while line <= end {
guard self.line_at(line) is Some(slice) else {
line += 1
continue
}
out.write_string(prefix_for_line(line, width))
out.write_string(visible(slice.text))
out.write_char('\n')
if line == position.line {
out.write_string(repeat_index_char(' ', width))
out.write_string(" | ")
out.write_string(caret_for_column(position.column))
out.write_char('\n')
}
line += 1
}
out.to_string()
}
///|
pub fn line_window(source : String, offset : Int, context? : Int = 1) -> String {
build_line_index(source).window_at(offset, context~)
}
///|
pub fn location_string(source : String, offset : Int) -> String {
build_line_index(source).position_at(offset).format()
}
///|
pub fn compact_location_string(source : String, offset : Int) -> String {
build_line_index(source).position_at(offset).compact()
}
///|
fn make_located_finding(index : LineIndex, finding : Finding) -> LocatedFinding {
let position = index.position_at(finding.offset)
let line = index.line_for_offset(finding.offset)
{
finding,
position,
line_text: line.text,
caret: caret_for_column(position.column),
}
}
///|
pub fn locate_findings(
source : String,
findings : Array[Finding],
) -> Array[LocatedFinding] {
let index = build_line_index(source)
let located : Array[LocatedFinding] = []
for finding in findings {
located.push(make_located_finding(index, finding))
}
located
}
///|
pub fn AuditResult::located(
self : AuditResult,
source : String,
) -> Array[LocatedFinding] {
locate_findings(source, self.findings)
}
///|
pub fn LocatedFinding::summary(self : LocatedFinding) -> String {
"\{self.position.compact()} " +
"\{severity_name(self.finding.severity)} " +
"\{self.finding.rule_id}: " +
self.finding.message
}
///|
pub fn LocatedFinding::markdown_row(self : LocatedFinding) -> String {
"| \{self.position.line} | \{self.position.column} | " +
markdown_escape(severity_name(self.finding.severity)) +
" | " +
markdown_escape(self.finding.rule_id) +
" | " +
markdown_escape(risk_name(self.finding.risk)) +
" | `" +
markdown_escape(self.finding.sequence) +
"` | " +
markdown_escape(self.finding.message) +
" |"
}
///|
pub fn located_findings_text(located : Array[LocatedFinding]) -> String {
let out = StringBuilder::new()
for item in located {
out.write_string(item.summary())
out.write_char('\n')
out.write_string(visible(item.line_text))
out.write_char('\n')
out.write_string(item.caret)
out.write_char('\n')
}
out.to_string()
}
///|
pub fn located_findings_markdown(located : Array[LocatedFinding]) -> String {
let out = StringBuilder::new()
out.write_string(
"| Line | Column | Severity | Rule | Risk | Sequence | Description |\n",
)
out.write_string("|---:|---:|---|---|---|---|---|\n")
for item in located {
out.write_string(item.markdown_row())
out.write_char('\n')
}
out.to_string()
}
///|
pub fn located_findings_json(located : Array[LocatedFinding]) -> String {
let out = StringBuilder::new()
out.write_char('[')
let mut first = true
for item in located {
if !first {
out.write_char(',')
}
first = false
out.write_string("{\"line\":")
out.write_string(item.position.line.to_string())
out.write_string(",\"column\":")
out.write_string(item.position.column.to_string())
out.write_string(",\"offset\":")
out.write_string(item.position.offset.to_string())
out.write_string(",\"rule_id\":")
out.write_string(json_quote(item.finding.rule_id))
out.write_string(",\"severity\":")
out.write_string(json_quote(severity_name(item.finding.severity)))
out.write_string(",\"risk\":")
out.write_string(json_quote(risk_name(item.finding.risk)))
out.write_string(",\"sequence\":")
out.write_string(json_quote(item.finding.sequence))
out.write_string(",\"line_text\":")
out.write_string(json_quote(item.line_text))
out.write_string("}")
}
out.write_char(']')
out.to_string()
}
///|
pub fn LineIndex::range_for_event(
self : LineIndex,
event : Event,
) -> SourceRange {
let start = self.position_at(event.offset)
let finish = self.position_at(event.offset + event.raw.iter().count())
{ start, end: finish }
}
///|
pub fn LineIndex::range_for_finding(
self : LineIndex,
finding : Finding,
) -> SourceRange {
let start = self.position_at(finding.offset)
let finish = self.position_at(
finding.offset + finding.sequence.iter().count(),
)
{ start, end: finish }
}
///|
pub fn SourceRange::format(self : SourceRange) -> String {
if self.start.line == self.end.line {
"line \{self.start.line}, columns \{self.start.column}-\{self.end.column}"
} else {
"\{self.start.compact()}..\{self.end.compact()}"
}
}
///|
pub fn line_index_summary(index : LineIndex) -> String {
"\{index.line_count()} line(s), \{index.chars} character(s)"
}