rawsql-ts
Version:
High-performance SQL parser and AST analyzer written in TypeScript. Provides fast parsing and advanced transformation capabilities.
341 lines • 16.2 kB
JavaScript
import { SqlComponent } from '../models/SqlComponent';
/**
* Utility class for editing comments on SQL components.
* Provides functions to add, edit, delete, and search comments in SQL AST.
*/
export class CommentEditor {
/**
* Add a comment to a SQL component using positioned comments system
* For SelectQuery components, adds to headerComments for query-level comments
* For other components, adds as 'before' positioned comment
* @param component The SQL component to add comment to
* @param comment The comment text to add
* @param position Optional position for comment ('before' | 'after'), defaults to 'before'
*/
static addComment(component, comment, position = 'before') {
// Check if this is a SelectQuery component - add to headerComments for query-level comments
if (this.isSelectQuery(component)) {
const selectQuery = component;
if (!selectQuery.headerComments) {
selectQuery.headerComments = [];
}
selectQuery.headerComments.push(comment);
}
else {
// For other components, add to positioned comments
component.addPositionedComments(position, [comment]);
}
}
/**
* Check if a component implements SelectQuery interface
* Uses discriminator property for robust type detection
* @param component The component to check
* @returns true if the component is a SelectQuery
*/
static isSelectQuery(component) {
// Use discriminator property for robust type detection
return '__selectQueryType' in component &&
component.__selectQueryType === 'SelectQuery';
}
/**
* Edit an existing comment by index
* For SelectQuery components, edits headerComments
* For other components, edits positioned comments
* @param component The SQL component containing the comment
* @param index The index of the comment to edit (0-based)
* @param newComment The new comment text
* @param position Position to edit ('before' | 'after'), defaults to 'before' for non-SelectQuery components
* @throws Error if index is invalid
*/
static editComment(component, index, newComment, position = 'before') {
var _a, _b;
if (this.isSelectQuery(component)) {
const selectQuery = component;
if (!selectQuery.headerComments || index < 0 || index >= selectQuery.headerComments.length) {
throw new Error(`Invalid comment index: ${index}. Component has ${((_a = selectQuery.headerComments) === null || _a === void 0 ? void 0 : _a.length) || 0} comments.`);
}
selectQuery.headerComments[index] = newComment;
}
else {
const positionedComments = component.getPositionedComments(position);
if (!positionedComments || index < 0 || index >= positionedComments.length) {
throw new Error(`Invalid comment index: ${index}. Component has ${(positionedComments === null || positionedComments === void 0 ? void 0 : positionedComments.length) || 0} ${position} positioned comments.`);
}
// Update the comment in the positioned comments
const positioned = (_b = component.positionedComments) === null || _b === void 0 ? void 0 : _b.find(pc => pc.position === position);
if (positioned) {
positioned.comments[index] = newComment;
}
}
}
/**
* Delete a comment by index
* For SelectQuery components, deletes from headerComments
* For other components, deletes from positioned comments
* @param component The SQL component containing the comment
* @param index The index of the comment to delete (0-based)
* @param position Position to delete from ('before' | 'after'), defaults to 'before' for non-SelectQuery components
* @throws Error if index is invalid
*/
static deleteComment(component, index, position = 'before') {
var _a, _b, _c, _d;
if (this.isSelectQuery(component)) {
const selectQuery = component;
if (!selectQuery.headerComments || index < 0 || index >= selectQuery.headerComments.length) {
throw new Error(`Invalid comment index: ${index}. Component has ${((_a = selectQuery.headerComments) === null || _a === void 0 ? void 0 : _a.length) || 0} comments.`);
}
selectQuery.headerComments.splice(index, 1);
if (selectQuery.headerComments.length === 0) {
selectQuery.headerComments = null;
}
}
else {
const positionedComments = component.getPositionedComments(position);
if (!positionedComments || index < 0 || index >= positionedComments.length) {
throw new Error(`Invalid comment index: ${index}. Component has ${(positionedComments === null || positionedComments === void 0 ? void 0 : positionedComments.length) || 0} ${position} positioned comments.`);
}
// Delete the comment from positioned comments
const positioned = (_b = component.positionedComments) === null || _b === void 0 ? void 0 : _b.find(pc => pc.position === position);
if (positioned) {
positioned.comments.splice(index, 1);
// Clean up empty positioned comment groups
if (positioned.comments.length === 0) {
component.positionedComments = ((_c = component.positionedComments) === null || _c === void 0 ? void 0 : _c.filter(pc => pc.position !== position)) || null;
if (((_d = component.positionedComments) === null || _d === void 0 ? void 0 : _d.length) === 0) {
component.positionedComments = null;
}
}
}
}
}
/**
* Delete all comments from a component
* @param component The SQL component to clear comments from
*/
static deleteAllComments(component) {
if (this.isSelectQuery(component)) {
const selectQuery = component;
selectQuery.headerComments = null;
}
else {
component.positionedComments = null;
}
}
/**
* Get all comments from a component
* For SelectQuery components, returns headerComments
* For other components, returns all positioned comments as a flat array
* @param component The SQL component to get comments from
* @returns Array of comment strings (empty array if no comments)
*/
static getComments(component) {
if (this.isSelectQuery(component)) {
const selectQuery = component;
return selectQuery.headerComments || [];
}
return component.getAllPositionedComments();
}
/**
* Find all components in the AST that have comments containing the search text
* @param root The root SQL component to search from
* @param searchText The text to search for in comments
* @param caseSensitive Whether the search should be case-sensitive (default: false)
* @returns Array of components that have matching comments
*/
static findComponentsWithComment(root, searchText, caseSensitive = false) {
const results = [];
const searchTerm = caseSensitive ? searchText : searchText.toLowerCase();
const traverse = (component) => {
if (component && component instanceof SqlComponent) {
let hasMatchingComment = false;
// Check positioned comments
const allPositionedComments = component.getAllPositionedComments();
if (allPositionedComments && allPositionedComments.some(c => {
const commentText = caseSensitive ? c : c.toLowerCase();
return commentText.includes(searchTerm);
})) {
hasMatchingComment = true;
}
// Check headerComments for SelectQuery components
if (this.isSelectQuery(component)) {
const selectQuery = component;
if (selectQuery.headerComments && selectQuery.headerComments.some(c => {
const commentText = caseSensitive ? c : c.toLowerCase();
return commentText.includes(searchTerm);
})) {
hasMatchingComment = true;
}
}
if (hasMatchingComment) {
results.push(component);
}
}
// Traverse all properties recursively
for (const key in component) {
if (component[key] && typeof component[key] === 'object') {
if (Array.isArray(component[key])) {
component[key].forEach(traverse);
}
else {
traverse(component[key]);
}
}
}
};
traverse(root);
return results;
}
/**
* Replace all occurrences of a text in comments across the entire AST
* @param root The root SQL component to search and replace in
* @param searchText The text to search for
* @param replaceText The text to replace with
* @param caseSensitive Whether the search should be case-sensitive (default: false)
* @returns Number of replacements made
*/
static replaceInComments(root, searchText, replaceText, caseSensitive = false) {
let replacementCount = 0;
const traverse = (component) => {
if (component && component instanceof SqlComponent) {
// Handle regular comments
if (component.comments) {
for (let i = 0; i < component.comments.length; i++) {
const originalComment = component.comments[i];
const flags = caseSensitive ? 'g' : 'gi';
const regex = new RegExp(searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
const newComment = originalComment.replace(regex, replaceText);
if (newComment !== originalComment) {
component.comments[i] = newComment;
replacementCount++;
}
}
}
// Handle positioned comments
if (component.positionedComments) {
for (const posComment of component.positionedComments) {
if (posComment.comments) {
for (let i = 0; i < posComment.comments.length; i++) {
const originalComment = posComment.comments[i];
const flags = caseSensitive ? 'g' : 'gi';
const regex = new RegExp(searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
const newComment = originalComment.replace(regex, replaceText);
if (newComment !== originalComment) {
posComment.comments[i] = newComment;
replacementCount++;
}
}
}
}
}
// Handle headerComments for SelectQuery components
if (this.isSelectQuery(component)) {
const selectQuery = component;
if (selectQuery.headerComments) {
for (let i = 0; i < selectQuery.headerComments.length; i++) {
const originalComment = selectQuery.headerComments[i];
const flags = caseSensitive ? 'g' : 'gi';
const regex = new RegExp(searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), flags);
const newComment = originalComment.replace(regex, replaceText);
if (newComment !== originalComment) {
selectQuery.headerComments[i] = newComment;
replacementCount++;
}
}
}
}
}
// Traverse all properties recursively
for (const key in component) {
if (component[key] && typeof component[key] === 'object') {
if (Array.isArray(component[key])) {
component[key].forEach(traverse);
}
else {
traverse(component[key]);
}
}
}
};
traverse(root);
return replacementCount;
}
/**
* Count total number of comments in the AST
* @param root The root SQL component to count comments in
* @returns Total number of comments
*/
static countComments(root) {
let count = 0;
const traverse = (component) => {
if (component && component instanceof SqlComponent) {
// Count positioned comments
const positionedComments = component.getAllPositionedComments();
if (positionedComments) {
count += positionedComments.length;
}
// Count headerComments for SelectQuery components
if (this.isSelectQuery(component)) {
const selectQuery = component;
if (selectQuery.headerComments) {
count += selectQuery.headerComments.length;
}
}
}
// Traverse all properties recursively
for (const key in component) {
if (component[key] && typeof component[key] === 'object') {
if (Array.isArray(component[key])) {
component[key].forEach(traverse);
}
else {
traverse(component[key]);
}
}
}
};
traverse(root);
return count;
}
/**
* Get all comments from the entire AST as a flat array with their source components
* @param root The root SQL component to extract comments from
* @returns Array of objects containing comment text and the component they belong to
*/
static getAllComments(root) {
const results = [];
const traverse = (component) => {
if (component && component instanceof SqlComponent) {
// Add positioned comments
const positionedComments = component.getAllPositionedComments();
if (positionedComments) {
positionedComments.forEach((comment, index) => {
results.push({ comment, component, index });
});
}
// Add headerComments for SelectQuery components
if (this.isSelectQuery(component)) {
const selectQuery = component;
if (selectQuery.headerComments) {
selectQuery.headerComments.forEach((comment, index) => {
results.push({ comment, component, index });
});
}
}
}
// Traverse all properties recursively
for (const key in component) {
if (component[key] && typeof component[key] === 'object') {
if (Array.isArray(component[key])) {
component[key].forEach(traverse);
}
else {
traverse(component[key]);
}
}
}
};
traverse(root);
return results;
}
}
//# sourceMappingURL=CommentEditor.js.map