1/* 2 * Copyright (c) 2023-2025 Huawei Device Co., Ltd. 3 * Licensed under the Apache License, Version 2.0 (the "License"); 4 * you may not use this file except in compliance with the License. 5 * You may obtain a copy of the License at 6 * 7 * http://www.apache.org/licenses/LICENSE-2.0 8 * 9 * Unless required by applicable law or agreed to in writing, software 10 * distributed under the License is distributed on an "AS IS" BASIS, 11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 * See the License for the specific language governing permissions and 13 * limitations under the License. 14 */ 15 16import * as fs from 'node:fs'; 17import * as path from 'node:path'; 18import type * as ts from 'typescript'; 19import { Logger } from '../Logger'; 20import type { ProblemInfo } from '../ProblemInfo'; 21import type { Autofix } from './Autofixer'; 22import type { LinterOptions } from '../LinterOptions'; 23import { USE_STATIC } from '../utils/consts/InteropAPI'; 24import { AUTOFIX_HTML_TEMPLATE_TEXT, AutofixHtmlTemplate } from './AutofixReportHtmlHelper'; 25 26const BACKUP_AFFIX = '~'; 27export const DEFAULT_MAX_AUTOFIX_PASSES = 10; 28 29export class QuasiEditor { 30 constructor( 31 readonly srcFileName: string, 32 readonly sourceText: string, 33 readonly linterOpts: LinterOptions, 34 readonly cancellationToken?: ts.CancellationToken, 35 readonly reportPath?: string 36 ) {} 37 38 private static getBackupFileName(filePath: string): string { 39 return filePath + BACKUP_AFFIX; 40 } 41 42 static backupSrcFile(filePath: string): void { 43 fs.copyFileSync(filePath, QuasiEditor.getBackupFileName(filePath)); 44 } 45 46 private generateReport(acceptedPatches: Autofix[]): void { 47 const report = { 48 filePath: this.srcFileName, 49 fixCount: acceptedPatches.length, 50 fixes: acceptedPatches.map((fix) => { 51 return { 52 line: fix.line, 53 colum: fix.column, 54 endLine: fix.endLine, 55 endColum: fix.endColumn, 56 start: fix.start, 57 end: fix.end, 58 replacement: fix.replacementText, 59 original: this.sourceText.slice(fix.start, fix.end) 60 }; 61 }) 62 }; 63 64 let reportFilePath = './autofix-report.html'; 65 if (this.reportPath !== undefined) { 66 reportFilePath = path.join(path.normalize(this.reportPath), 'autofix-report.html'); 67 } 68 const getOldJsonArray = (reportFilePath: string): Set<any> => { 69 try { 70 const RegexCaptureBraketFirst = 1; 71 const rawData = fs.readFileSync(reportFilePath, 'utf-8'); 72 const rawContent = rawData.match(/`([\s\S]*?)`/)?.[RegexCaptureBraketFirst] ?? ''; 73 return new Set(JSON.parse(rawContent) || []); 74 } catch { 75 return new Set(); 76 } 77 }; 78 79 try { 80 const existingReports = getOldJsonArray(reportFilePath); 81 existingReports.add(report); 82 const str = JSON.stringify([...existingReports], null, 2); 83 const HtmlContent = AutofixHtmlTemplate.replace(AUTOFIX_HTML_TEMPLATE_TEXT, str); 84 if (!fs.existsSync(path.dirname(reportFilePath))) { 85 fs.mkdirSync(path.dirname(reportFilePath), { recursive: true }); 86 } 87 fs.writeFileSync(reportFilePath, HtmlContent, { encoding: 'utf-8' }); 88 } catch (error) { 89 Logger.error(`Failed to update autofix report: ${(error as Error).message}`); 90 } 91 } 92 93 fix(problemInfos: ProblemInfo[], needAddUseStatic: boolean | undefined): string { 94 const acceptedPatches = QuasiEditor.sortAndRemoveIntersections(problemInfos); 95 let result = this.applyFixes(acceptedPatches); 96 97 if (this.linterOpts.migrationReport) { 98 this.generateReport(acceptedPatches); 99 } 100 if (needAddUseStatic) { 101 result = QuasiEditor.addUseStaticDirective(result); 102 } 103 return result; 104 } 105 106 private applyFixes(autofixes: Autofix[]): string { 107 let output: string = ''; 108 let lastFixEnd = Number.NEGATIVE_INFINITY; 109 110 const doFix = (fix: Autofix): void => { 111 const { replacementText, start, end } = fix; 112 113 if (lastFixEnd > start || start > end) { 114 Logger.error(`Failed to apply autofix in range [${start}, ${end}] at ${this.srcFileName}`); 115 return; 116 } 117 118 output += this.sourceText.slice(Math.max(0, lastFixEnd), Math.max(0, start)); 119 output += replacementText; 120 lastFixEnd = end; 121 }; 122 123 autofixes.forEach(doFix); 124 output += this.sourceText.slice(Math.max(0, lastFixEnd)); 125 126 return output; 127 } 128 129 private static sortAndRemoveIntersections(problemInfos: ProblemInfo[]): Autofix[] { 130 let acceptedPatches: Autofix[] = []; 131 132 problemInfos.forEach((problemInfo): void => { 133 if (!problemInfo.autofix) { 134 return; 135 } 136 137 const consideredAutofix = QuasiEditor.sortAutofixes(problemInfo.autofix); 138 if (QuasiEditor.intersect(consideredAutofix, acceptedPatches)) { 139 return; 140 } 141 142 acceptedPatches.push(...consideredAutofix); 143 acceptedPatches = QuasiEditor.sortAutofixes(acceptedPatches); 144 }); 145 146 return acceptedPatches; 147 } 148 149 private static sortAutofixes(autofixes: Autofix[]): Autofix[] { 150 return autofixes.sort((a, b): number => { 151 return a.start - b.start; 152 }); 153 } 154 155 /** 156 * Determine if considered autofix can be accepted. 157 * 158 * @param consideredAutofix sorted patches of the considered autofix 159 * @param acceptedFixes sorted list of accepted autofixes 160 * @returns 161 */ 162 private static intersect(consideredAutofix: readonly Autofix[], acceptedFixes: readonly Autofix[]): boolean { 163 const start = consideredAutofix[0].start; 164 const end = consideredAutofix[consideredAutofix.length - 1].end; 165 166 for (const acceptedFix of acceptedFixes) { 167 if (acceptedFix.start > end) { 168 break; 169 } 170 171 if (acceptedFix.end < start) { 172 continue; 173 } 174 175 for (const consideredFix of consideredAutofix) { 176 if (QuasiEditor.autofixesIntersect(acceptedFix, consideredFix)) { 177 return true; 178 } 179 } 180 } 181 return false; 182 } 183 184 private static autofixesIntersect(lhs: Autofix, rhs: Autofix): boolean { 185 186 /* 187 * Ranges don't intersect if either 188 * [--] (lhs) 189 * [--] (rhs) 190 * or 191 * [--] (lhs) 192 * [--] (rhs) 193 */ 194 return !(lhs.end < rhs.start || rhs.end < lhs.start); 195 } 196 197 private static addUseStaticDirective(content: string): string { 198 const lines = content.split('\n'); 199 if (lines.length > 0 && lines[0].trim() === USE_STATIC) { 200 return content; 201 } 202 return USE_STATIC + '\n' + content; 203 } 204 205 static hasAnyAutofixes(problemInfos: ProblemInfo[]): boolean { 206 return problemInfos.some((problemInfo) => { 207 return problemInfo.autofix !== undefined; 208 }); 209 } 210} 211