- 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."
186 lines
7.5 KiB
JavaScript
186 lines
7.5 KiB
JavaScript
'use strict';
|
|
|
|
var utils = require('../utils.js');
|
|
require('eslint-visitor-keys');
|
|
require('espree');
|
|
require('estraverse');
|
|
|
|
const optionDefaults = {
|
|
allow: "none"
|
|
};
|
|
const messages = {
|
|
moveToNewLine: "`{{descriptor}}` must be placed on a new line"
|
|
};
|
|
var jsxOneExpressionPerLine = utils.createRule({
|
|
name: "jsx-one-expression-per-line",
|
|
package: "jsx",
|
|
meta: {
|
|
type: "layout",
|
|
docs: {
|
|
description: "Require one JSX element per line"
|
|
},
|
|
fixable: "whitespace",
|
|
messages,
|
|
schema: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
allow: {
|
|
type: "string",
|
|
enum: ["none", "literal", "single-child", "single-line", "non-jsx"]
|
|
}
|
|
},
|
|
default: optionDefaults,
|
|
additionalProperties: false
|
|
}
|
|
]
|
|
},
|
|
create(context) {
|
|
const options = Object.assign({}, optionDefaults, context.options[0]);
|
|
function nodeKey(node) {
|
|
return `${node.loc.start.line},${node.loc.start.column}`;
|
|
}
|
|
function nodeDescriptor(n) {
|
|
return "openingElement" in n && n.openingElement && "name" in n.openingElement.name ? String(n.openingElement.name.name) : context.sourceCode.getText(n).replace(/\n/g, "");
|
|
}
|
|
function handleJSX(node) {
|
|
const children = node.children;
|
|
if (!children || !children.length)
|
|
return;
|
|
if (options.allow === "non-jsx" && !children.find((child) => child.type === "JSXFragment" || child.type === "JSXElement")) {
|
|
return;
|
|
}
|
|
const openingElement = node.openingElement || node.openingFragment;
|
|
const closingElement = node.closingElement || node.closingFragment;
|
|
const openingElementStartLine = openingElement.loc.start.line;
|
|
const openingElementEndLine = openingElement.loc.end.line;
|
|
const closingElementStartLine = closingElement.loc.start.line;
|
|
const closingElementEndLine = closingElement.loc.end.line;
|
|
if (children.length === 1) {
|
|
const child = children[0];
|
|
if (openingElementStartLine === openingElementEndLine && openingElementEndLine === closingElementStartLine && closingElementStartLine === closingElementEndLine && closingElementEndLine === child.loc.start.line && child.loc.start.line === child.loc.end.line) {
|
|
if (options.allow === "single-child" || options.allow === "literal" && (child.type === "Literal" || child.type === "JSXText") || options.allow === "single-line") {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (options.allow === "single-line") {
|
|
const firstChild = children[0];
|
|
const lastChild = children[children.length - 1];
|
|
const lineDifference = lastChild.loc.end.line - firstChild.loc.start.line;
|
|
let lineBreaks = 0;
|
|
if (firstChild.type === "Literal" || firstChild.type === "JSXText") {
|
|
if (/^\s*?\n/.test(firstChild.raw))
|
|
lineBreaks += 1;
|
|
}
|
|
if (lastChild.type === "Literal" || lastChild.type === "JSXText") {
|
|
if (/\n\s*$/.test(lastChild.raw))
|
|
lineBreaks += 1;
|
|
}
|
|
if (lineDifference === 0 && lineBreaks === 0 || lineDifference === 2 && lineBreaks === 2)
|
|
return;
|
|
}
|
|
const childrenGroupedByLine = {};
|
|
const fixDetailsByNode = {};
|
|
children.forEach((child) => {
|
|
let countNewLinesBeforeContent = 0;
|
|
let countNewLinesAfterContent = 0;
|
|
if (child.type === "Literal" || child.type === "JSXText") {
|
|
if (utils.isWhiteSpaces(child.raw))
|
|
return;
|
|
countNewLinesBeforeContent = (child.raw.match(/^\s*\n/g) || []).length;
|
|
countNewLinesAfterContent = (child.raw.match(/\n\s*$/g) || []).length;
|
|
}
|
|
const startLine = child.loc.start.line + countNewLinesBeforeContent;
|
|
const endLine = child.loc.end.line - countNewLinesAfterContent;
|
|
if (startLine === endLine) {
|
|
if (!childrenGroupedByLine[startLine])
|
|
childrenGroupedByLine[startLine] = [];
|
|
childrenGroupedByLine[startLine].push(child);
|
|
} else {
|
|
if (!childrenGroupedByLine[startLine])
|
|
childrenGroupedByLine[startLine] = [];
|
|
childrenGroupedByLine[startLine].push(child);
|
|
if (!childrenGroupedByLine[endLine])
|
|
childrenGroupedByLine[endLine] = [];
|
|
childrenGroupedByLine[endLine].push(child);
|
|
}
|
|
});
|
|
Object.keys(childrenGroupedByLine).forEach((_line) => {
|
|
const line = parseInt(_line, 10);
|
|
const firstIndex = 0;
|
|
const lastIndex = childrenGroupedByLine[line].length - 1;
|
|
childrenGroupedByLine[line].forEach((child, i) => {
|
|
let prevChild;
|
|
let nextChild;
|
|
if (i === firstIndex) {
|
|
if (line === openingElementEndLine)
|
|
prevChild = openingElement;
|
|
} else {
|
|
prevChild = childrenGroupedByLine[line][i - 1];
|
|
}
|
|
if (i === lastIndex) {
|
|
if (line === closingElementStartLine)
|
|
nextChild = closingElement;
|
|
}
|
|
if (!prevChild && !nextChild)
|
|
return;
|
|
const spaceBetweenPrev = () => {
|
|
return (prevChild.type === "Literal" || prevChild.type === "JSXText") && prevChild.raw.endsWith(" ") || (child.type === "Literal" || child.type === "JSXText") && child.raw.startsWith(" ") || context.sourceCode.isSpaceBetweenTokens(prevChild, child);
|
|
};
|
|
const spaceBetweenNext = () => {
|
|
return (nextChild.type === "Literal" || nextChild.type === "JSXText") && nextChild.raw.startsWith(" ") || (child.type === "Literal" || child.type === "JSXText") && child.raw.endsWith(" ") || context.sourceCode.isSpaceBetweenTokens(child, nextChild);
|
|
};
|
|
const source = context.sourceCode.getText(child);
|
|
const leadingSpace = !!(prevChild && spaceBetweenPrev());
|
|
const trailingSpace = !!(nextChild && spaceBetweenNext());
|
|
const leadingNewLine = !!prevChild;
|
|
const trailingNewLine = !!nextChild;
|
|
const key = nodeKey(child);
|
|
if (!fixDetailsByNode[key]) {
|
|
fixDetailsByNode[key] = {
|
|
node: child,
|
|
source,
|
|
descriptor: nodeDescriptor(child)
|
|
};
|
|
}
|
|
if (leadingSpace)
|
|
fixDetailsByNode[key].leadingSpace = true;
|
|
if (leadingNewLine)
|
|
fixDetailsByNode[key].leadingNewLine = true;
|
|
if (trailingNewLine)
|
|
fixDetailsByNode[key].trailingNewLine = true;
|
|
if (trailingSpace)
|
|
fixDetailsByNode[key].trailingSpace = true;
|
|
});
|
|
});
|
|
Object.keys(fixDetailsByNode).forEach((key) => {
|
|
const details = fixDetailsByNode[key];
|
|
const nodeToReport = details.node;
|
|
const descriptor = details.descriptor;
|
|
const source = details.source.replace(/(^ +| +$)/g, "");
|
|
const leadingSpaceString = details.leadingSpace ? "\n{' '}" : "";
|
|
const trailingSpaceString = details.trailingSpace ? "{' '}\n" : "";
|
|
const leadingNewLineString = details.leadingNewLine ? "\n" : "";
|
|
const trailingNewLineString = details.trailingNewLine ? "\n" : "";
|
|
const replaceText = `${leadingSpaceString}${leadingNewLineString}${source}${trailingNewLineString}${trailingSpaceString}`;
|
|
context.report({
|
|
messageId: "moveToNewLine",
|
|
node: nodeToReport,
|
|
data: {
|
|
descriptor
|
|
},
|
|
fix(fixer) {
|
|
return fixer.replaceText(nodeToReport, replaceText);
|
|
}
|
|
});
|
|
});
|
|
}
|
|
return {
|
|
JSXElement: handleJSX,
|
|
JSXFragment: handleJSX
|
|
};
|
|
}
|
|
});
|
|
|
|
module.exports = jsxOneExpressionPerLine;
|