UNPKG

eslint-plugin-svelte

Version:
233 lines (232 loc) 9.49 kB
import { Input } from 'postcss'; import tokenize from 'postcss/lib/tokenize'; import { createRule } from '../utils/index.js'; /** Check if a comment occupies the entire source line (matching ESLint core max-lines behavior). */ function isFullLineComment(line, lineNumber, loc) { return ((loc.start.line < lineNumber || !line.slice(0, loc.start.column).trim()) && (loc.end.line > lineNumber || !line.slice(loc.end.column).trim())); } /** Collect line numbers where AST comments occupy the full line. */ function collectAstCommentLines(comments, sourceLines, startLine, endLine) { const lines = new Set(); for (const comment of comments) { if (comment.loc.end.line < startLine || comment.loc.start.line > endLine) continue; for (let i = Math.max(comment.loc.start.line, startLine); i <= Math.min(comment.loc.end.line, endLine); i++) { if (isFullLineComment(sourceLines[i - 1], i, comment.loc)) { lines.add(i); } } } return lines; } /** Collect line numbers where CSS comments occupy the full line, using postcss tokenizer. */ function collectCssCommentLines(sourceLines, startLine, endLine) { const result = new Set(); const cssText = sourceLines.slice(startLine - 1, endLine).join('\n'); if (!cssText.trim()) return result; try { const input = new Input(cssText); const tk = tokenize(input); const commentLines = new Set(); const codeLines = new Set(); let token; while ((token = tk.nextToken())) { if (token[2] == null) continue; const startPos = input.fromOffset(token[2]); if (!startPos) continue; const tokenLine = startPos.line + startLine - 1; if (token[0] === 'comment') { const endPos = token[3] != null ? input.fromOffset(token[3]) : null; const commentEndLine = endPos ? endPos.line + startLine - 1 : tokenLine; for (let i = tokenLine; i <= commentEndLine; i++) { commentLines.add(i); } } else { codeLines.add(tokenLine); } } for (const line of commentLines) { if (!codeLines.has(line)) result.add(line); } } catch { // Malformed CSS — don't skip any lines } return result; } /** Count inner content lines, skipping blanks and/or comment lines. */ function countLines(sourceLines, startLine, endLine, skipBlankLines, commentLines) { if (endLine - startLine <= 1) return 0; let count = 0; for (let i = startLine + 1; i < endLine; i++) { if (skipBlankLines && sourceLines[i - 1].trim().length === 0) continue; if (commentLines.has(i)) continue; count++; } return count; } function isSvelteOptions(node) { return node.name.type === 'SvelteName' && node.name.name === 'svelte:options'; } export default createRule('max-lines-per-block', { meta: { docs: { description: 'enforce maximum number of lines in svelte component blocks', category: 'Stylistic Issues', recommended: false, conflictWithPrettier: false }, schema: [ { type: 'object', properties: { script: { type: 'integer', minimum: 1 }, template: { type: 'integer', minimum: 1 }, style: { type: 'integer', minimum: 1 }, skipBlankLines: { type: 'boolean' }, skipComments: { type: 'boolean' } }, additionalProperties: false } ], messages: { tooManyLines: '{{block}} block has too many lines ({{lineCount}}). Maximum allowed is {{max}}.' }, type: 'suggestion' }, create(context) { const options = context.options[0] ?? {}; const scriptMax = options.script; const templateMax = options.template; const styleMax = options.style; const skipBlankLines = options.skipBlankLines ?? false; const skipComments = options.skipComments ?? false; const sourceCode = context.sourceCode; const htmlCommentNodes = []; const emptySet = new Set(); return { SvelteHTMLComment(node) { htmlCommentNodes.push(node); }, SvelteScriptElement(node) { if (scriptMax == null) return; const commentLines = skipComments ? collectAstCommentLines(sourceCode.getAllComments(), sourceCode.lines, node.loc.start.line + 1, node.loc.end.line - 1) : emptySet; const lineCount = countLines(sourceCode.lines, node.loc.start.line, node.loc.end.line, skipBlankLines, commentLines); if (lineCount > scriptMax) { context.report({ node, messageId: 'tooManyLines', data: { block: '<script>', lineCount: String(lineCount), max: String(scriptMax) } }); } }, SvelteStyleElement(node) { if (styleMax == null) return; const commentLines = skipComments ? collectCssCommentLines(sourceCode.lines, node.loc.start.line + 1, node.loc.end.line - 1) : emptySet; const lineCount = countLines(sourceCode.lines, node.loc.start.line, node.loc.end.line, skipBlankLines, commentLines); if (lineCount > styleMax) { context.report({ node, messageId: 'tooManyLines', data: { block: '<style>', lineCount: String(lineCount), max: String(styleMax) } }); } }, 'Program:exit'(program) { if (templateMax == null) return; const totalLines = sourceCode.lines.length; // Exclude lines occupied by <script>, <style>, and <svelte:options> const excludedLines = new Set(); for (const child of program.body) { if (child.type === 'SvelteScriptElement' || child.type === 'SvelteStyleElement' || (child.type === 'SvelteElement' && isSvelteOptions(child))) { for (let i = child.loc.start.line; i <= child.loc.end.line; i++) { excludedLines.add(i); } } } // Collect full-line comment lines for template region const commentLines = new Set(); if (skipComments) { const allComments = [ ...htmlCommentNodes, ...sourceCode.getAllComments() ]; for (const comment of allComments) { for (let i = comment.loc.start.line; i <= comment.loc.end.line; i++) { if (excludedLines.has(i)) continue; if (isFullLineComment(sourceCode.lines[i - 1], i, comment.loc)) { commentLines.add(i); } } } } let templateLineCount = 0; for (let i = 1; i <= totalLines; i++) { if (excludedLines.has(i)) continue; if (skipBlankLines && sourceCode.lines[i - 1].trim().length === 0) continue; if (commentLines.has(i)) continue; templateLineCount++; } if (templateLineCount > templateMax) { const firstTemplateNode = program.body.find((child) => child.type !== 'SvelteScriptElement' && child.type !== 'SvelteStyleElement' && !(child.type === 'SvelteElement' && isSvelteOptions(child))); if (firstTemplateNode) { context.report({ node: firstTemplateNode, messageId: 'tooManyLines', data: { block: 'template', lineCount: String(templateLineCount), max: String(templateMax) } }); } } } }; } });