- 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."
252 lines
11 KiB
JavaScript
252 lines
11 KiB
JavaScript
'use strict';
|
|
|
|
var utils = require('../utils.js');
|
|
require('eslint-visitor-keys');
|
|
require('espree');
|
|
require('estraverse');
|
|
|
|
const OPTION_ALWAYS = "always";
|
|
const OPTION_NEVER = "never";
|
|
const OPTION_IGNORE = "ignore";
|
|
const OPTION_VALUES = [
|
|
OPTION_ALWAYS,
|
|
OPTION_NEVER,
|
|
OPTION_IGNORE
|
|
];
|
|
const DEFAULT_CONFIG = { props: OPTION_NEVER, children: OPTION_NEVER, propElementValues: OPTION_IGNORE };
|
|
const messages = {
|
|
unnecessaryCurly: "Curly braces are unnecessary here.",
|
|
missingCurly: "Need to wrap this literal in a JSX expression."
|
|
};
|
|
var jsxCurlyBracePresence = utils.createRule({
|
|
name: "jsx-curly-brace-presence",
|
|
package: "jsx",
|
|
meta: {
|
|
type: "layout",
|
|
docs: {
|
|
description: "Disallow unnecessary JSX expressions when literals alone are sufficient or enforce JSX expressions on literals in JSX children or attributes"
|
|
},
|
|
fixable: "code",
|
|
messages,
|
|
schema: [
|
|
{
|
|
anyOf: [
|
|
{
|
|
type: "object",
|
|
properties: {
|
|
props: { type: "string", enum: OPTION_VALUES },
|
|
children: { type: "string", enum: OPTION_VALUES },
|
|
propElementValues: { type: "string", enum: OPTION_VALUES }
|
|
},
|
|
additionalProperties: false
|
|
},
|
|
{
|
|
type: "string",
|
|
enum: OPTION_VALUES
|
|
}
|
|
]
|
|
}
|
|
]
|
|
},
|
|
create(context) {
|
|
const HTML_ENTITY_REGEX = () => /&[A-Z\d#]+;/gi;
|
|
const ruleOptions = context.options[0];
|
|
const userConfig = typeof ruleOptions === "string" ? { props: ruleOptions, children: ruleOptions, propElementValues: OPTION_IGNORE } : Object.assign({}, DEFAULT_CONFIG, ruleOptions);
|
|
function containsLineTerminators(rawStringValue) {
|
|
return /[\n\r\u2028\u2029]/.test(rawStringValue);
|
|
}
|
|
function containsBackslash(rawStringValue) {
|
|
return rawStringValue.includes("\\");
|
|
}
|
|
function containsHTMLEntity(rawStringValue) {
|
|
return HTML_ENTITY_REGEX().test(rawStringValue);
|
|
}
|
|
function containsOnlyHtmlEntities(rawStringValue) {
|
|
return rawStringValue.replace(HTML_ENTITY_REGEX(), "").trim() === "";
|
|
}
|
|
function containsDisallowedJSXTextChars(rawStringValue) {
|
|
return /[{<>}]/.test(rawStringValue);
|
|
}
|
|
function containsQuoteCharacters(value) {
|
|
return /['"]/.test(value);
|
|
}
|
|
function containsMultilineComment(value) {
|
|
return /\/\*/.test(value);
|
|
}
|
|
function escapeDoubleQuotes(rawStringValue) {
|
|
return rawStringValue.replace(/\\"/g, '"').replace(/"/g, '\\"');
|
|
}
|
|
function escapeBackslashes(rawStringValue) {
|
|
return rawStringValue.replace(/\\/g, "\\\\");
|
|
}
|
|
function needToEscapeCharacterForJSX(raw, node) {
|
|
return containsBackslash(raw) || containsHTMLEntity(raw) || node.parent.type !== "JSXAttribute" && containsDisallowedJSXTextChars(raw);
|
|
}
|
|
function containsWhitespaceExpression(child) {
|
|
if (child.type === "JSXExpressionContainer") {
|
|
const value = child.expression.value;
|
|
return value ? utils.isWhiteSpaces(value) : false;
|
|
}
|
|
return false;
|
|
}
|
|
function isLineBreak(text) {
|
|
return containsLineTerminators(text) && text.trim() === "";
|
|
}
|
|
function wrapNonHTMLEntities(text) {
|
|
const HTML_ENTITY = "<HTML_ENTITY>";
|
|
const withCurlyBraces = text.split(HTML_ENTITY_REGEX()).map((word) => word === "" ? "" : `{${JSON.stringify(word)}}`).join(HTML_ENTITY);
|
|
const htmlEntities = text.match(HTML_ENTITY_REGEX());
|
|
return htmlEntities.reduce((acc, htmlEntity) => acc.replace(HTML_ENTITY, htmlEntity), withCurlyBraces);
|
|
}
|
|
function wrapWithCurlyBraces(rawText) {
|
|
if (!containsLineTerminators(rawText))
|
|
return `{${JSON.stringify(rawText)}}`;
|
|
return rawText.split("\n").map((line) => {
|
|
if (line.trim() === "")
|
|
return line;
|
|
const firstCharIndex = line.search(/\S/);
|
|
const leftWhitespace = line.slice(0, firstCharIndex);
|
|
const text = line.slice(firstCharIndex);
|
|
if (containsHTMLEntity(line))
|
|
return `${leftWhitespace}${wrapNonHTMLEntities(text)}`;
|
|
return `${leftWhitespace}{${JSON.stringify(text)}}`;
|
|
}).join("\n");
|
|
}
|
|
function reportUnnecessaryCurly(JSXExpressionNode) {
|
|
context.report({
|
|
messageId: "unnecessaryCurly",
|
|
node: JSXExpressionNode,
|
|
fix(fixer) {
|
|
const expression = JSXExpressionNode.expression;
|
|
let textToReplace;
|
|
if (utils.isJSX(expression)) {
|
|
const sourceCode = context.sourceCode;
|
|
textToReplace = sourceCode.getText(expression);
|
|
} else {
|
|
const parentType = JSXExpressionNode.parent.type;
|
|
if (parentType === "JSXAttribute") {
|
|
textToReplace = `"${expression.type === "TemplateLiteral" ? expression.quasis[0].value.raw : expression.raw.slice(1, -1)}"`;
|
|
} else if (utils.isJSX(expression)) {
|
|
const sourceCode = context.sourceCode;
|
|
textToReplace = sourceCode.getText(expression);
|
|
} else {
|
|
textToReplace = expression.type === "TemplateLiteral" ? expression.quasis[0].value.cooked : expression.value;
|
|
}
|
|
}
|
|
return fixer.replaceText(JSXExpressionNode, textToReplace);
|
|
}
|
|
});
|
|
}
|
|
function reportMissingCurly(literalNode) {
|
|
context.report({
|
|
messageId: "missingCurly",
|
|
node: literalNode,
|
|
fix(fixer) {
|
|
if (utils.isJSX(literalNode))
|
|
return fixer.replaceText(literalNode, `{${context.sourceCode.getText(literalNode)}}`);
|
|
if (containsOnlyHtmlEntities(literalNode.raw) || literalNode.parent.type === "JSXAttribute" && containsLineTerminators(literalNode.raw) || isLineBreak(literalNode.raw)) {
|
|
return null;
|
|
}
|
|
const expression = literalNode.parent.type === "JSXAttribute" ? `{"${escapeDoubleQuotes(escapeBackslashes(
|
|
literalNode.raw.slice(1, -1)
|
|
))}"}` : wrapWithCurlyBraces(literalNode.raw);
|
|
return fixer.replaceText(literalNode, expression);
|
|
}
|
|
});
|
|
}
|
|
function isWhiteSpaceLiteral(node) {
|
|
return node.type && node.type === "Literal" && node.value && utils.isWhiteSpaces(node.value);
|
|
}
|
|
function isStringWithTrailingWhiteSpaces(value) {
|
|
return /^\s|\s$/.test(value);
|
|
}
|
|
function isLiteralWithTrailingWhiteSpaces(node) {
|
|
return node.type && node.type === "Literal" && node.value && isStringWithTrailingWhiteSpaces(node.value);
|
|
}
|
|
function lintUnnecessaryCurly(JSXExpressionNode) {
|
|
const expression = JSXExpressionNode.expression;
|
|
const expressionType = expression.type;
|
|
const sourceCode = context.sourceCode;
|
|
if (sourceCode.getCommentsInside && sourceCode.getCommentsInside(JSXExpressionNode).length > 0)
|
|
return;
|
|
if ((expressionType === "Literal" || expressionType === "JSXText") && typeof expression.value === "string" && (JSXExpressionNode.parent.type === "JSXAttribute" && !isWhiteSpaceLiteral(expression) || !isLiteralWithTrailingWhiteSpaces(expression)) && !containsMultilineComment(expression.value) && !needToEscapeCharacterForJSX(expression.raw, JSXExpressionNode) && (utils.isJSX(JSXExpressionNode.parent) || !containsQuoteCharacters(expression.value))) {
|
|
reportUnnecessaryCurly(JSXExpressionNode);
|
|
} else if (expressionType === "TemplateLiteral" && expression.expressions.length === 0 && !expression.quasis[0].value.raw.includes("\n") && !isStringWithTrailingWhiteSpaces(expression.quasis[0].value.raw) && !needToEscapeCharacterForJSX(expression.quasis[0].value.raw, JSXExpressionNode) && !containsQuoteCharacters(expression.quasis[0].value.cooked)) {
|
|
reportUnnecessaryCurly(JSXExpressionNode);
|
|
} else if (utils.isJSX(expression)) {
|
|
reportUnnecessaryCurly(JSXExpressionNode);
|
|
}
|
|
}
|
|
function areRuleConditionsSatisfied(parent, config, ruleCondition) {
|
|
return parent.type === "JSXAttribute" && typeof config.props === "string" && config.props === ruleCondition || utils.isJSX(parent) && typeof config.children === "string" && config.children === ruleCondition;
|
|
}
|
|
function getAdjacentSiblings(node, children) {
|
|
for (let i = 1; i < children.length - 1; i++) {
|
|
const child = children[i];
|
|
if (node === child)
|
|
return [children[i - 1], children[i + 1]];
|
|
}
|
|
if (node === children[0] && children[1])
|
|
return [children[1]];
|
|
if (node === children[children.length - 1] && children[children.length - 2])
|
|
return [children[children.length - 2]];
|
|
return [];
|
|
}
|
|
function hasAdjacentJsxExpressionContainers(node, children) {
|
|
if (!children)
|
|
return false;
|
|
const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child));
|
|
const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral);
|
|
return adjSiblings.some((x) => x.type && x.type === "JSXExpressionContainer");
|
|
}
|
|
function hasAdjacentJsx(node, children) {
|
|
if (!children)
|
|
return false;
|
|
const childrenExcludingWhitespaceLiteral = children.filter((child) => !isWhiteSpaceLiteral(child));
|
|
const adjSiblings = getAdjacentSiblings(node, childrenExcludingWhitespaceLiteral);
|
|
return adjSiblings.some((x) => x.type && ["JSXExpressionContainer", "JSXElement"].includes(x.type));
|
|
}
|
|
function shouldCheckForUnnecessaryCurly(node, config) {
|
|
const parent = node.parent;
|
|
if (parent.type && parent.type === "JSXAttribute" && (node.expression && node.expression.type && node.expression.type !== "Literal" && node.expression.type !== "StringLiteral" && node.expression.type !== "TemplateLiteral")) {
|
|
return false;
|
|
}
|
|
if (utils.isJSX(parent) && hasAdjacentJsxExpressionContainers(node, parent.children))
|
|
return false;
|
|
if (containsWhitespaceExpression(node) && hasAdjacentJsx(node, parent.children))
|
|
return false;
|
|
if (parent.children && parent.children.length === 1 && containsWhitespaceExpression(node)) {
|
|
return false;
|
|
}
|
|
return areRuleConditionsSatisfied(parent, config, OPTION_NEVER);
|
|
}
|
|
function shouldCheckForMissingCurly(node, config) {
|
|
if (utils.isJSX(node))
|
|
return config.propElementValues !== OPTION_IGNORE;
|
|
if (isLineBreak(node.raw) || containsOnlyHtmlEntities(node.raw)) {
|
|
return false;
|
|
}
|
|
const parent = node.parent;
|
|
if (parent.children && parent.children.length === 1 && containsWhitespaceExpression(parent.children[0])) {
|
|
return false;
|
|
}
|
|
return areRuleConditionsSatisfied(parent, config, OPTION_ALWAYS);
|
|
}
|
|
return {
|
|
"JSXAttribute > JSXExpressionContainer > JSXElement": function(node) {
|
|
if (userConfig.propElementValues === OPTION_NEVER)
|
|
reportUnnecessaryCurly(node.parent);
|
|
},
|
|
JSXExpressionContainer(node) {
|
|
if (shouldCheckForUnnecessaryCurly(node, userConfig))
|
|
lintUnnecessaryCurly(node);
|
|
},
|
|
"JSXAttribute > JSXElement, Literal, JSXText": function(node) {
|
|
if (shouldCheckForMissingCurly(node, userConfig))
|
|
reportMissingCurly(node);
|
|
}
|
|
};
|
|
}
|
|
});
|
|
|
|
module.exports = jsxCurlyBracePresence;
|