diff --git a/package.json b/package.json index 604c9b6..1dd83ec 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "@types/vscode": "^1.130", "prettier": "^3.9.6", "prettier-plugin-multiline-arrays": "^4.1.11", - "typescript": "^5.7" + "typescript": "^7.0.0" }, "dependencies": { "is-wsl": "^3.1.0", diff --git a/src/configuration.ts b/src/configuration.ts index 8c52848..79ef468 100644 --- a/src/configuration.ts +++ b/src/configuration.ts @@ -95,7 +95,7 @@ export class Configuration { // Read the default multi-line config from the JSON file and cache it for later use. this.defaultMultiLineConfig = utils.readJsonFile(`${configPath}/default-multi-line-config.json`) as vscode.LanguageConfiguration; // Read the languages to skip from the JSON file and cache it for later use. - this.languagesToSkip = utils.readJsonFile(`${configPath}/skip-languages.jsonc`); + this.languagesToSkip = utils.readJsonFile(`${configPath}/skip-languages.jsonc`) ?? {}; this.findAllLanguageConfigFilePaths(); this.setLanguageConfigDefinitions(); @@ -255,7 +255,7 @@ export class Configuration { * ``` */ public getConfigurationValue(key: K): Settings[K] { - return this.getConfiguration().get(key); + return this.getConfiguration().get(key) as Settings[K]; } /** @@ -343,8 +343,8 @@ export class Configuration { const builtInExtensionsPath = this.extensionData.getExtensionDiscoveryPath("builtInExtensionsPath"); // Read the paths and create arrays of the extensions. - const userExtensions = this.readExtensionsFromDirectory(userExtensionsPath); - const builtInExtensions = this.readExtensionsFromDirectory(builtInExtensionsPath); + const userExtensions = userExtensionsPath ? this.readExtensionsFromDirectory(userExtensionsPath) : []; + const builtInExtensions = builtInExtensionsPath ? this.readExtensionsFromDirectory(builtInExtensionsPath) : []; // Add all installed extensions (including built-in ones) into the extensions array. // If running WSL, these will be the WSL-installed extensions. @@ -372,7 +372,7 @@ export class Configuration { // If the langId already exists... if (this.languageConfigFilePaths.has(langId)) { // Push the new config path into the array of the existing langId. - this.languageConfigFilePaths.get(langId).push(configPath); + this.languageConfigFilePaths.get(langId)?.push(configPath); } // Otherwise, if the langId doesn't exist... else { @@ -412,7 +412,7 @@ export class Configuration { // Define a new array as the new AutoClosingPair. const autoClosingPairsArray: vscode.AutoClosingPair[] = []; // Loop through the config's autoClosingPairs... - config.autoClosingPairs.forEach((item) => { + (config.autoClosingPairs ?? []).forEach((item) => { // If the item is an array... if (Array.isArray(item)) { // Create a new object with the 1st array element [0] as the @@ -439,7 +439,7 @@ export class Configuration { const existingConfig = this.languageConfigs.get(langId); // Only merge if both configs have comments - if (existingConfig.comments && config.comments) { + if (existingConfig?.comments && config.comments) { // Start with existing comments as base const mergedComments = {...existingConfig.comments}; @@ -449,7 +449,9 @@ export class Configuration { if (Array.isArray(value) && value.length === 0) { return; } - mergedComments[key] = value; + if (key === "lineComment" || key === "blockComment") { + mergedComments[key] = value; + } }); // Update the config with merged comments @@ -555,7 +557,7 @@ export class Configuration { this.languageConfigs.forEach((config: vscode.LanguageConfiguration, langId: LanguageId) => { // If the config object has own property of comments AND the comments key has // own property of blockComment... - if (Object.hasOwn(config, "comments") && Object.hasOwn(config.comments, "blockComment")) { + if (config.comments && Object.hasOwn(config.comments, "blockComment") && config.comments.blockComment) { // If the blockComment array includes the multi-line start of "/*"... if (config.comments.blockComment.includes("/*")) { // console.log(langId, config.comments); @@ -627,7 +629,7 @@ export class Configuration { // If the config object has own property of comments AND the comments key has // own property of lineComment... - if (Object.hasOwn(config, "comments") && Object.hasOwn(config.comments, "lineComment")) { + if (config.comments && Object.hasOwn(config.comments, "lineComment")) { let lineComment = config.comments.lineComment; // Line comments can be a string or an object with a "comment" key. @@ -752,21 +754,19 @@ export class Configuration { // Deep-clone the internalLangConfig so modifications never write back // into the cached `languageConfigs` Map by accident. - let langConfig: vscode.LanguageConfiguration = internalLangConfig - ? structuredClone(internalLangConfig) - : {}; + let langConfig: vscode.LanguageConfiguration = internalLangConfig ? structuredClone(internalLangConfig) : {}; if (multiLine) { langConfig.autoClosingPairs = utils.mergeArraysBy( - this.defaultMultiLineConfig.autoClosingPairs, - internalLangConfig?.autoClosingPairs, + this.defaultMultiLineConfig.autoClosingPairs ?? [], + internalLangConfig?.autoClosingPairs ?? [], "open" ); // Add the multi-line onEnter rules to the langConfig. langConfig.onEnterRules = utils.mergeArraysBy( Rules.multilineEnterRules, - internalLangConfig?.onEnterRules, + internalLangConfig?.onEnterRules ?? [], "beforeText" ); @@ -780,7 +780,7 @@ export class Configuration { if (this.isLangIdMultiLineCommentOverridden(langId) && langConfig.comments?.blockComment) { langConfig.comments.blockComment = [ this.getOverriddenMultiLineComment(langId), - langConfig.comments.blockComment[1] + langConfig.comments.blockComment[1], ]; } @@ -792,6 +792,7 @@ export class Configuration { // If bladeComments has a value... if (bladeComments) { + langConfig.comments ??= {}; langConfig.comments.blockComment = bladeComments; } } @@ -805,22 +806,26 @@ export class Configuration { if (isOnEnter && singleLineStyle) { // //-style comments if (singleLineStyle === "//") { - langConfig.onEnterRules = utils.mergeArraysBy(Rules.slashEnterRules, langConfig?.onEnterRules, "beforeText"); + langConfig.onEnterRules = utils.mergeArraysBy(Rules.slashEnterRules, langConfig.onEnterRules ?? [], "beforeText"); } // #-style comments else if (singleLineStyle === "#") { - langConfig.onEnterRules = utils.mergeArraysBy(Rules.hashEnterRules, langConfig?.onEnterRules, "beforeText"); + langConfig.onEnterRules = utils.mergeArraysBy(Rules.hashEnterRules, langConfig.onEnterRules ?? [], "beforeText"); } // ;-style comments else if (singleLineStyle === ";") { - langConfig.onEnterRules = utils.mergeArraysBy(Rules.semicolonEnterRules, langConfig?.onEnterRules, "beforeText"); + langConfig.onEnterRules = utils.mergeArraysBy( + Rules.semicolonEnterRules, + langConfig.onEnterRules ?? [], + "beforeText" + ); } } // If isOnEnter is false AND singleLineStyle isn't false, i.e. a string. else if (!isOnEnter && singleLineStyle) { // If langConfig does NOT have a comments key OR // the comments key exists but does NOT have the lineComment key... - if (!Object.hasOwn(langConfig, "comments") || !Object.hasOwn(langConfig.comments, "lineComment")) { + if (!langConfig.comments || !Object.hasOwn(langConfig.comments, "lineComment")) { // Add the singleLineStyle to the lineComments key and make sure any // blockComments aren't overwritten. langConfig.comments = {...langConfig.comments, lineComment: singleLineStyle}; @@ -838,7 +843,7 @@ export class Configuration { // Check if isOnEnter OR multiline is true. if (isOnEnter || multiLine) { - langConfig.onEnterRules.forEach((item) => { + (langConfig.onEnterRules ?? []).forEach((item) => { // Check if the item has a "beforeText" property and reconstruct its regex pattern. if (Object.hasOwn(item, "beforeText")) { item.beforeText = utils.reconstructRegex(item, "beforeText"); @@ -941,12 +946,13 @@ export class Configuration { // Get the langId from the auto-supported langs. If it doesn't exist, try getting it from // the custom-supported langs instead. - let style: SingleLineCommentStyle | ExtraSingleLineCommentStyles = singleLineLangs.get(langId) ?? customSingleLineLangs.get(langId); + let style: SingleLineCommentStyle | ExtraSingleLineCommentStyles | undefined = + singleLineLangs.get(langId) ?? customSingleLineLangs.get(langId); if (style && textEditor.selection.isEmpty) { let line = textEditor.document.lineAt(textEditor.selection.active); let isCommentLine = true; - let indentRegex: RegExp; + let indentRegex: RegExp | undefined; if (style === "//" && line.text.search(/^\s*\/\/\s*/) !== -1) { indentRegex = /\//; @@ -972,7 +978,7 @@ export class Configuration { isCommentLine = false; } - if (!isCommentLine) { + if (!isCommentLine || !indentRegex) { return; } diff --git a/src/extension.ts b/src/extension.ts index 9de99ed..ca89b93 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -9,9 +9,6 @@ import {addDevEnvVariables} from "./utils"; import {LogLevel} from "./interfaces/utils"; export function activate(context: vscode.ExtensionContext) { - // Setup logger first - logger.setupOutputChannel(); - const initialLogLevel = vscode.workspace.getConfiguration("auto-comment-blocks").get("logLevel", "debug"); logger.setLogLevel(initialLogLevel); diff --git a/src/extensionData.ts b/src/extensionData.ts index dbdeac4..b37efe7 100644 --- a/src/extensionData.ts +++ b/src/extensionData.ts @@ -61,9 +61,9 @@ export class ExtensionData { /** * The package.json data for this extension. * - * @type {IPackageJson} + * @type {IPackageJson | null} */ - private packageJsonData: IPackageJson; + private packageJsonData: IPackageJson | null; /** * Create an instance of the ExtensionData class, which retrieves and stores metadata @@ -90,10 +90,7 @@ export class ExtensionData { this.packageJsonData = this.getExtensionPackageJsonData(); - // Only proceed with extension data setup if packageJsonData is NOT null. - if (this.packageJsonData !== null) { - this.setExtensionData(); - } + this.setExtensionData(); this.setExtensionDiscoveryPaths(); } @@ -101,7 +98,7 @@ export class ExtensionData { /** * Get the names, id, and version of this extension from package.json. * - * @returns {IPackageJson | null} The package.json data for this extension, with extra custom keys. + * @returns {IPackageJson | null} The package.json data for this extension. */ private getExtensionPackageJsonData(): IPackageJson | null { // Get the package.json file path. @@ -113,26 +110,32 @@ export class ExtensionData { * Set the extension data into the extensionData Map. */ private setExtensionData() { + // Only proceed if packageJsonData is NOT falsy, otherwise return early. + const packageJsonData = this.packageJsonData; + if (!packageJsonData) { + return; + } + // Create the extension ID (publisher.name). - const id = `${this.packageJsonData.publisher}.${this.packageJsonData.name}`; + const id = `${packageJsonData.publisher}.${packageJsonData.name}`; // Set each key-value pair directly into the Map this.extensionData.set("id", id); - this.extensionData.set("name", this.packageJsonData.name); + this.extensionData.set("name", packageJsonData.name); // Only set the namespace if it dealing with this extension. - if (this.packageJsonData.name === "automatic-comment-blocks") { + if (packageJsonData.name === "automatic-comment-blocks") { // The configuration settings namespace is a shortened version of the extension name. // We just need to replace "automatic" with "auto" in the name. - const settingsNamespace: string = this.packageJsonData.name.replace("automatic", "auto"); + const settingsNamespace: string = packageJsonData.name.replace("automatic", "auto"); this.extensionData.set("namespace", settingsNamespace); } - this.extensionData.set("displayName", this.packageJsonData.displayName); - this.extensionData.set("version", this.packageJsonData.version); + this.extensionData.set("displayName", packageJsonData.displayName); + this.extensionData.set("version", packageJsonData.version); this.extensionData.set("extensionPath", this.extensionPath); - this.extensionData.set("packageJSON", this.packageJsonData); + this.extensionData.set("packageJSON", packageJsonData); } /** diff --git a/src/interfaces/commands.ts b/src/interfaces/commands.ts index f39d732..56bb138 100644 --- a/src/interfaces/commands.ts +++ b/src/interfaces/commands.ts @@ -18,10 +18,10 @@ export interface CommandRegistration { * command is executed. * * @param textEditor The text editor - * @param edit The text editor edits. Optional because some commands may not need it. + * @param edit The text editor edits. * @returns void */ - handler: (textEditor: vscode.TextEditor, edit?: vscode.TextEditorEdit) => void; + handler: (textEditor: vscode.TextEditor, edit: vscode.TextEditorEdit) => void; } /** diff --git a/src/logger.ts b/src/logger.ts index 6f8bee2..328ae01 100644 --- a/src/logger.ts +++ b/src/logger.ts @@ -5,6 +5,7 @@ import {LogLevel, logLevels} from "./interfaces/utils"; /** * Logger class for the Auto Comment Blocks extension. * This class handles logging messages of differing log levels to the output channel. + * Logger is a singleton class, and should only be instantiated once (in this file). * * @class Logger */ @@ -41,15 +42,10 @@ class Logger { ***********/ /** - * Override the output channel - * - * @param {OutputChannel} channelOverride A vscode output channel. + * Constructor for the Logger class, which + * initialises the output channel for logging. */ - public setupOutputChannel(channelOverride?: OutputChannel): void { - if (channelOverride) { - this.outputChannel = channelOverride; - return; - } + constructor() { this.outputChannel = window.createOutputChannel("Auto Comment Blocks", "log"); } @@ -90,9 +86,7 @@ class Logger { * Show the output channel to the user. */ public showChannel(): void { - if (this.outputChannel) { - this.outputChannel.show(); - } + this.outputChannel.show(); } /** @@ -190,10 +184,6 @@ class Logger { * @param {unknown} meta Extra data as needed. */ private logMessage(level: string, message: string, meta?: unknown): void { - if (!this.outputChannel) { - this.setupOutputChannel(); - } - message = this.redactUsername(message); const time = new Date().toLocaleTimeString(); @@ -310,4 +300,5 @@ class Logger { } } +// Create and export the singleton instance of the Logger class. export const logger = new Logger(); diff --git a/src/utils.ts b/src/utils.ts index 3e5f2d6..3135b51 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -23,7 +23,7 @@ export function readJsonFile(filepath: string, // If throwOnFileMissing param is true, throw an error. if (throwOnFileMissing) { const error = new Error(`JSON file not found: "${filepath}"`); - logger.error(error.stack); + logger.error(error.stack ?? error.message); throw error; } // Otherwise just return null. @@ -60,7 +60,7 @@ function parseJsonContent(filepath: string, fi const errorMsg = "Failed to parse a required JSON file"; const error = new Error(`${errorMsg}: "${filepath}"\n\n\tParse Errors:\n\n${errorMessages}\n\tStack Trace:`); - logger.error(error.stack); + logger.error(error.stack ?? error.message); window .showErrorMessage( @@ -137,18 +137,20 @@ export function ensureDirExists(dir: string) { * Reconstruct the regex pattern because vscode doesn't like the regex pattern as a string, * or some patterns are not working as expected. * - * @param {unknown} obj The object + * @param {T} obj The object * @param {string} key The key to check in the object * @returns {RegExp} The reconstructed regex pattern. */ -export function reconstructRegex(obj: unknown, key: string): RegExp { +export function reconstructRegex(obj: T, key: K): RegExp { + const value = obj[key]; + // If key has a "pattern" key, then it's an object... - if (Object.hasOwn(obj[key], "pattern")) { - return new RegExp(obj[key].pattern); + if (typeof value === "object" && value !== null && Object.hasOwn(value, "pattern")) { + return new RegExp((value as unknown as {pattern: string}).pattern); } // Otherwise it's a string. else { - return new RegExp(obj[key]); + return new RegExp(value as string); } } @@ -185,7 +187,7 @@ export function reconstructRegex(obj: unknown, key: string): RegExp { * } */ export function convertMapToReversedObject(m: Map>): T { - const result = {}; + const result: Record> = {}; // Convert a nested key:value Map from inside another Map into an key:array object, // while reversing/switching the keys and values. The Map's values are now the keys of @@ -199,15 +201,15 @@ export function convertMapToReversedObject(m: // Reverse the inner object mapping. // - // Loop through the object (o) keys, assigns a new object (r) with the value of the - // object key (k) as the new key (eg. "//") and the new value is an array of all - // the original object keys (o[k]) (eg. "php"). - // If the key (o[k]) already exists in the new object (r), then just add the - // original key to the array, otherwise start a new array ([]) with the original - // key as value ( (r[o[k]] || []).concat(k) ). - // Add this new reversed object to the result object with the outer map key - // as the key. - result[key] = Object.keys(o).reduce((r, k) => Object.assign(r, {[o[k]]: (r[o[k]] || []).concat(k)}), {}); + // Loop through the object (o) keys, and for each one, push the key (itemKey, eg. "php") + // onto the array keyed by its value (o[itemKey], eg. "//") in the reversed object, + // creating that array on first use. Add this reversed object to the result object + // with the outer map key as the key. + result[key] = Object.keys(o).reduce>((reversed, itemKey) => { + const value = o[itemKey]; + (reversed[value] ??= []).push(itemKey); + return reversed; + }, {}); } return result as T; } @@ -279,9 +281,9 @@ function validateDevEnvVariables() { // Trim whitespace and resolve the path to an absolute path let devPath = path.resolve(process.env.DEV_USER_EXTENSIONS_PATH.trim()); - let stats: fs.Stats; + let stats: fs.Stats | undefined; let errorMsg: string = ""; - let errorData: Error; + let errorData: Error | undefined; // Get the file system stats for the path to check if it exists. // statSync throws an exception if the no file system data exists for the path, @@ -293,13 +295,13 @@ function validateDevEnvVariables() { const errorCode = nodeError.code || "UNKNOWN"; // Handle specific file system errors with user-friendly messages. - const errorMessages = { + const errorMessages: Record = { ENOENT: "Path from env variable 'DEV_USER_EXTENSIONS_PATH' does not exist", EACCES: "Permission denied accessing path from env variable 'DEV_USER_EXTENSIONS_PATH'", UNKNOWN: "Unknown error accessing the path from env variable 'DEV_USER_EXTENSIONS_PATH'", }; - errorMsg = `${errorCode}: ${errorMessages[errorCode]}: "${devPath}". Removing from environment.`; + errorMsg = `${errorCode}: ${errorMessages[errorCode] ?? errorMessages.UNKNOWN}: "${devPath}". Removing from environment.`; errorData = error as Error; diff --git a/tsconfig.json b/tsconfig.json index 27594a4..b37a283 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,15 +1,26 @@ { "compilerOptions": { "module": "commonjs", - "target": "es2022", + "target": "es2025", "outDir": "out", - "lib": ["es2022"], + "lib": [ + "es2025" + ], "sourceMap": true, "rootDir": ".", - "typeRoots": ["./node_modules/@types"] + "typeRoots": [ + "./node_modules/@types" + ], + "types": [ + "node", + "vscode" + ] }, "typeAcquisition": { - "enable": true, + "enable": true }, - "exclude": ["node_modules", ".vscode-test"] + "exclude": [ + "node_modules", + ".vscode-test" + ] }