@ton-ai-core/vibecode-linter
Version:
Advanced TypeScript linter with Git integration, dependency analysis, and comprehensive error reporting
155 lines • 6.23 kB
JavaScript
// CHANGE: Extract pure highlight/formatting utilities from shell/output
// WHY: FCIS — keep all pure computations in CORE; SHELL only does IO (console/file)
// QUOTE(ТЗ): "CORE: Исключительно чистые функции, неизменяемые данные"
// REF: Architecture plan - move output formatting logic to core
// PURITY: CORE
// INVARIANT: No side effects; deterministic mapping from inputs to outputs
// COMPLEXITY: O(n) per line where n = |currentLine|
import { pipe } from "effect";
/**
* Calculate next visual width when consuming a character.
*
* @pure true
*/
function calculateVisualWidth(char, currentVisual) {
if (char === "\t") {
const tabSize = 8;
return Math.floor(currentVisual / tabSize + 1) * tabSize;
}
if (char === "\r")
return currentVisual;
return currentVisual + 1;
}
/**
* Find real (string index) column for a desired visual column on a line.
*
* @param currentLine - immutable line content
* @param targetVisualColumn - desired visual column (0-based)
* @returns real column index (0..line.length)
*
* @pure true
* @invariant 0 ≤ result ≤ currentLine.length
* @complexity O(n) where n = |currentLine|
*/
export function calculateColumnPosition(currentLine, targetVisualColumn) {
let realColumn = 0;
let visualColumn = 0;
for (let charIndex = 0; charIndex <= currentLine.length; charIndex += 1) {
if (visualColumn >= targetVisualColumn) {
realColumn = charIndex;
break;
}
if (charIndex >= currentLine.length) {
realColumn = currentLine.length;
break;
}
const ch = currentLine.charAt(charIndex);
// exact-optional-char handling
if (ch !== "")
visualColumn = calculateVisualWidth(ch, visualColumn);
}
if (visualColumn < targetVisualColumn)
realColumn = currentLine.length;
return Math.max(0, Math.min(realColumn, currentLine.length));
}
/**
* Skip whitespace in a line starting from given index.
*
* @pure true
* @invariant while ensures pos < length → charAt(pos) !== ""
*/
function skipWhitespace(line, start) {
let pos = start;
while (pos < line.length) {
const ch = line.charAt(pos);
// CHANGE: Remove ch === "" check (unreachable due to while condition)
// WHY: while ensures pos < line.length → charAt never returns ""
// INVARIANT: ∀ pos < line.length. charAt(pos) ∈ line (non-empty)
if (!/\s/.test(ch))
break;
pos += 1;
}
return pos;
}
/**
* Heuristic: when TS complains about "Expected N arguments", highlight
* from next argument position.
*
* @pure true
* @invariant funcCallMatch exists → "(" found in string → openParenPos ≥ 0
*/
function calculateFunctionArgsEnd(currentLine, startCol) {
const beforeCursor = currentLine.substring(0, startCol + 15);
const funcCallMatch = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*$/.exec(beforeCursor);
if (!funcCallMatch)
return startCol + 1;
// CHANGE: Remove targetPos === -1 check (unreachable)
// WHY: Regex /\([^)]*$/ guarantees "(" exists → indexOf("(") >= 0
// INVARIANT: funcCallMatch exists → openParenPos >= 0 → targetPos >= 0
const lastCommaPos = beforeCursor.lastIndexOf(",");
const openParenPos = beforeCursor.lastIndexOf("(");
const targetPos = Math.max(lastCommaPos, openParenPos);
const newStartCol = skipWhitespace(currentLine, targetPos + 1);
return newStartCol + 1;
}
/**
* Heuristic: highlight a word at the given start when not TypeScript specific.
*
* @pure true
* @invariant charAtPos ∈ [a-zA-Z_$] → wordMatch !== null
*/
function calculateWordEnd(currentLine, startCol) {
const charAtPos = currentLine.charAt(startCol);
if (charAtPos === "" || !/[a-zA-Z_$]/.test(charAtPos))
return startCol + 1;
// CHANGE: Use type guard instead of non-null assertion
// WHY: After charAtPos passes /[a-zA-Z_$]/ test, match always succeeds
// INVARIANT: ∀ line, col: charAt(col) ∈ [a-zA-Z_$] → match finds ≥1 char
// PROOF: If charAt(startCol) ∈ [a-zA-Z_$], then substring(startCol)[0] ∈ [a-zA-Z_$]
// Therefore regex /^[a-zA-Z_$][a-zA-Z0-9_$]*/ must match at least 1 char
// Thus wordMatch will always be non-null (guaranteed by invariant)
const remainingLine = currentLine.substring(startCol);
const wordMatch = /^[a-zA-Z_$][a-zA-Z0-9_$]*/.exec(remainingLine);
const wordLen = wordMatch ? wordMatch[0].length : 0;
return Math.min(startCol + wordLen, currentLine.length);
}
/**
* Compute highlight range for a lint message on a given line.
*
* @param m - LintMessage with file and positions
* @param currentLine - line content
* @param startCol - starting real column index
* @returns [start, end) range as closed interval on string indices
*
* @pure true
* @invariant 0 ≤ start ≤ end ≤ currentLine.length
* @complexity O(1) (heuristics) + O(k) where k depends on local parsing
*/
export function calculateHighlightRange(m, currentLine, startCol) {
return pipe(m,
// CHANGE: Use pipe for functional composition of highlight calculation
// WHY: Makes control flow explicit and composable
// INVARIANT: endCol computation is pure deterministic function of (m, currentLine, startCol)
(msg) => {
const { source, message } = msg;
if ("endColumn" in msg &&
typeof msg.endColumn === "number" &&
Number.isFinite(msg.endColumn)) {
return Math.min(msg.endColumn - 1, currentLine.length);
}
if (source === "typescript") {
if (message.includes("Expected") && message.includes("arguments")) {
return calculateFunctionArgsEnd(currentLine, startCol);
}
return calculateWordEnd(currentLine, startCol);
}
return startCol + 1;
},
// CHANGE: Compose endCol → {start, end} transformation
// INVARIANT: start ≤ end ≤ currentLine.length
(endCol) => ({
start: startCol,
end: Math.max(startCol, Math.min(endCol, currentLine.length)),
}));
}
//# sourceMappingURL=highlight.js.map