312 lines
8.8 KiB
JavaScript
312 lines
8.8 KiB
JavaScript
"use strict";
|
|
|
|
const findLast = require("lodash/findLast");
|
|
|
|
/**
|
|
* Search where is the position of the comment in the token array by
|
|
* using dichotomic search.
|
|
* @param {*} tokens ordered array of tokens
|
|
* @param {*} comment comment token
|
|
* @return the position of the token next to the comment
|
|
*/
|
|
function findUpperBoundToken(tokens, comment) {
|
|
let diff;
|
|
let i;
|
|
let current;
|
|
|
|
let len = tokens.length;
|
|
i = 0;
|
|
|
|
while (len) {
|
|
diff = len >>> 1;
|
|
current = i + diff;
|
|
if (tokens[current].startOffset > comment.startOffset) {
|
|
len = diff;
|
|
} else {
|
|
i = current + 1;
|
|
len -= diff + 1;
|
|
}
|
|
}
|
|
return i;
|
|
}
|
|
|
|
function isPrettierIgnoreComment(comment) {
|
|
return comment.image.match(
|
|
/(\/\/(\s*)prettier-ignore(\s*))|(\/\*(\s*)prettier-ignore(\s*)\*\/)/gm
|
|
);
|
|
}
|
|
|
|
function isFormatterOffOnComment(comment) {
|
|
return comment.image.match(
|
|
/(\/\/(\s*)@formatter:(off|on)(\s*))|(\/\*(\s*)@formatter:(off|on)(\s*)\*\/)/gm
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pre-processing of tokens in order to
|
|
* complete the parser's mostEnclosiveCstNodeByStartOffset and mostEnclosiveCstNodeByEndOffset structures.
|
|
*
|
|
* @param {ITokens[]} tokens - array of tokens
|
|
* @param {{[startOffset: number]: CSTNode}} mostEnclosiveCstNodeByStartOffset
|
|
* @param {{[endOffset: number]: CSTNode}} mostEnclosiveCstNodeByEndOffset
|
|
*/
|
|
function completeMostEnclosiveCSTNodeByOffset(
|
|
tokens,
|
|
mostEnclosiveCstNodeByStartOffset,
|
|
mostEnclosiveCstNodeByEndOffset
|
|
) {
|
|
tokens.forEach(token => {
|
|
if (mostEnclosiveCstNodeByStartOffset[token.startOffset] === undefined) {
|
|
mostEnclosiveCstNodeByStartOffset[token.startOffset] = token;
|
|
}
|
|
|
|
if (mostEnclosiveCstNodeByEndOffset[token.endOffset] === undefined) {
|
|
mostEnclosiveCstNodeByEndOffset[token.endOffset] = token;
|
|
}
|
|
});
|
|
}
|
|
|
|
function extendRangeOffset(comments, tokens) {
|
|
let position;
|
|
comments.forEach(comment => {
|
|
position = findUpperBoundToken(tokens, comment);
|
|
|
|
const extendedStartOffset =
|
|
position - 1 < 0 ? comment.startOffset : tokens[position - 1].endOffset;
|
|
const extendedEndOffset =
|
|
position == tokens.length
|
|
? comment.endOffset
|
|
: tokens[position].startOffset;
|
|
comment.extendedOffset = {
|
|
startOffset: extendedStartOffset,
|
|
endOffset: extendedEndOffset
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create two data structures we use to know at which offset a comment can be attached.
|
|
* - commentsByExtendedStartOffset: map a comment by the endOffset of the previous token.
|
|
* - commentsByExtendedEndOffset: map a comment by the startOffset of the next token
|
|
*
|
|
* @param {ITokens[]} tokens - array of tokens
|
|
*
|
|
* @return {{commentsByExtendedStartOffset: {[extendedStartOffset: number]: Comment[]}, commentsByExtendedEndOffset: {[extendedEndOffset: number]: Comment[]}}}
|
|
*/
|
|
function mapCommentsByExtendedRange(comments) {
|
|
const commentsByExtendedEndOffset = {};
|
|
const commentsByExtendedStartOffset = {};
|
|
|
|
comments.forEach(comment => {
|
|
const extendedStartOffset = comment.extendedOffset.startOffset;
|
|
const extendedEndOffset = comment.extendedOffset.endOffset;
|
|
|
|
if (commentsByExtendedEndOffset[extendedEndOffset] === undefined) {
|
|
commentsByExtendedEndOffset[extendedEndOffset] = [comment];
|
|
} else {
|
|
commentsByExtendedEndOffset[extendedEndOffset].push(comment);
|
|
}
|
|
|
|
if (commentsByExtendedStartOffset[extendedStartOffset] === undefined) {
|
|
commentsByExtendedStartOffset[extendedStartOffset] = [comment];
|
|
} else {
|
|
commentsByExtendedStartOffset[extendedStartOffset].push(comment);
|
|
}
|
|
});
|
|
|
|
return { commentsByExtendedEndOffset, commentsByExtendedStartOffset };
|
|
}
|
|
|
|
/**
|
|
* Determine if a comment should be attached as a trailing comment to a specific node.
|
|
* A comment should be trailing if it is on the same line than the previous token and
|
|
* not on the same line than the next token
|
|
*
|
|
* @param {*} comment
|
|
* @param {CSTNode} node
|
|
* @param {{[startOffset: number]: CSTNode}} mostEnclosiveCstNodeByStartOffset
|
|
*/
|
|
function shouldAttachTrailingComments(
|
|
comment,
|
|
node,
|
|
mostEnclosiveCstNodeByStartOffset
|
|
) {
|
|
if (isPrettierIgnoreComment(comment)) {
|
|
return false;
|
|
}
|
|
|
|
const nextNode =
|
|
mostEnclosiveCstNodeByStartOffset[comment.extendedOffset.endOffset];
|
|
|
|
// Last node of the file
|
|
if (nextNode === undefined) {
|
|
return true;
|
|
}
|
|
|
|
const nodeEndLine =
|
|
node.location !== undefined ? node.location.endLine : node.endLine;
|
|
|
|
if (comment.startLine !== nodeEndLine) {
|
|
return false;
|
|
}
|
|
|
|
const nextNodeStartLine =
|
|
nextNode.location !== undefined
|
|
? nextNode.location.startLine
|
|
: nextNode.startLine;
|
|
return comment.endLine !== nextNodeStartLine;
|
|
}
|
|
|
|
/**
|
|
* Attach comments to the most enclosive CSTNode (node or token)
|
|
*
|
|
* @param {ITokens[]} tokens
|
|
* @param {*} comments
|
|
* @param {{[startOffset: number]: CSTNode}} mostEnclosiveCstNodeByStartOffset
|
|
* @param {{[endOffset: number]: CSTNode}} mostEnclosiveCstNodeByEndOffset
|
|
*/
|
|
function attachComments(
|
|
tokens,
|
|
comments,
|
|
mostEnclosiveCstNodeByStartOffset,
|
|
mostEnclosiveCstNodeByEndOffset
|
|
) {
|
|
// Edge case: only comments in the file
|
|
if (tokens.length === 0) {
|
|
mostEnclosiveCstNodeByStartOffset[NaN].leadingComments = comments;
|
|
return;
|
|
}
|
|
|
|
// Pre-processing phase to complete the data structures we need to attach
|
|
// a comment to the right place
|
|
completeMostEnclosiveCSTNodeByOffset(
|
|
tokens,
|
|
mostEnclosiveCstNodeByStartOffset,
|
|
mostEnclosiveCstNodeByEndOffset
|
|
);
|
|
|
|
extendRangeOffset(comments, tokens);
|
|
const { commentsByExtendedStartOffset, commentsByExtendedEndOffset } =
|
|
mapCommentsByExtendedRange(comments);
|
|
|
|
/*
|
|
This set is here to ensure that we attach comments only once
|
|
If a comment is attached to a node or token, we remove it from this set
|
|
*/
|
|
const commentsToAttach = new Set(comments);
|
|
|
|
// Attach comments as trailing comments if desirable
|
|
Object.keys(mostEnclosiveCstNodeByEndOffset).forEach(endOffset => {
|
|
// We look if some comments is directly following this node/token
|
|
if (commentsByExtendedStartOffset[endOffset] !== undefined) {
|
|
const nodeTrailingComments = commentsByExtendedStartOffset[
|
|
endOffset
|
|
].filter(comment => {
|
|
return (
|
|
shouldAttachTrailingComments(
|
|
comment,
|
|
mostEnclosiveCstNodeByEndOffset[endOffset],
|
|
mostEnclosiveCstNodeByStartOffset
|
|
) && commentsToAttach.has(comment)
|
|
);
|
|
});
|
|
|
|
if (nodeTrailingComments.length > 0) {
|
|
mostEnclosiveCstNodeByEndOffset[endOffset].trailingComments =
|
|
nodeTrailingComments;
|
|
}
|
|
|
|
nodeTrailingComments.forEach(comment => {
|
|
commentsToAttach.delete(comment);
|
|
});
|
|
}
|
|
});
|
|
|
|
// Attach rest of comments as leading comments
|
|
Object.keys(mostEnclosiveCstNodeByStartOffset).forEach(startOffset => {
|
|
// We look if some comments is directly preceding this node/token
|
|
if (commentsByExtendedEndOffset[startOffset] !== undefined) {
|
|
const nodeLeadingComments = commentsByExtendedEndOffset[
|
|
startOffset
|
|
].filter(comment => commentsToAttach.has(comment));
|
|
|
|
if (nodeLeadingComments.length > 0) {
|
|
mostEnclosiveCstNodeByStartOffset[startOffset].leadingComments =
|
|
nodeLeadingComments;
|
|
}
|
|
|
|
// prettier ignore support
|
|
for (let i = 0; i < nodeLeadingComments.length; i++) {
|
|
if (isPrettierIgnoreComment(nodeLeadingComments[i])) {
|
|
mostEnclosiveCstNodeByStartOffset[startOffset].ignore = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Create pairs of formatter:off and formatter:on
|
|
* @param comments
|
|
* @returns pairs of formatter:off and formatter:on
|
|
*/
|
|
function matchFormatterOffOnPairs(comments) {
|
|
const onOffComments = comments.filter(comment =>
|
|
isFormatterOffOnComment(comment)
|
|
);
|
|
|
|
let isPreviousCommentOff = false;
|
|
let isCurrentCommentOff = true;
|
|
const pairs = [];
|
|
let paired = {};
|
|
onOffComments.forEach(comment => {
|
|
isCurrentCommentOff = comment.image.slice(-3) === "off";
|
|
|
|
if (!isPreviousCommentOff) {
|
|
if (isCurrentCommentOff) {
|
|
paired.off = comment;
|
|
}
|
|
} else {
|
|
if (!isCurrentCommentOff) {
|
|
paired.on = comment;
|
|
pairs.push(paired);
|
|
paired = {};
|
|
}
|
|
}
|
|
isPreviousCommentOff = isCurrentCommentOff;
|
|
});
|
|
|
|
if (onOffComments.length > 0 && isCurrentCommentOff) {
|
|
paired.on = undefined;
|
|
pairs.push(paired);
|
|
}
|
|
|
|
return pairs;
|
|
}
|
|
|
|
/**
|
|
* Check if the node is between formatter:off and formatter:on and change his ignore state
|
|
* @param node
|
|
* @param commentPairs
|
|
*/
|
|
function shouldNotFormat(node, commentPairs) {
|
|
const matchingPair = findLast(
|
|
commentPairs,
|
|
comment => comment.off.endOffset < node.location.startOffset
|
|
);
|
|
if (
|
|
matchingPair !== undefined &&
|
|
(matchingPair.on === undefined ||
|
|
matchingPair.on.startOffset > node.location.endOffset)
|
|
) {
|
|
node.ignore = true;
|
|
}
|
|
}
|
|
|
|
module.exports = {
|
|
matchFormatterOffOnPairs,
|
|
shouldNotFormat,
|
|
attachComments
|
|
};
|