ifc-language-server/node_modules/@stylistic/eslint-plugin/dist/rules/padding-line-between-statements.js
Ryan Schultz 8afacf268a Implemented a working Language Server Protocol (LSP) for IFC files with:
- Hover provider showing entity information and type
- Go-to-definition (F12) for entity references
- Basic IFC file validation (ISO-10303-21 header check)
- Entity parsing with regex-based detection
- Proper CommonJS module system (avoiding ES module issues)

This replaces the broken baseline from ifc-developer-tools which had:
- Non-functional ES module configuration
- Circular dependency issues
- Parser crashes
- Non-working PositionVisitor

Built on Microsoft's LSP example template for a clean, maintainable foundation.

Next: Add hierarchical entity dependency tree in hover tooltip."
2025-12-07 10:20:07 -06:00

438 lines
16 KiB
JavaScript

'use strict';
var utils$1 = require('../utils.js');
var utils = require('@typescript-eslint/utils');
var astUtils = require('@typescript-eslint/utils/ast-utils');
require('eslint-visitor-keys');
require('espree');
require('estraverse');
const CJS_EXPORT = /^(?:module\s*\.\s*)?exports(?:\s*\.|\s*\[|$)/u;
const CJS_IMPORT = /^require\(/u;
const LT = `[${Array.from(
/* @__PURE__ */ new Set(["\r\n", "\r", "\n", "\u2028", "\u2029"])
).join("")}]`;
const PADDING_LINE_SEQUENCE = new RegExp(
String.raw`^(\s*?${LT})\s*${LT}(\s*;?)$`,
"u"
);
function newKeywordTester(type, keyword) {
return {
test(node, sourceCode) {
const isSameKeyword = sourceCode.getFirstToken(node)?.value === keyword;
const isSameType = Array.isArray(type) ? type.includes(node.type) : type === node.type;
return isSameKeyword && isSameType;
}
};
}
function newSinglelineKeywordTester(keyword) {
return {
test(node, sourceCode) {
return node.loc.start.line === node.loc.end.line && sourceCode.getFirstToken(node).value === keyword;
}
};
}
function newMultilineKeywordTester(keyword) {
return {
test(node, sourceCode) {
return node.loc.start.line !== node.loc.end.line && sourceCode.getFirstToken(node).value === keyword;
}
};
}
function newNodeTypeTester(type) {
return {
test: (node) => node.type === type
};
}
function skipChainExpression(node) {
return node && node.type === utils.AST_NODE_TYPES.ChainExpression ? node.expression : node;
}
function isIIFEStatement(node) {
if (node.type === utils.AST_NODE_TYPES.ExpressionStatement) {
let expression = skipChainExpression(node.expression);
if (expression.type === utils.AST_NODE_TYPES.UnaryExpression)
expression = skipChainExpression(expression.argument);
if (expression.type === utils.AST_NODE_TYPES.CallExpression) {
let node2 = expression.callee;
while (node2.type === utils.AST_NODE_TYPES.SequenceExpression)
node2 = node2.expressions[node2.expressions.length - 1];
return astUtils.isFunction(node2);
}
}
return false;
}
function isCJSRequire(node) {
if (node.type === utils.AST_NODE_TYPES.VariableDeclaration) {
const declaration = node.declarations[0];
if (declaration?.init) {
let call = declaration?.init;
while (call.type === utils.AST_NODE_TYPES.MemberExpression)
call = call.object;
if (call.type === utils.AST_NODE_TYPES.CallExpression && call.callee.type === utils.AST_NODE_TYPES.Identifier) {
return call.callee.name === "require";
}
}
}
return false;
}
function isBlockLikeStatement(node, sourceCode) {
if (node.type === utils.AST_NODE_TYPES.DoWhileStatement && node.body.type === utils.AST_NODE_TYPES.BlockStatement) {
return true;
}
if (isIIFEStatement(node))
return true;
const lastToken = sourceCode.getLastToken(node, astUtils.isNotSemicolonToken);
const belongingNode = lastToken && astUtils.isClosingBraceToken(lastToken) ? sourceCode.getNodeByRangeIndex(lastToken.range[0]) : null;
return !!belongingNode && (belongingNode.type === utils.AST_NODE_TYPES.BlockStatement || belongingNode.type === utils.AST_NODE_TYPES.SwitchStatement);
}
function isDirective(node, sourceCode) {
return node.type === utils.AST_NODE_TYPES.ExpressionStatement && (node.parent?.type === utils.AST_NODE_TYPES.Program || node.parent?.type === utils.AST_NODE_TYPES.BlockStatement && astUtils.isFunction(node.parent.parent)) && node.expression.type === utils.AST_NODE_TYPES.Literal && typeof node.expression.value === "string" && !astUtils.isParenthesized(node.expression, sourceCode);
}
function isDirectivePrologue(node, sourceCode) {
if (isDirective(node, sourceCode) && node.parent && "body" in node.parent && Array.isArray(node.parent.body)) {
for (const sibling of node.parent.body) {
if (sibling === node)
break;
if (!isDirective(sibling, sourceCode))
return false;
}
return true;
}
return false;
}
function isCJSExport(node) {
if (node.type === utils.AST_NODE_TYPES.ExpressionStatement) {
const expression = node.expression;
if (expression.type === utils.AST_NODE_TYPES.AssignmentExpression) {
let left = expression.left;
if (left.type === utils.AST_NODE_TYPES.MemberExpression) {
while (left.object.type === utils.AST_NODE_TYPES.MemberExpression)
left = left.object;
return left.object.type === utils.AST_NODE_TYPES.Identifier && (left.object.name === "exports" || left.object.name === "module" && left.property.type === utils.AST_NODE_TYPES.Identifier && left.property.name === "exports");
}
}
}
return false;
}
function isExpression(node, sourceCode) {
return node.type === utils.AST_NODE_TYPES.ExpressionStatement && !isDirectivePrologue(node, sourceCode);
}
function getActualLastToken(node, sourceCode) {
const semiToken = sourceCode.getLastToken(node);
const prevToken = sourceCode.getTokenBefore(semiToken);
const nextToken = sourceCode.getTokenAfter(semiToken);
const isSemicolonLessStyle = prevToken && nextToken && prevToken.range[0] >= node.range[0] && astUtils.isSemicolonToken(semiToken) && semiToken.loc.start.line !== prevToken.loc.end.line && semiToken.loc.end.line === nextToken.loc.start.line;
return isSemicolonLessStyle ? prevToken : semiToken;
}
function replacerToRemovePaddingLines(_, trailingSpaces, indentSpaces) {
return trailingSpaces + indentSpaces;
}
function verifyForAny() {
}
function verifyForNever(context, _, nextNode, paddingLines) {
if (paddingLines.length === 0)
return;
context.report({
node: nextNode,
messageId: "unexpectedBlankLine",
fix(fixer) {
if (paddingLines.length >= 2)
return null;
const prevToken = paddingLines[0][0];
const nextToken = paddingLines[0][1];
const start = prevToken.range[1];
const end = nextToken.range[0];
const text = context.getSourceCode().text.slice(start, end).replace(PADDING_LINE_SEQUENCE, replacerToRemovePaddingLines);
return fixer.replaceTextRange([start, end], text);
}
});
}
function verifyForAlways(context, prevNode, nextNode, paddingLines) {
if (paddingLines.length > 0)
return;
context.report({
node: nextNode,
messageId: "expectedBlankLine",
fix(fixer) {
const sourceCode = context.sourceCode;
let prevToken = getActualLastToken(prevNode, sourceCode);
const nextToken = sourceCode.getFirstTokenBetween(prevToken, nextNode, {
includeComments: true,
/**
* Skip the trailing comments of the previous node.
* This inserts a blank line after the last trailing comment.
*
* For example:
*
* foo(); // trailing comment.
* // comment.
* bar();
*
* Get fixed to:
*
* foo(); // trailing comment.
*
* // comment.
* bar();
* @param token The token to check.
* @returns `true` if the token is not a trailing comment.
* @private
*/
filter(token) {
if (astUtils.isTokenOnSameLine(prevToken, token)) {
prevToken = token;
return false;
}
return true;
}
}) || nextNode;
const insertText = astUtils.isTokenOnSameLine(prevToken, nextToken) ? "\n\n" : "\n";
return fixer.insertTextAfter(prevToken, insertText);
}
});
}
const PaddingTypes = {
any: { verify: verifyForAny },
never: { verify: verifyForNever },
always: { verify: verifyForAlways }
};
const StatementTypes = {
"*": { test: () => true },
"block-like": { test: isBlockLikeStatement },
"exports": { test: isCJSExport },
"require": { test: isCJSRequire },
"directive": { test: isDirectivePrologue },
"expression": { test: isExpression },
"iife": { test: isIIFEStatement },
"multiline-block-like": {
test: (node, sourceCode) => node.loc.start.line !== node.loc.end.line && isBlockLikeStatement(node, sourceCode)
},
"multiline-expression": {
test: (node, sourceCode) => node.loc.start.line !== node.loc.end.line && node.type === utils.AST_NODE_TYPES.ExpressionStatement && !isDirectivePrologue(node, sourceCode)
},
"multiline-const": newMultilineKeywordTester("const"),
"multiline-let": newMultilineKeywordTester("let"),
"multiline-var": newMultilineKeywordTester("var"),
"singleline-const": newSinglelineKeywordTester("const"),
"singleline-let": newSinglelineKeywordTester("let"),
"singleline-var": newSinglelineKeywordTester("var"),
"block": newNodeTypeTester(utils.AST_NODE_TYPES.BlockStatement),
"empty": newNodeTypeTester(utils.AST_NODE_TYPES.EmptyStatement),
"function": newNodeTypeTester(utils.AST_NODE_TYPES.FunctionDeclaration),
"ts-method": newNodeTypeTester(utils.AST_NODE_TYPES.TSMethodSignature),
"break": newKeywordTester(utils.AST_NODE_TYPES.BreakStatement, "break"),
"case": newKeywordTester(utils.AST_NODE_TYPES.SwitchCase, "case"),
"class": newKeywordTester(utils.AST_NODE_TYPES.ClassDeclaration, "class"),
"const": newKeywordTester(utils.AST_NODE_TYPES.VariableDeclaration, "const"),
"continue": newKeywordTester(utils.AST_NODE_TYPES.ContinueStatement, "continue"),
"debugger": newKeywordTester(utils.AST_NODE_TYPES.DebuggerStatement, "debugger"),
"default": newKeywordTester(
[utils.AST_NODE_TYPES.SwitchCase, utils.AST_NODE_TYPES.ExportDefaultDeclaration],
"default"
),
"do": newKeywordTester(utils.AST_NODE_TYPES.DoWhileStatement, "do"),
"export": newKeywordTester(
[
utils.AST_NODE_TYPES.ExportAllDeclaration,
utils.AST_NODE_TYPES.ExportDefaultDeclaration,
utils.AST_NODE_TYPES.ExportNamedDeclaration
],
"export"
),
"for": newKeywordTester(
[
utils.AST_NODE_TYPES.ForStatement,
utils.AST_NODE_TYPES.ForInStatement,
utils.AST_NODE_TYPES.ForOfStatement
],
"for"
),
"if": newKeywordTester(utils.AST_NODE_TYPES.IfStatement, "if"),
"import": newKeywordTester(utils.AST_NODE_TYPES.ImportDeclaration, "import"),
"let": newKeywordTester(utils.AST_NODE_TYPES.VariableDeclaration, "let"),
"return": newKeywordTester(utils.AST_NODE_TYPES.ReturnStatement, "return"),
"switch": newKeywordTester(utils.AST_NODE_TYPES.SwitchStatement, "switch"),
"throw": newKeywordTester(utils.AST_NODE_TYPES.ThrowStatement, "throw"),
"try": newKeywordTester(utils.AST_NODE_TYPES.TryStatement, "try"),
"var": newKeywordTester(utils.AST_NODE_TYPES.VariableDeclaration, "var"),
"while": newKeywordTester(
[utils.AST_NODE_TYPES.WhileStatement, utils.AST_NODE_TYPES.DoWhileStatement],
"while"
),
"with": newKeywordTester(utils.AST_NODE_TYPES.WithStatement, "with"),
"cjs-export": {
test: (node, sourceCode) => node.type === "ExpressionStatement" && node.expression.type === "AssignmentExpression" && CJS_EXPORT.test(sourceCode.getText(node.expression.left))
},
"cjs-import": {
test: (node, sourceCode) => node.type === "VariableDeclaration" && node.declarations.length > 0 && Boolean(node.declarations[0].init) && CJS_IMPORT.test(sourceCode.getText(node.declarations[0].init))
},
// Additional Typescript constructs
"enum": newKeywordTester(
utils.AST_NODE_TYPES.TSEnumDeclaration,
"enum"
),
"interface": newKeywordTester(
utils.AST_NODE_TYPES.TSInterfaceDeclaration,
"interface"
),
"type": newKeywordTester(
utils.AST_NODE_TYPES.TSTypeAliasDeclaration,
"type"
),
"function-overload": {
test: (node) => node.type === "TSDeclareFunction"
}
};
var paddingLineBetweenStatements = utils$1.createRule({
name: "padding-line-between-statements",
package: "ts",
meta: {
type: "layout",
docs: {
description: "Require or disallow padding lines between statements"
},
fixable: "whitespace",
hasSuggestions: false,
// This is intentionally an array schema as you can pass 0..n config objects
schema: {
$defs: {
paddingType: {
type: "string",
enum: Object.keys(PaddingTypes)
},
statementType: {
anyOf: [
{
type: "string",
enum: Object.keys(StatementTypes)
},
{
type: "array",
items: {
type: "string",
enum: Object.keys(StatementTypes)
},
minItems: 1,
uniqueItems: true,
additionalItems: false
}
]
}
},
type: "array",
additionalItems: false,
items: {
type: "object",
properties: {
blankLine: { $ref: "#/$defs/paddingType" },
prev: { $ref: "#/$defs/statementType" },
next: { $ref: "#/$defs/statementType" }
},
additionalProperties: false,
required: ["blankLine", "prev", "next"]
}
},
messages: {
unexpectedBlankLine: "Unexpected blank line before this statement.",
expectedBlankLine: "Expected blank line before this statement."
}
},
defaultOptions: [],
create(context) {
const sourceCode = context.sourceCode;
const configureList = context.options || [];
let scopeInfo = null;
function enterScope() {
scopeInfo = {
upper: scopeInfo,
prevNode: null
};
}
function exitScope() {
if (scopeInfo)
scopeInfo = scopeInfo.upper;
}
function match(node, type) {
let innerStatementNode = node;
while (innerStatementNode.type === utils.AST_NODE_TYPES.LabeledStatement)
innerStatementNode = innerStatementNode.body;
if (Array.isArray(type))
return type.some(match.bind(null, innerStatementNode));
return StatementTypes[type].test(innerStatementNode, sourceCode);
}
function getPaddingType(prevNode, nextNode) {
for (let i = configureList.length - 1; i >= 0; --i) {
const configure = configureList[i];
if (match(prevNode, configure.prev) && match(nextNode, configure.next)) {
return PaddingTypes[configure.blankLine];
}
}
return PaddingTypes.any;
}
function getPaddingLineSequences(prevNode, nextNode) {
const pairs = [];
let prevToken = getActualLastToken(prevNode, sourceCode);
if (nextNode.loc.start.line - prevToken.loc.end.line >= 2) {
do {
const token = sourceCode.getTokenAfter(prevToken, {
includeComments: true
});
if (token.loc.start.line - prevToken.loc.end.line >= 2)
pairs.push([prevToken, token]);
prevToken = token;
} while (prevToken.range[0] < nextNode.range[0]);
}
return pairs;
}
function verify(node) {
if (!node.parent || ![
utils.AST_NODE_TYPES.BlockStatement,
utils.AST_NODE_TYPES.Program,
utils.AST_NODE_TYPES.StaticBlock,
utils.AST_NODE_TYPES.SwitchCase,
utils.AST_NODE_TYPES.SwitchStatement,
utils.AST_NODE_TYPES.TSInterfaceBody,
utils.AST_NODE_TYPES.TSModuleBlock,
utils.AST_NODE_TYPES.TSTypeLiteral
].includes(node.parent.type)) {
return;
}
const prevNode = scopeInfo.prevNode;
if (prevNode) {
const type = getPaddingType(prevNode, node);
const paddingLines = getPaddingLineSequences(prevNode, node);
type.verify(context, prevNode, node, paddingLines);
}
scopeInfo.prevNode = node;
}
function verifyThenEnterScope(node) {
verify(node);
enterScope();
}
return {
"Program": enterScope,
"BlockStatement": enterScope,
"SwitchStatement": enterScope,
"StaticBlock": enterScope,
"TSInterfaceBody": enterScope,
"TSModuleBlock": enterScope,
"TSTypeLiteral": enterScope,
"Program:exit": exitScope,
"BlockStatement:exit": exitScope,
"SwitchStatement:exit": exitScope,
"StaticBlock:exit": exitScope,
"TSInterfaceBody:exit": exitScope,
"TSModuleBlock:exit": exitScope,
"TSTypeLiteral:exit": exitScope,
":statement": verify,
"SwitchCase": verifyThenEnterScope,
"TSDeclareFunction": verifyThenEnterScope,
"TSMethodSignature": verifyThenEnterScope,
"SwitchCase:exit": exitScope,
"TSDeclareFunction:exit": exitScope,
"TSMethodSignature:exit": exitScope
};
}
});
module.exports = paddingLineBetweenStatements;