@rehearsal/plugins
Advanced tools
Comparing version 2.0.2-beta to 2.1.0
@@ -1,5 +0,6 @@ | ||
import { type SourceFile } from 'typescript'; | ||
import { type FormatCodeSettings, type SourceFile } from 'typescript'; | ||
import { Location } from '@rehearsal/reporter'; | ||
export declare function getLocation(sourceFile: SourceFile, start: number, length: number): Location; | ||
export declare function setProcessTTYto(setting: boolean): void; | ||
export declare function getFormatCodeSettingsForFile(filePath: string): Promise<FormatCodeSettings>; | ||
//# sourceMappingURL=helpers.d.ts.map |
@@ -0,1 +1,9 @@ | ||
import ts from 'typescript'; | ||
import { dirname, resolve } from 'node:path'; | ||
import { fileURLToPath } from 'node:url'; | ||
import Module from 'node:module'; | ||
const __filename = fileURLToPath(import.meta.url); | ||
const __dirname = dirname(__filename); | ||
const require = Module.createRequire(import.meta.url); | ||
const { SemicolonPreference, getDefaultFormatCodeSettings } = ts; | ||
const INDEX_BUMP = 1; //bump line and column numbers from 0 to 1 for sarif reader | ||
@@ -17,2 +25,45 @@ export function getLocation(sourceFile, start, length) { | ||
} | ||
export async function getFormatCodeSettingsForFile(filePath) { | ||
let prettierConfig = null; | ||
try { | ||
const prettier = importPrettier(filePath); | ||
prettierConfig = await prettier.resolveConfig(filePath, { | ||
editorconfig: true, | ||
}); | ||
} | ||
catch (e) { | ||
// swallow the error. Prettier is not installed | ||
} | ||
const tsFormatCodeOptions = getDefaultFormatCodeSettings(); | ||
let useSemicolons = true; | ||
let indentSize = tsFormatCodeOptions.tabSize ?? 2; | ||
let convertTabsToSpaces = true; | ||
if (prettierConfig) { | ||
useSemicolons = prettierConfig.semi !== false; | ||
indentSize = prettierConfig.tabWidth ?? indentSize; | ||
convertTabsToSpaces = prettierConfig.useTabs !== true; | ||
} | ||
return { | ||
...tsFormatCodeOptions, | ||
baseIndentSize: indentSize, | ||
convertTabsToSpaces, | ||
indentSize, | ||
semicolons: useSemicolons ? SemicolonPreference.Insert : SemicolonPreference.Remove, | ||
}; | ||
} | ||
function importPrettier(fromPath) { | ||
const pkg = getPackageInfo('prettier', fromPath); | ||
const main = resolve(pkg.path); | ||
return require(main); | ||
} | ||
function getPackageInfo(packageName, fromPath) { | ||
const paths = [__dirname]; | ||
paths.unshift(fromPath); | ||
const packageJSONPath = require.resolve(`${packageName}/package.json`, { | ||
paths, | ||
}); | ||
return { | ||
path: dirname(packageJSONPath), | ||
}; | ||
} | ||
//# sourceMappingURL=helpers.js.map |
@@ -1,2 +0,2 @@ | ||
import ts from 'typescript'; | ||
import ts, { type FormatCodeSettings } from 'typescript'; | ||
import { type DiagnosticWithContext } from '@rehearsal/codefixes'; | ||
@@ -8,2 +8,3 @@ import { PluginOptions, PluginsRunnerContext, Service, Plugin } from '@rehearsal/service'; | ||
strictTyping?: boolean; | ||
mode: 'single-pass' | 'drain'; | ||
} | ||
@@ -21,7 +22,9 @@ /** | ||
run(): Promise<string[]>; | ||
applyFixes(context: PluginsRunnerContext, fileName: string, diagnosticCategory: ts.DiagnosticCategory, options: DiagnosticFixPluginOptions): Promise<void>; | ||
private singlePassMode; | ||
private drainMode; | ||
applyFixes(context: PluginsRunnerContext, fileName: string, diagnosticCategory: ts.DiagnosticCategory, options: DiagnosticFixPluginOptions, formatCodeSettings: FormatCodeSettings): Promise<void>; | ||
/** | ||
* Returns the list of diagnostics with location and additional context of the application | ||
*/ | ||
getDiagnostics(service: Service, fileName: string, diagnosticFilterCategory: ts.DiagnosticCategory): DiagnosticWithContext[]; | ||
getDiagnostics(service: Service, fileName: string, diagnosticFilterCategories: ts.DiagnosticCategory[]): DiagnosticWithContext[]; | ||
private applyCommandAction; | ||
@@ -31,5 +34,5 @@ /** | ||
*/ | ||
getCodeFix(diagnostic: DiagnosticWithContext, options: DiagnosticFixPluginOptions): ts.CodeFixAction | undefined; | ||
getCodeFix(diagnostic: DiagnosticWithContext, options: DiagnosticFixPluginOptions, formatCodeSettings: FormatCodeSettings): ts.CodeFixAction | undefined; | ||
private wasAttemptedToFix; | ||
} | ||
//# sourceMappingURL=diagnostic-fix.plugin.d.ts.map |
@@ -5,5 +5,6 @@ import { createRequire } from 'node:module'; | ||
import ts from 'typescript'; | ||
import { codefixes, getDiagnosticOrder, isInstallPackageCommand, } from '@rehearsal/codefixes'; | ||
import { codefixes, getDiagnosticOrder, isInstallPackageCommand, applyCodeFix, } from '@rehearsal/codefixes'; | ||
import { Plugin } from '@rehearsal/service'; | ||
import { findNodeAtPosition } from '@rehearsal/ts-utils'; | ||
import { getFormatCodeSettingsForFile } from '../helpers.js'; | ||
const require = createRequire(import.meta.url); | ||
@@ -13,2 +14,3 @@ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const DEBUG_CALLBACK = debug('rehearsal:plugins:diagnostic-fix'); | ||
const { DiagnosticCategory } = ts; | ||
/** | ||
@@ -26,21 +28,71 @@ * Diagnose issues in the file and applied transforms to fix them | ||
async run() { | ||
const { options } = this; | ||
const { mode } = options; | ||
switch (mode) { | ||
case 'drain': | ||
await this.drainMode(); | ||
break; | ||
case 'single-pass': | ||
default: | ||
await this.singlePassMode(); | ||
} | ||
return Array.from(this.allFixedFiles); | ||
} | ||
async singlePassMode() { | ||
const { fileName, context, options } = this; | ||
DEBUG_CALLBACK(`Plugin 'DiagnosticFix' run on %O:`, fileName); | ||
const formatCodeSettings = await getFormatCodeSettingsForFile(fileName); | ||
// First attempt to fix all the errors | ||
await this.applyFixes(context, fileName, ts.DiagnosticCategory.Error, options); | ||
await this.applyFixes(context, fileName, ts.DiagnosticCategory.Error, options, formatCodeSettings); | ||
// Then attempt to run the suggestions | ||
await this.applyFixes(context, fileName, ts.DiagnosticCategory.Suggestion, options); | ||
await this.applyFixes(context, fileName, ts.DiagnosticCategory.Suggestion, options, formatCodeSettings); | ||
this.changeTrackers.forEach((tracker, file) => { | ||
context.service.setFileText(file, tracker.toString()); | ||
}); | ||
return Array.from(this.allFixedFiles); | ||
} | ||
async applyFixes(context, fileName, diagnosticCategory, options) { | ||
const diagnostics = this.getDiagnostics(context.service, fileName, diagnosticCategory); | ||
for (const diagnostic of diagnostics) { | ||
if (!diagnostic.node) { | ||
DEBUG_CALLBACK(` - TS${diagnostic.code} at ${diagnostic.start}:\t node not found`); | ||
async drainMode() { | ||
const { fileName, context, options } = this; | ||
const formatCodeSettings = await getFormatCodeSettingsForFile(fileName); | ||
let diagnostics = this.getDiagnostics(context.service, fileName, [ | ||
DiagnosticCategory.Error, | ||
DiagnosticCategory.Suggestion, | ||
]); | ||
// In the drain mode diagnostics list is getting refreshed in every cycle which might have end up | ||
// with more error need to be fixed then was originally. The limit based on original amount of diagnostics | ||
// helps to avoid an infinitive loop in some edge cases when new errors keep coming when previous fixed. | ||
let limit = diagnostics.length * 10; | ||
while (limit-- && diagnostics.length) { | ||
const diagnostic = diagnostics.shift(); | ||
const fix = this.getCodeFix(diagnostic, options, formatCodeSettings); | ||
if (!fix) { | ||
DEBUG_CALLBACK(` - TS${diagnostic.code} at ${diagnostic.start}:\t didn't fix`); | ||
continue; | ||
} | ||
const fix = this.getCodeFix(diagnostic, options); | ||
if (isInstallPackageCommand(fix)) { | ||
await this.applyCommandAction(fix.commands, context); | ||
} | ||
applyCodeFix(fix, { | ||
getText(filename) { | ||
return context.service.getFileText(filename); | ||
}, | ||
applyText(newText) { | ||
DEBUG_CALLBACK(`- TS${diagnostic.code} at ${diagnostic.start}:\t ${newText}`); | ||
}, | ||
setText: (filename, text) => { | ||
context.service.setFileText(filename, text); | ||
context.reporter.incrementRunFixedItemCount(); | ||
this.allFixedFiles.add(filename); | ||
DEBUG_CALLBACK(`- TS${diagnostic.code} at ${diagnostic.start}:\t codefix applied`); | ||
}, | ||
}); | ||
diagnostics = this.getDiagnostics(context.service, fileName, [ | ||
DiagnosticCategory.Error, | ||
DiagnosticCategory.Suggestion, | ||
]); | ||
} | ||
} | ||
async applyFixes(context, fileName, diagnosticCategory, options, formatCodeSettings) { | ||
const diagnostics = this.getDiagnostics(context.service, fileName, [diagnosticCategory]); | ||
for (const diagnostic of diagnostics) { | ||
const fix = this.getCodeFix(diagnostic, options, formatCodeSettings); | ||
if (!fix) { | ||
@@ -67,2 +119,3 @@ DEBUG_CALLBACK(` - TS${diagnostic.code} at ${diagnostic.start}:\t didn't fix`); | ||
changeTracker.remove(change.span.start, change.span.start + change.span.length); | ||
this.allFixedFiles.add(fileTextChange.fileName); | ||
} | ||
@@ -88,3 +141,3 @@ if (!this.appliedAtOffset[fileTextChange.fileName]) { | ||
*/ | ||
getDiagnostics(service, fileName, diagnosticFilterCategory) { | ||
getDiagnostics(service, fileName, diagnosticFilterCategories) { | ||
const languageService = service.getLanguageService(); | ||
@@ -96,3 +149,3 @@ const program = languageService.getProgram(); | ||
.filter((diagnostic) => { | ||
return diagnostic.category === diagnosticFilterCategory; | ||
return diagnosticFilterCategories.some((category) => category === diagnostic.category); | ||
}) | ||
@@ -124,7 +177,7 @@ // Convert DiagnosticWithLocation to DiagnosticWithContext | ||
*/ | ||
getCodeFix(diagnostic, options) { | ||
getCodeFix(diagnostic, options, formatCodeSettings) { | ||
const fixes = codefixes.getCodeFixes(diagnostic, { | ||
safeFixes: options.safeFixes, | ||
strictTyping: options.strictTyping, | ||
}); | ||
}, formatCodeSettings); | ||
if (fixes.length === 0) { | ||
@@ -131,0 +184,0 @@ return undefined; |
@@ -9,6 +9,2 @@ import { DiagnosticWithContext } from '@rehearsal/codefixes'; | ||
run(): Promise<string[]>; | ||
getBoundaryOfCommentBlock(start: number, length: number, text: string): { | ||
start: number; | ||
end: number; | ||
}; | ||
getDiagnostics(service: Service, fileName: string): DiagnosticWithContext[]; | ||
@@ -15,0 +11,0 @@ isValidDiagnostic(diagnostic: DiagnosticWithContext): boolean; |
@@ -7,3 +7,3 @@ import { hints } from '@rehearsal/codefixes'; | ||
import { getLocation } from '../helpers.js'; | ||
const { isLineBreak } = ts; | ||
import { getBoundaryOfCommentBlock } from './utils.js'; | ||
const DEBUG_CALLBACK = debug('rehearsal:plugins:diagnostic-report'); | ||
@@ -14,5 +14,5 @@ export class DiagnosticReportPlugin extends Plugin { | ||
DEBUG_CALLBACK(`Plugin 'DiagnosticReport' run on %O:`, fileName); | ||
const originalConentWithErrorsSupressed = context.service.getFileText(fileName); | ||
const lineHasSupression = {}; | ||
let contentWithErrors = originalConentWithErrorsSupressed; | ||
const originalContentWithErrorsSuppressed = context.service.getFileText(fileName); | ||
const lineHasSuppression = {}; | ||
let contentWithErrors = originalContentWithErrorsSuppressed; | ||
const sourceFile = context.service.getSourceFile(fileName); | ||
@@ -30,7 +30,8 @@ const tagStarts = [...contentWithErrors.matchAll(new RegExp(options.commentTag, 'g'))].map((m) => m.index); | ||
// Remove comment, together with the {} that wraps around comments in React | ||
const boundary = this.getBoundaryOfCommentBlock(commentSpan.start, commentSpan.length, contentWithErrors); | ||
const boundary = getBoundaryOfCommentBlock(commentSpan.start, commentSpan.length, contentWithErrors); | ||
contentWithErrors = | ||
contentWithErrors.substring(0, boundary.start) + contentWithErrors.substring(boundary.end); | ||
contentWithErrors.substring(0, boundary.start) + | ||
contentWithErrors.substring(boundary.end + 1); | ||
} | ||
// Our document now has unsupressed errors in it. Set that content into the langauge server so we can type check it | ||
// Our document now has unsuppressed errors in it. Set that content into the language server, so we can type check it | ||
context.service.setFileText(fileName, contentWithErrors); | ||
@@ -43,21 +44,11 @@ const diagnostics = this.getDiagnostics(context.service, fileName); | ||
// We only allow for a single entry per line | ||
if (!lineHasSupression[location.startLine]) { | ||
if (!lineHasSuppression[location.startLine]) { | ||
context.reporter.addTSItemToRun(diagnostic, diagnostic.node, location, hint, helpUrl, options.addHints); | ||
lineHasSupression[location.startLine] = true; | ||
lineHasSuppression[location.startLine] = true; | ||
} | ||
} | ||
// We have now collected the correct line / cols of the errors. We can now set the document back to one without errors. | ||
context.service.setFileText(fileName, originalConentWithErrorsSupressed); | ||
context.service.setFileText(fileName, originalContentWithErrorsSuppressed); | ||
return Promise.resolve([]); | ||
} | ||
getBoundaryOfCommentBlock(start, length, text) { | ||
const newStart = start - 1 >= 0 && text[start - 1] === '{' ? start - 1 : start; | ||
let end = start + length - 1; | ||
end = end + 1 < text.length && text[end + 1] === '}' ? end + 1 : end; | ||
end = isLineBreak(text.charCodeAt(end + 1)) ? end + 1 : end; | ||
return { | ||
start: newStart, | ||
end, | ||
}; | ||
} | ||
getDiagnostics(service, fileName) { | ||
@@ -64,0 +55,0 @@ const languageService = service.getLanguageService(); |
@@ -1,5 +0,5 @@ | ||
import { type TransformManager } from '@glint/core'; | ||
import { DiagnosticWithContext } from '@rehearsal/codefixes'; | ||
import { GlintService, PluginOptions, Service, PathUtils, Plugin } from '@rehearsal/service'; | ||
import { type Location } from '@rehearsal/reporter'; | ||
import type { TransformManager } from '@glint/core'; | ||
import type MS from 'magic-string'; | ||
@@ -6,0 +6,0 @@ export interface GlintCommentPluginOptions extends PluginOptions { |
@@ -78,2 +78,7 @@ import { extname } from 'node:path'; | ||
const isInHbsContext = this.shouldUseHbsComment(service.pathUtils, info, diagnostic.file.fileName, changeTracker.original, diagnostic, index); | ||
if (isInHbsContext) { | ||
// Abort trying to comment an hbs context until https://github.com/rehearsal-js/rehearsal-js/issues/1119 is resolved | ||
// For now, we DO NOT WANT add any {{! @glint-expect-errors }} directives in hbs contexts. | ||
return; | ||
} | ||
const message = `${commentTag} TODO TS${diagnostic.code}: ${hint}`; | ||
@@ -113,3 +118,14 @@ const tsIgnoreCommentText = isInHbsContext | ||
if (module) { | ||
const template = module.findTemplateAtOriginalOffset(filePath, diagnostic.start); | ||
let template; | ||
// We must wrap this with a try catch because the findTemplateAtOriginalOffset will throw | ||
// on a template only conmponent with only a string and no logic or args. | ||
try { | ||
template = module.findTemplateAtOriginalOffset(filePath, diagnostic.start); | ||
} | ||
catch (e) { | ||
DEBUG_CALLBACK(`Unable to findTemplateAtOriginalOffset for ${diagnostic.code} with ${filePath}`); | ||
if (extname(filePath) === '.hbs') { | ||
return true; | ||
} | ||
} | ||
// If we're able to find a template associated with the diagnostic, then we know the error | ||
@@ -116,0 +132,0 @@ // is pointing to the body of a template, and we likely to use HBS comments |
@@ -1,8 +0,14 @@ | ||
import ts from 'typescript'; | ||
import { Diagnostic } from 'vscode-languageserver'; | ||
import { DiagnosticWithContext } from '@rehearsal/codefixes'; | ||
import ts, { FormatCodeSettings } from 'typescript'; | ||
import { Plugin, GlintService, PluginsRunnerContext } from '@rehearsal/service'; | ||
import type { Diagnostic } from 'vscode-languageserver'; | ||
import type MS from 'magic-string'; | ||
export declare class GlintFixPlugin extends Plugin { | ||
appliedAtOffset: { | ||
[file: string]: number[]; | ||
import type { TextChange } from 'typescript'; | ||
export interface GlintFixPluginOptions { | ||
mode: 'single-pass' | 'drain'; | ||
} | ||
export declare class GlintFixPlugin extends Plugin<GlintFixPluginOptions> { | ||
attemptedToFix: string[]; | ||
appliedTextChanges: { | ||
[file: string]: TextChange[]; | ||
}; | ||
@@ -12,6 +18,11 @@ changeTrackers: Map<string, MS.default>; | ||
run(): Promise<string[]>; | ||
applyFix(fileName: string, context: PluginsRunnerContext, diagnosticCategory: ts.DiagnosticCategory): void; | ||
getDiagnostics(service: GlintService, fileName: string, diagnosticCategory: ts.DiagnosticCategory): Diagnostic[]; | ||
getCodeFix(fileName: string, diagnostic: Diagnostic, service: GlintService): ts.CodeFixAction | undefined; | ||
private drainMode; | ||
private singlePassMode; | ||
applyFix(fileName: string, context: PluginsRunnerContext, diagnosticCategory: ts.DiagnosticCategory, formatCodeSettings: FormatCodeSettings): void; | ||
hasAppliedChange(fileName: string, change: TextChange): boolean; | ||
getDiagnostics(service: GlintService, fileName: string, diagnosticCategories: ts.DiagnosticCategory[]): Diagnostic[]; | ||
getCodeFix(fileName: string, diagnostic: Diagnostic, service: GlintService, formatCodeSettings: FormatCodeSettings): ts.CodeFixAction | undefined; | ||
private wasAttemptedToFix; | ||
getDiagnosticsWithContext(fileName: string, diagnostic: Diagnostic, service: GlintService): DiagnosticWithContext; | ||
} | ||
//# sourceMappingURL=glint-fix.plugin.d.ts.map |
import { createRequire } from 'node:module'; | ||
import { makeCodeFixStrict } from '@rehearsal/codefixes'; | ||
import { glintCodeFixes, applyCodeFix, getDiagnosticOrder, } from '@rehearsal/codefixes'; | ||
import debug from 'debug'; | ||
import ts from 'typescript'; | ||
import { CodeActionKind } from 'vscode-languageserver'; | ||
import { Plugin } from '@rehearsal/service'; | ||
import hash from 'object-hash'; | ||
import { findNodeAtPosition, isSameChange, normalizeTextChanges } from '@rehearsal/ts-utils'; | ||
import { getFormatCodeSettingsForFile } from '../helpers.js'; | ||
const require = createRequire(import.meta.url); | ||
@@ -11,6 +13,8 @@ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment | ||
const DEBUG_CALLBACK = debug('rehearsal:plugins:glint-fix'); | ||
const { DiagnosticCategory } = ts; | ||
export class GlintFixPlugin extends Plugin { | ||
constructor() { | ||
super(...arguments); | ||
this.appliedAtOffset = {}; | ||
this.attemptedToFix = []; | ||
this.appliedTextChanges = {}; | ||
this.changeTrackers = new Map(); | ||
@@ -20,17 +24,71 @@ this.allFixedFiles = new Set(); | ||
async run() { | ||
const { options } = this; | ||
const { mode } = options; | ||
switch (mode) { | ||
case 'drain': | ||
await this.drainMode(); | ||
break; | ||
case 'single-pass': | ||
default: | ||
await this.singlePassMode(); | ||
} | ||
return Promise.resolve(Array.from(this.allFixedFiles)); | ||
} | ||
async drainMode() { | ||
const { fileName, context } = this; | ||
this.applyFix(fileName, context, ts.DiagnosticCategory.Error); | ||
this.applyFix(fileName, context, ts.DiagnosticCategory.Suggestion); | ||
const service = context.service; | ||
const formatCodeSettings = await getFormatCodeSettingsForFile(fileName); | ||
let diagnostics = this.getDiagnostics(service, fileName, [ | ||
DiagnosticCategory.Error, | ||
DiagnosticCategory.Suggestion, | ||
]); | ||
// In the drain mode diagnostics list is getting refreshed in every cycle which might have end up | ||
// with more error need to be fixed then was originally. The limit based on original amount of diagnostics | ||
// helps to avoid an infinitive loop in some edge cases when new errors keep coming when previous fixed. | ||
let limit = diagnostics.length * 10; | ||
while (limit-- && diagnostics.length) { | ||
const diagnostic = diagnostics.shift(); | ||
const fix = this.getCodeFix(fileName, diagnostic, service, formatCodeSettings); | ||
if (fix === undefined) { | ||
DEBUG_CALLBACK(` - TS${diagnostic.code} at ${diagnostic.range.start.line}:${diagnostic.range.start.character}:\t fix not found`); | ||
continue; | ||
} | ||
applyCodeFix(fix, { | ||
getText(filename) { | ||
return context.service.getFileText(filename); | ||
}, | ||
applyText(newText) { | ||
DEBUG_CALLBACK(`- TS${diagnostic.code} at ${diagnostic.range.start.line}:${diagnostic.range.start.character}:\t ${newText}`); | ||
}, | ||
setText: (filename, text) => { | ||
context.service.setFileText(filename, text); | ||
context.reporter.incrementRunFixedItemCount(); | ||
this.allFixedFiles.add(filename); | ||
DEBUG_CALLBACK(`- TS${diagnostic.code} at ${diagnostic.range.start.line}:${diagnostic.range.start.character}:\t codefix applied`); | ||
}, | ||
}); | ||
context.reporter.incrementRunFixedItemCount(); | ||
diagnostics = this.getDiagnostics(service, fileName, [ | ||
DiagnosticCategory.Error, | ||
DiagnosticCategory.Suggestion, | ||
]); | ||
} | ||
} | ||
async singlePassMode() { | ||
const { fileName, context } = this; | ||
const formatCodeSettings = await getFormatCodeSettingsForFile(fileName); | ||
this.applyFix(fileName, context, ts.DiagnosticCategory.Error, formatCodeSettings); | ||
this.applyFix(fileName, context, ts.DiagnosticCategory.Suggestion, formatCodeSettings); | ||
this.changeTrackers.forEach((tracker, fileName) => { | ||
context.service.setFileText(fileName, tracker.toString()); | ||
}); | ||
return Promise.resolve(Array.from(this.allFixedFiles)); | ||
} | ||
applyFix(fileName, context, diagnosticCategory) { | ||
applyFix(fileName, context, diagnosticCategory, formatCodeSettings) { | ||
var _a; | ||
const service = context.service; | ||
const diagnostics = this.getDiagnostics(service, fileName, diagnosticCategory); | ||
const diagnostics = this.getDiagnostics(service, fileName, [diagnosticCategory]); | ||
for (const diagnostic of diagnostics) { | ||
const fix = this.getCodeFix(fileName, diagnostic, service); | ||
const fix = this.getCodeFix(fileName, diagnostic, service, formatCodeSettings); | ||
if (fix === undefined) { | ||
DEBUG_CALLBACK(` - TS${diagnostic.code} at ${diagnostic.range.start}:\t node not found`); | ||
DEBUG_CALLBACK(` - TS${diagnostic.code} at ${diagnostic.range.start.line}:${diagnostic.range.start.character}:\t fix not found`); | ||
continue; | ||
@@ -53,13 +111,17 @@ } | ||
} | ||
if (!this.appliedAtOffset[fileTextChange.fileName]) { | ||
this.appliedAtOffset[fileTextChange.fileName] = []; | ||
const targetFileName = fileTextChange.fileName; | ||
// Track the applied changes so we can dedupe as we go | ||
if (this.hasAppliedChange(targetFileName, change)) { | ||
continue; // Skip if a duplicate change | ||
} | ||
if (this.appliedAtOffset[fileTextChange.fileName].includes(change.span.start)) { | ||
continue; | ||
} | ||
else { | ||
// this.appliedAtOffset[fileTextChange.fileName].push(change.span.start); | ||
// Init if undefined | ||
(_a = this.appliedTextChanges)[targetFileName] ?? (_a[targetFileName] = []); | ||
// Append and normalize | ||
this.appliedTextChanges[targetFileName].push(change); | ||
this.appliedTextChanges[targetFileName] = normalizeTextChanges(this.appliedTextChanges[targetFileName]); | ||
} | ||
DEBUG_CALLBACK(`- TS${diagnostic.code} at ${diagnostic.range.start}:\t ${change.newText}`); | ||
changeTracker.appendLeft(change.span.start, change.newText); | ||
context.reporter.incrementRunFixedItemCount(); | ||
this.allFixedFiles.add(fileTextChange.fileName); | ||
@@ -70,23 +132,65 @@ } | ||
} | ||
getDiagnostics(service, fileName, diagnosticCategory) { | ||
return service | ||
.getDiagnostics(fileName) | ||
.filter((d) => d.category === diagnosticCategory) | ||
hasAppliedChange(fileName, change) { | ||
if (!this.appliedTextChanges[fileName]) { | ||
return false; | ||
} | ||
return !!this.appliedTextChanges[fileName].find((existing) => isSameChange(existing, change)); | ||
} | ||
getDiagnostics(service, fileName, diagnosticCategories) { | ||
const unordered = service.getDiagnostics(fileName); | ||
const diagnostics = getDiagnosticOrder(unordered); | ||
return diagnostics | ||
.filter((d) => diagnosticCategories.some((category) => category === d.category)) | ||
.map((d) => service.convertTsDiagnosticToLSP(d)); | ||
} | ||
getCodeFix(fileName, diagnostic, service) { | ||
const glintService = service.getGlintService(); | ||
const rawActions = glintService.getCodeActions(fileName, CodeActionKind.QuickFix, diagnostic.range, [diagnostic]); | ||
const transformedActions = service | ||
.transformCodeActionToCodeFixAction(rawActions) | ||
.reduce((acc, fix) => { | ||
const strictFix = makeCodeFixStrict(fix); | ||
if (strictFix) { | ||
acc.push(strictFix); | ||
} | ||
return acc; | ||
}, []); | ||
return transformedActions[0]; | ||
getCodeFix(fileName, diagnostic, service, formatCodeSettings) { | ||
const diagnosticWithContext = this.getDiagnosticsWithContext(fileName, diagnostic, service); | ||
const fixes = glintCodeFixes.getCodeFixes(diagnosticWithContext, { | ||
safeFixes: true, | ||
strictTyping: true, | ||
}, formatCodeSettings); | ||
if (fixes.length === 0) { | ||
return undefined; | ||
} | ||
// Use the first available codefix in automatic mode | ||
let fix = fixes.shift(); | ||
while (fix && this.wasAttemptedToFix(diagnostic, fix)) { | ||
// Try the next fix if we already tried the first one | ||
fix = fixes.shift(); | ||
} | ||
if (fix === undefined) { | ||
DEBUG_CALLBACK(` - TS${diagnostic.code} at ${diagnosticWithContext.start}:\t fixes didn't work`); | ||
} | ||
return fix; | ||
} | ||
wasAttemptedToFix(diagnostic, fix) { | ||
const diagnosticFixHash = hash([ | ||
diagnostic.code, | ||
diagnostic.range.start.line, | ||
diagnostic.range.start.character, | ||
fix, | ||
]); | ||
if (!this.attemptedToFix.includes(diagnosticFixHash)) { | ||
this.attemptedToFix.push(diagnosticFixHash); | ||
return false; | ||
} | ||
return true; | ||
} | ||
getDiagnosticsWithContext(fileName, diagnostic, service) { | ||
const program = service.getLanguageService().getProgram(); | ||
const checker = program.getTypeChecker(); | ||
const diagnosticWithLocation = service.convertLSPDiagnosticToTs(fileName, diagnostic); | ||
return { | ||
...diagnosticWithLocation, | ||
...{ | ||
glintService: service, | ||
glintDiagnostic: diagnostic, | ||
service: service.getLanguageService(), | ||
program, | ||
checker, | ||
node: findNodeAtPosition(diagnosticWithLocation.file, diagnosticWithLocation.start, diagnosticWithLocation.length), | ||
}, | ||
}; | ||
} | ||
} | ||
//# sourceMappingURL=glint-fix.plugin.js.map |
@@ -1,2 +0,2 @@ | ||
import { hints, getDiagnosticOrder } from '@rehearsal/codefixes'; | ||
import { hints } from '@rehearsal/codefixes'; | ||
import { Plugin } from '@rehearsal/service'; | ||
@@ -31,3 +31,3 @@ import debug from 'debug'; | ||
} | ||
// Our document now has unsupressed errors in it. Set that content into the langauge server so we can type check it | ||
// Our document now has unsuppressed errors in it. Set that content into the langauge server so we can type check it | ||
service.setFileText(fileName, contentWithErrors); | ||
@@ -41,7 +41,12 @@ const diagnostics = this.getDiagnostics(service, fileName); | ||
if (!lineHasSupression[location.startLine]) { | ||
context.reporter.addTSItemToRun(diagnostic, diagnostic.node, location, hint, helpUrl); | ||
if (diagnostic.source === 'glint') { | ||
context.reporter.addGlintItemToRun(diagnostic, diagnostic.node, location, hint, helpUrl); | ||
} | ||
else { | ||
context.reporter.addTSItemToRun(diagnostic, diagnostic.node, location, hint, helpUrl); | ||
} | ||
lineHasSupression[location.startLine] = true; | ||
} | ||
} | ||
// Set the document back to the content with the errors supressed | ||
// Set the document back to the content with the errors suppressed | ||
service.setFileText(fileName, originalContentWithErrorsSuppressed); | ||
@@ -54,3 +59,3 @@ return Promise.resolve([]); | ||
const checker = program.getTypeChecker(); | ||
const diagnostics = getDiagnosticOrder(service.getDiagnostics(fileName)).filter((d) => this.isErrorDiagnostic(d)); | ||
const diagnostics = service.getDiagnostics(fileName).filter((d) => this.isErrorDiagnostic(d)); | ||
return diagnostics.reduce((acc, diagnostic) => { | ||
@@ -57,0 +62,0 @@ const location = getLocation(diagnostic.file, diagnostic.start, diagnostic.length); |
@@ -11,3 +11,3 @@ import { Plugin } from '@rehearsal/service'; | ||
*/ | ||
export declare function isPrettierUsedForFormatting(fileName: string): boolean; | ||
export declare function isPrettierUsedForFormatting(fileName: string): Promise<boolean>; | ||
//# sourceMappingURL=prettier.plugin.d.ts.map |
@@ -13,5 +13,5 @@ import { Plugin } from '@rehearsal/service'; | ||
try { | ||
const prettierOptions = prettier.resolveConfig.sync(fileName) || {}; | ||
const prettierOptions = (await prettier.resolveConfig(fileName)) || {}; | ||
prettierOptions.filepath = fileName; | ||
const result = prettier.format(text, prettierOptions); | ||
const result = await prettier.format(text, prettierOptions); | ||
DEBUG_CALLBACK(`Plugin 'Prettier' run on %O:`, fileName); | ||
@@ -30,6 +30,6 @@ context.service.setFileText(fileName, result); | ||
*/ | ||
export function isPrettierUsedForFormatting(fileName) { | ||
export async function isPrettierUsedForFormatting(fileName) { | ||
// TODO: Better validation can be implemented | ||
return prettier.resolveConfigFile.sync(fileName) !== null; | ||
return (await prettier.resolveConfigFile(fileName)) !== null; | ||
} | ||
//# sourceMappingURL=prettier.plugin.js.map |
@@ -10,7 +10,3 @@ import { PluginOptions, Plugin } from '@rehearsal/service'; | ||
run(): Promise<string[]>; | ||
getBoundaryOfCommentBlock(start: number, length: number, text: string): { | ||
start: number; | ||
end: number; | ||
}; | ||
} | ||
//# sourceMappingURL=re-rehearse.plugin.d.ts.map |
@@ -1,6 +0,5 @@ | ||
import ts from 'typescript'; | ||
import { Plugin } from '@rehearsal/service'; | ||
import debug from 'debug'; | ||
import { getBoundaryOfCommentBlock } from './utils.js'; | ||
const DEBUG_CALLBACK = debug('rehearsal:plugins:rerehearse'); | ||
const { isLineBreak } = ts; | ||
/** | ||
@@ -12,2 +11,3 @@ * Removes all comments with `@rehearsal` tag inside | ||
const { fileName, context, options } = this; | ||
DEBUG_CALLBACK(`this: %O`, this); | ||
let text = context.service.getFileText(fileName); | ||
@@ -25,4 +25,4 @@ const sourceFile = context.service.getSourceFile(fileName); | ||
// Remove comment, together with the {} that wraps around comments in React | ||
const boundary = this.getBoundaryOfCommentBlock(commentSpan.start, commentSpan.length, text); | ||
text = text.substring(0, boundary.start) + text.substring(boundary.end); | ||
const boundary = getBoundaryOfCommentBlock(commentSpan.start, commentSpan.length, text); | ||
text = text.substring(0, boundary.start) + text.substring(boundary.end + 1); | ||
} | ||
@@ -33,13 +33,3 @@ context.service.setFileText(fileName, text); | ||
} | ||
getBoundaryOfCommentBlock(start, length, text) { | ||
const newStart = start - 1 >= 0 && text[start - 1] === '{' ? start - 1 : start; | ||
let end = start + length - 1; | ||
end = end + 1 < text.length && text[end + 1] === '}' ? end + 1 : end; | ||
end = isLineBreak(text.charCodeAt(end + 1)) ? end + 1 : end; | ||
return { | ||
start: newStart, | ||
end, | ||
}; | ||
} | ||
} | ||
//# sourceMappingURL=re-rehearse.plugin.js.map |
@@ -34,2 +34,3 @@ import { Plugin, PluginOptions } from '@rehearsal/service'; | ||
toClassName(str: string): string; | ||
hasServiceInMap(serviceName: string): boolean; | ||
/** | ||
@@ -36,0 +37,0 @@ * Search for service in the services map using camelCase and kebab-case as a key |
@@ -61,15 +61,15 @@ import { extname } from 'node:path'; | ||
let serviceModule; | ||
if (this.isFullyQualifiedService(qualifiedService)) { | ||
if (this.hasServiceInMap(qualifiedService)) { | ||
// Strategy: Use services map | ||
DEBUG_CALLBACK(`Found service in map ${qualifiedService}`); | ||
serviceModule = this.findServiceInMap(qualifiedService); | ||
const [, serviceName] = this.parseFullyQualifiedService(qualifiedService); | ||
serviceClass = this.toClassName(serviceName); | ||
} | ||
else if (this.isFullyQualifiedService(qualifiedService)) { | ||
// Strategy: Ember fully qualified service names: `addon@service` | ||
const [addon, serviceName] = this.parseFullyQualifiedService(qualifiedService); | ||
const [packageName, serviceName] = this.parseFullyQualifiedService(qualifiedService); | ||
serviceClass = this.toClassName(serviceName); | ||
serviceModule = `${addon}/services/${this.toKebabCase(serviceName)}`; | ||
serviceModule = `${packageName}/services/${this.toKebabCase(serviceName)}`; | ||
} | ||
else { | ||
// Strategy: Use services map | ||
serviceModule = this.findServiceInMap(qualifiedService); | ||
if (serviceModule) { | ||
serviceClass = this.toClassName(qualifiedService); | ||
} | ||
} | ||
if (!serviceClass || !serviceModule) { | ||
@@ -161,16 +161,11 @@ continue; | ||
getQualifiedServiceName(decorator, prop) { | ||
if (ts.isCallExpression(decorator.expression)) { | ||
// Covers `@service('service-name')` and `@service('addon@service-name')` | ||
if (decorator.expression.arguments.length) { | ||
const arg = decorator.expression.arguments[0]; | ||
return ts.isStringLiteral(arg) ? arg.text : undefined; | ||
} | ||
// Covers `@service('service-name')` and `@service('addon@service-name')` | ||
if (ts.isCallExpression(decorator.expression) && decorator.expression.arguments.length) { | ||
const arg = decorator.expression.arguments[0]; | ||
return ts.isStringLiteral(arg) ? arg.text : undefined; | ||
} | ||
else { | ||
// Covers `@service serviceName` | ||
if (ts.isPropertyDeclaration(prop) && ts.isIdentifier(prop.name)) { | ||
return this.toKebabCase(prop.name.escapedText.toString()); | ||
} | ||
// Covers `@service serviceName` and `@service() serviceName` | ||
if (ts.isPropertyDeclaration(prop) && ts.isIdentifier(prop.name)) { | ||
return this.toKebabCase(prop.name.escapedText.toString()); | ||
} | ||
// Covers @service() propName | ||
return undefined; | ||
@@ -185,3 +180,7 @@ } | ||
parseFullyQualifiedService(service) { | ||
return service.split('@'); | ||
if (!this.isFullyQualifiedService(service)) { | ||
return ['', service]; | ||
} | ||
const idx = service.lastIndexOf('@'); | ||
return [service.substring(0, idx), service.substring(idx + 1)]; | ||
} | ||
@@ -207,2 +206,5 @@ /** | ||
} | ||
hasServiceInMap(serviceName) { | ||
return this.findServiceInMap(serviceName) !== undefined; | ||
} | ||
/** | ||
@@ -215,2 +217,5 @@ * Search for service in the services map using camelCase and kebab-case as a key | ||
} | ||
if (this.options.servicesMap.has(serviceName)) { | ||
return this.options.servicesMap.get(serviceName); | ||
} | ||
const serviceNameKebab = this.toKebabCase(serviceName); | ||
@@ -217,0 +222,0 @@ if (this.options.servicesMap.has(serviceNameKebab)) { |
@@ -17,2 +17,6 @@ import ts from 'typescript'; | ||
export declare function inJsxText(sourceFile: ts.SourceFile, pos: number): boolean; | ||
export declare function getBoundaryOfCommentBlock(start: number, length: number, text: string): { | ||
start: number; | ||
end: number; | ||
}; | ||
//# sourceMappingURL=utils.d.ts.map |
@@ -90,2 +90,12 @@ import ts from 'typescript'; | ||
} | ||
export function getBoundaryOfCommentBlock(start, length, text) { | ||
const newStart = start - 1 >= 0 && text[start - 1] === '{' ? start - 1 : start; | ||
let end = start + length - 1; | ||
end = end + 1 < text.length && text[end + 1] === '}' ? end + 1 : end; | ||
end = ts.isLineBreak(text.charCodeAt(end + 1)) ? end + 1 : end; | ||
return { | ||
start: newStart, | ||
end, | ||
}; | ||
} | ||
//# sourceMappingURL=utils.js.map |
{ | ||
"name": "@rehearsal/plugins", | ||
"version": "2.0.2-beta", | ||
"version": "2.1.0", | ||
"description": "Rehearsal JavaScript to TypeScript Shared Libraries", | ||
@@ -36,13 +36,13 @@ "keywords": [ | ||
"object-hash": "^3.0.0", | ||
"prettier": "^2.8.7", | ||
"vscode-languageserver": "^8.1.0", | ||
"@rehearsal/codefixes": "2.0.2-beta", | ||
"@rehearsal/reporter": "2.0.2-beta", | ||
"@rehearsal/service": "2.0.2-beta", | ||
"@rehearsal/ts-utils": "2.0.2-beta" | ||
"@rehearsal/reporter": "2.1.0", | ||
"@rehearsal/codefixes": "2.1.0", | ||
"@rehearsal/ts-utils": "2.1.0", | ||
"@rehearsal/service": "2.1.0" | ||
}, | ||
"devDependencies": { | ||
"@types/eslint": "^8.37.0", | ||
"@vitest/coverage-c8": "^0.30.1", | ||
"@vitest/coverage-c8": "^0.33.0", | ||
"fixturify-project": "^5.2.0", | ||
"prettier": "^3.0.2", | ||
"vitest": "^0.30.0" | ||
@@ -52,5 +52,5 @@ }, | ||
"@glint/core": "^1.0.2", | ||
"typescript": "^5.0" | ||
"typescript": "^5.1" | ||
}, | ||
"packageManager": "pnpm@8.2.0", | ||
"packageManager": "pnpm@8.6.7", | ||
"engines": { | ||
@@ -68,3 +68,2 @@ "node": ">=14.16.0" | ||
"test": "vitest --run --config ./vitest.config.ts --coverage", | ||
"test:slow": "vitest --run", | ||
"test:watch": "vitest --coverage --watch", | ||
@@ -71,0 +70,0 @@ "version": "pnpm version" |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
No v1
QualityPackage is not semver >=1. This means it is not stable and does not support ^ ranges.
Found 1 instance in 1 package
271287
12
1566
1
5
2
+ Added@rehearsal/codefixes@2.1.0(transitive)
+ Added@rehearsal/reporter@2.1.0(transitive)
+ Added@rehearsal/service@2.1.0(transitive)
+ Added@rehearsal/ts-utils@2.1.0(transitive)
+ Added@rehearsal/utils@2.1.0(transitive)
+ Addedfind-up@6.3.0(transitive)
+ Addedlocate-path@7.2.0(transitive)
+ Addedp-limit@4.0.0(transitive)
+ Addedp-locate@6.0.0(transitive)
+ Addedpath-exists@5.0.0(transitive)
+ Addedyocto-queue@1.1.1(transitive)
- Removedprettier@^2.8.7
- Removed@rehearsal/codefixes@2.0.2-beta(transitive)
- Removed@rehearsal/reporter@2.0.2-beta(transitive)
- Removed@rehearsal/service@2.0.2-beta(transitive)
- Removed@rehearsal/ts-utils@2.0.2-beta(transitive)
- Removed@rehearsal/utils@2.0.2-beta(transitive)
- Removedbraces@3.0.3(transitive)
- Removedchalk@5.4.1(transitive)
- Removedcommander@10.0.1(transitive)
- Removedcompare-versions@6.0.0-rc.1(transitive)
- Removeddetect-file@1.0.0(transitive)
- Removedexpand-tilde@2.0.2(transitive)
- Removedfast-glob@3.3.3(transitive)
- Removedfill-range@7.1.1(transitive)
- Removedfindup-sync@5.0.0(transitive)
- Removedglob-parent@5.1.2(transitive)
- Removedglobal-modules@1.0.0(transitive)
- Removedglobal-prefix@1.0.2(transitive)
- Removedhomedir-polyfill@1.0.3(transitive)
- Removedini@1.3.8(transitive)
- Removedis-number@7.0.0(transitive)
- Removedis-windows@1.0.2(transitive)
- Removedmerge2@1.4.1(transitive)
- Removedmicromatch@4.0.8(transitive)
- Removedparse-passwd@1.0.0(transitive)
- Removedpicomatch@2.3.1(transitive)
- Removedprettier@2.8.8(transitive)
- Removedresolve-dir@1.0.1(transitive)
- Removedto-regex-range@5.0.1(transitive)
- Removedwhich@1.3.1(transitive)
Updated@rehearsal/codefixes@2.1.0
Updated@rehearsal/reporter@2.1.0
Updated@rehearsal/service@2.1.0
Updated@rehearsal/ts-utils@2.1.0