• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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