diff --git a/src/content-linter/lib/helpers/liquid-utils.ts b/src/content-linter/lib/helpers/liquid-utils.ts index 4ff9ca7bdb..f8c3b22047 100644 --- a/src/content-linter/lib/helpers/liquid-utils.ts +++ b/src/content-linter/lib/helpers/liquid-utils.ts @@ -1,20 +1,16 @@ import { Tokenizer, TokenKind } from 'liquidjs' +import type { TopLevelToken, TagToken } from 'liquidjs' import { deprecated } from '@/versions/lib/enterprise-server-releases' -// Using `any` for the cache because TopLevelToken is a complex union type from liquidjs -// that includes TagToken, OutputToken, and HTMLToken with different properties. -// The cache is private to this module and we control all access to it. -const liquidTokenCache = new Map() +// Cache for liquid tokens to improve performance +const liquidTokenCache = new Map() -// Returns `any[]` instead of `TopLevelToken[]` because TopLevelToken is a union type -// (TagToken | OutputToken | HTMLToken) and consumers of this function access properties -// like `name` and `args` that only exist on TagToken. Using `any` here avoids complex -// type narrowing throughout the codebase. +// Returns TopLevelToken array from liquidjs which is a union of TagToken, OutputToken, and HTMLToken export function getLiquidTokens( content: string, { noCache = false }: { noCache?: boolean } = {}, -): any[] { +): TopLevelToken[] { if (!content) return [] if (noCache) { @@ -23,13 +19,13 @@ export function getLiquidTokens( } if (liquidTokenCache.has(content)) { - return liquidTokenCache.get(content) + return liquidTokenCache.get(content)! } const tokenizer = new Tokenizer(content) const tokens = tokenizer.readTopLevelTokens() liquidTokenCache.set(content, tokens) - return liquidTokenCache.get(content) + return liquidTokenCache.get(content)! } export const OUTPUT_OPEN = '{%' @@ -40,10 +36,9 @@ export const TAG_CLOSE = '}}' export const conditionalTags = ['if', 'elseif', 'unless', 'case', 'ifversion'] const CONDITIONAL_TAG_NAMES = ['if', 'ifversion', 'elsif', 'else', 'endif'] -// Token is `any` because it's used with different token types from liquidjs -// that all have `begin` and `end` properties but are part of complex union types. +// Token parameter uses TopLevelToken which has begin and end properties export function getPositionData( - token: any, + token: TopLevelToken, lines: string[], ): { lineNumber: number; column: number; length: number } { // Liquid indexes are 0-based, but we want to @@ -77,9 +72,9 @@ export function getPositionData( * by Markdownlint: * [ { lineNumber: 1, column: 1, deleteCount: 3, }] */ -// Token is `any` because it's used with different token types from liquidjs. +// Token parameter uses TopLevelToken from liquidjs export function getContentDeleteData( - token: any, + token: TopLevelToken, tokenEnd: number, lines: string[], ): Array<{ lineNumber: number; column: number; deleteCount: number }> { @@ -123,15 +118,14 @@ export function getContentDeleteData( // related elsif, else, and endif tags). // Docs doesn't use the standard `if` tag for versioning, instead the // `ifversion` tag is used. -// Returns `any[]` because the tokens need to be accessed as TagToken with `name` and `args` properties, -// but TopLevelToken union type would require complex type narrowing. -export function getLiquidIfVersionTokens(content: string): any[] { +// Returns TagToken array since we filter to only Tag tokens +export function getLiquidIfVersionTokens(content: string): TagToken[] { const tokens = getLiquidTokens(content) - .filter((token) => token.kind === TokenKind.Tag) + .filter((token): token is TagToken => token.kind === TokenKind.Tag) .filter((token) => CONDITIONAL_TAG_NAMES.includes(token.name)) let inIfStatement = false - const ifVersionTokens: any[] = [] + const ifVersionTokens: TagToken[] = [] for (const token of tokens) { if (token.name === 'if') { inIfStatement = true diff --git a/src/content-linter/lib/helpers/utils.ts b/src/content-linter/lib/helpers/utils.ts index 24af845b97..2a28b651a8 100644 --- a/src/content-linter/lib/helpers/utils.ts +++ b/src/content-linter/lib/helpers/utils.ts @@ -11,8 +11,8 @@ export function addFixErrorDetail( actual: string, // Using flexible type to accommodate different range formats from various linting rules range: [number, number] | number[] | null, - // Using any for fixInfo as markdownlint-rule-helpers accepts various fix info structures - fixInfo: any, + // Using unknown for fixInfo as markdownlint-rule-helpers accepts various fix info structures + fixInfo: unknown, ): void { addError(onError, lineNumber, `Expected: ${expected}`, ` Actual: ${actual}`, range, fixInfo) } @@ -20,9 +20,11 @@ export function addFixErrorDetail( export function forEachInlineChild( params: RuleParams, type: string, - // Using any for child and token types because different linting rules pass tokens with varying structures - // beyond the base MarkdownToken interface (e.g., ImageToken with additional properties) - handler: (child: any, token: any) => void, + // Handler uses `any` for function parameter variance reasons. TypeScript's contravariance rules for function + // parameters mean that a function accepting a specific type cannot be assigned to a parameter of type `unknown`. + // Therefore, `unknown` cannot be used here, as different linting rules pass tokens with varying structures + // beyond the base MarkdownToken interface, and some handlers are async. + handler: (child: any, token?: any) => void | Promise, ): void { filterTokens(params, 'inline', (token: MarkdownToken) => { for (const child of token.children!.filter((c) => c.type === type)) { @@ -146,8 +148,8 @@ export const docsDomains = ['docs.github.com', 'help.github.com', 'developer.git // This is the format we get from Markdownlint. // Returns null if the lines do not contain // frontmatter properties. -// Returns frontmatter as a Record with any values since YAML can contain various types -export function getFrontmatter(lines: string[]): Record | null { +// Returns frontmatter as a Record with unknown values since YAML can contain various types +export function getFrontmatter(lines: string[]): Record | null { const fmString = lines.join('\n') const { data } = matter(fmString) // If there is no frontmatter or the frontmatter contains diff --git a/src/content-linter/lib/linting-rules/internal-links-slash.ts b/src/content-linter/lib/linting-rules/internal-links-slash.ts index 0076c1531a..2a4101ee19 100644 --- a/src/content-linter/lib/linting-rules/internal-links-slash.ts +++ b/src/content-linter/lib/linting-rules/internal-links-slash.ts @@ -1,7 +1,7 @@ import { filterTokens } from 'markdownlint-rule-helpers' import { addFixErrorDetail, getRange } from '../helpers/utils' -import type { RuleParams, RuleErrorCallback, Rule } from '../../types' +import type { RuleParams, RuleErrorCallback, Rule, MarkdownToken } from '../../types' export const internalLinksSlash: Rule = { names: ['GHD003', 'internal-links-slash'], @@ -9,8 +9,8 @@ export const internalLinksSlash: Rule = { tags: ['links', 'url'], parser: 'markdownit', function: (params: RuleParams, onError: RuleErrorCallback) => { - // Using 'any' type for token as markdownlint-rule-helpers doesn't provide TypeScript types - filterTokens(params, 'inline', (token: any) => { + filterTokens(params, 'inline', (token: MarkdownToken) => { + if (!token.children) return for (const child of token.children) { if (child.type !== 'link_open') continue @@ -20,6 +20,7 @@ export const internalLinksSlash: Rule = { // ['rel', 'canonical'], // ] // Attribute arrays are tuples of [attributeName, attributeValue] from markdownit parser + if (!child.attrs) continue const hrefsMissingSlashes = child.attrs // The attribute could also be `target` or `rel` .filter((attr: [string, string]) => attr[0] === 'href') diff --git a/src/content-linter/lib/linting-rules/liquid-data-tags.ts b/src/content-linter/lib/linting-rules/liquid-data-tags.ts index d66d5f9747..5a626fa8f5 100644 --- a/src/content-linter/lib/linting-rules/liquid-data-tags.ts +++ b/src/content-linter/lib/linting-rules/liquid-data-tags.ts @@ -1,5 +1,6 @@ import { addError } from 'markdownlint-rule-helpers' import { TokenKind } from 'liquidjs' +import type { TagToken } from 'liquidjs' import { getDataByLanguage } from '@/data-directory/lib/get-data' import { @@ -23,10 +24,9 @@ export const liquidDataReferencesDefined = { parser: 'markdownit', function: (params: RuleParams, onError: RuleErrorCallback) => { const content = params.lines.join('\n') - // Using any type because getLiquidTokens returns tokens from liquidjs library without complete type definitions const tokens = getLiquidTokens(content) - .filter((token: any) => token.kind === TokenKind.Tag) - .filter((token: any) => token.name === 'data' || token.name === 'indented_data_reference') + .filter((token): token is TagToken => token.kind === TokenKind.Tag) + .filter((token) => token.name === 'data' || token.name === 'indented_data_reference') if (!tokens.length) return @@ -60,13 +60,11 @@ export const liquidDataTagFormat = { function: (params: RuleParams, onError: RuleErrorCallback) => { const CHECK_LIQUID_TAGS = [OUTPUT_OPEN, OUTPUT_CLOSE, '{', '}'] const content = params.lines.join('\n') - // Using any type because getLiquidTokens returns tokens from liquidjs library without complete type definitions - // Tokens have properties like 'kind', 'name', 'args', and 'content' that aren't fully typed - const tokenTags = getLiquidTokens(content).filter((token: any) => token.kind === TokenKind.Tag) - const dataTags = tokenTags.filter((token: any) => token.name === 'data') - const indentedDataTags = tokenTags.filter( - (token: any) => token.name === 'indented_data_reference', + const tokenTags = getLiquidTokens(content).filter( + (token): token is TagToken => token.kind === TokenKind.Tag, ) + const dataTags = tokenTags.filter((token) => token.name === 'data') + const indentedDataTags = tokenTags.filter((token) => token.name === 'indented_data_reference') for (const token of dataTags) { // A data tag has only one argument, the data directory path. diff --git a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts index 6631133278..c56439c41d 100644 --- a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts +++ b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts @@ -1,4 +1,5 @@ import { addError } from 'markdownlint-rule-helpers' +import type { TopLevelToken } from 'liquidjs' import { getLiquidIfVersionTokens, @@ -35,8 +36,11 @@ export const liquidIfversionVersions = { const fileVersionsFm = params.name.startsWith('data') ? { ghec: '*', ghes: '*', fpt: '*' } : fm - ? fm.versions - : getFrontmatter(params.frontMatterLines)?.versions + ? (fm.versions as string | Record | undefined) + : (getFrontmatter(params.frontMatterLines)?.versions as + | string + | Record + | undefined) // This will only contain valid (non-deprecated) and future versions const fileVersions = getApplicableVersions(fileVersionsFm, '', { doNotThrow: true, @@ -134,7 +138,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines: { begin: item.begin, end: item.end, - }, + } as TopLevelToken, lines, ) const deleteCount = length - column + 1 === lines[lineNumber - 1].length ? -1 : length @@ -159,7 +163,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines: { begin: item.contentrange[0], end: item.contentrange[1], - }, + } as TopLevelToken, lines, ) const insertText = `${item.action.name || item.name} ${item.action.cond || item.cond}` diff --git a/src/content-linter/lib/linting-rules/liquid-quoted-conditional-arg.ts b/src/content-linter/lib/linting-rules/liquid-quoted-conditional-arg.ts index 66b1320bf2..13ad700cb1 100644 --- a/src/content-linter/lib/linting-rules/liquid-quoted-conditional-arg.ts +++ b/src/content-linter/lib/linting-rules/liquid-quoted-conditional-arg.ts @@ -1,4 +1,5 @@ import { TokenKind } from 'liquidjs' +import type { TagToken } from 'liquidjs' import { addError } from 'markdownlint-rule-helpers' import { getLiquidTokens, conditionalTags, getPositionData } from '../helpers/liquid-utils' @@ -19,14 +20,12 @@ export const liquidQuotedConditionalArg: Rule = { tags: ['liquid', 'format'], function: (params: RuleParams, onError: RuleErrorCallback) => { const content = params.lines.join('\n') - // Using 'any' type for tokens as getLiquidTokens returns tokens from liquid-utils.ts which lacks type definitions const tokens = getLiquidTokens(content) - .filter((token: any) => token.kind === TokenKind.Tag) - .filter((token: any) => conditionalTags.includes(token.name)) - .filter((token: any) => { + .filter((token): token is TagToken => token.kind === TokenKind.Tag) + .filter((token) => conditionalTags.includes(token.name)) + .filter((token) => { const tokensArray = token.args.split(/\s+/g) - // Using 'any' for args as they come from the untyped liquid token structure - if (tokensArray.some((arg: any) => isStringQuoted(arg))) return true + if (tokensArray.some((arg) => isStringQuoted(arg))) return true return false }) diff --git a/src/content-linter/lib/linting-rules/liquid-syntax.ts b/src/content-linter/lib/linting-rules/liquid-syntax.ts index debb548e78..5e3a93ea8d 100644 --- a/src/content-linter/lib/linting-rules/liquid-syntax.ts +++ b/src/content-linter/lib/linting-rules/liquid-syntax.ts @@ -33,6 +33,7 @@ export const frontmatterLiquidSyntax = { for (const key of keysWithLiquid) { const value = fm[key] + if (typeof value !== 'string') continue try { liquid.parse(value) } catch (error) { diff --git a/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts b/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts index e0678607ac..1bdae8501f 100644 --- a/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts +++ b/src/content-linter/lib/linting-rules/liquid-tag-whitespace.ts @@ -1,4 +1,5 @@ import { TokenKind } from 'liquidjs' +import type { TopLevelToken } from 'liquidjs' import { getLiquidTokens, getPositionData } from '../helpers/liquid-utils' import { addFixErrorDetail } from '../helpers/utils' @@ -36,7 +37,10 @@ export const liquidTagWhitespace: Rule = { (token: LiquidToken) => token.kind === TokenKind.Tag, ) for (const token of tokens) { - const { lineNumber, column, length } = getPositionData(token, params.lines) + const { lineNumber, column, length } = getPositionData( + token as unknown as TopLevelToken, + params.lines, + ) const range = [column, length] const tag = params.lines[lineNumber - 1].slice(column - 1, column - 1 + length) diff --git a/src/content-linter/lib/linting-rules/liquid-versioning.ts b/src/content-linter/lib/linting-rules/liquid-versioning.ts index 6fa1471de0..a9062aa042 100644 --- a/src/content-linter/lib/linting-rules/liquid-versioning.ts +++ b/src/content-linter/lib/linting-rules/liquid-versioning.ts @@ -1,5 +1,6 @@ import semver from 'semver' import { TokenKind } from 'liquidjs' +import type { TagToken } from 'liquidjs' import { addError } from 'markdownlint-rule-helpers' import { getRange, addFixErrorDetail } from '../helpers/utils' @@ -13,7 +14,7 @@ import type { RuleParams, RuleErrorCallback } from '@/content-linter/types' interface Feature { versions: Record - [key: string]: any + [key: string]: unknown } type AllFeatures = Record @@ -60,12 +61,13 @@ export const liquidIfTags = { function: (params: RuleParams, onError: RuleErrorCallback) => { const content = params.lines.join('\n') - const tokens = getLiquidTokens(content).filter( - (token) => - token.kind === TokenKind.Tag && - token.name === 'if' && - token.args.split(/\s+/).some((arg: string) => getAllPossibleVersionNames().has(arg)), - ) + const tokens = getLiquidTokens(content) + .filter((token): token is TagToken => token.kind === TokenKind.Tag) + .filter( + (token) => + token.name === 'if' && + token.args.split(/\s+/).some((arg: string) => getAllPossibleVersionNames().has(arg)), + ) for (const token of tokens) { const args = token.args @@ -90,7 +92,7 @@ export const liquidIfVersionTags = { function: (params: RuleParams, onError: RuleErrorCallback) => { const content = params.lines.join('\n') const tokens = getLiquidTokens(content) - .filter((token) => token.kind === TokenKind.Tag) + .filter((token): token is TagToken => token.kind === TokenKind.Tag) .filter((token) => token.name === 'ifversion' || token.name === 'elsif') for (const token of tokens) { diff --git a/src/content-linter/lib/linting-rules/rai-reusable-usage.ts b/src/content-linter/lib/linting-rules/rai-reusable-usage.ts index 2072611f16..56ce74eb1c 100644 --- a/src/content-linter/lib/linting-rules/rai-reusable-usage.ts +++ b/src/content-linter/lib/linting-rules/rai-reusable-usage.ts @@ -1,5 +1,6 @@ import { addError } from 'markdownlint-rule-helpers' import { TokenKind } from 'liquidjs' +import type { TopLevelToken } from 'liquidjs' import path from 'path' import { getFrontmatter } from '../helpers/utils' @@ -45,7 +46,10 @@ export const raiReusableUsage: Rule = { if (dataDirectoryReference.startsWith('reusables.rai')) continue const lines = params.lines - const { lineNumber, column, length } = getPositionData(token, lines) + const { lineNumber, column, length } = getPositionData( + token as unknown as TopLevelToken, + lines, + ) addError( onError, lineNumber, diff --git a/src/content-linter/scripts/find-unsed-variables.ts b/src/content-linter/scripts/find-unsed-variables.ts index 29a409c6bb..cfe38135c2 100644 --- a/src/content-linter/scripts/find-unsed-variables.ts +++ b/src/content-linter/scripts/find-unsed-variables.ts @@ -22,7 +22,8 @@ import yaml from 'js-yaml' import { program } from 'commander' import { loadPages, loadUnversionedTree } from '@/frame/lib/page-data' -import { TokenizationError } from 'liquidjs' +import { TokenizationError, TokenKind } from 'liquidjs' +import type { TagToken } from 'liquidjs' import readFrontmatter from '@/frame/lib/read-frontmatter' import { getLiquidTokens } from '@/content-linter/lib/helpers/liquid-utils' @@ -137,7 +138,10 @@ function getReusableFiles(root = 'data') { function checkString(string: string, variables: Map) { try { - for (const token of getLiquidTokens(string)) { + const tokens = getLiquidTokens(string).filter( + (token): token is TagToken => token.kind === TokenKind.Tag, + ) + for (const token of tokens) { if (token.name === 'data') { const { args } = token variables.delete(args) diff --git a/src/content-render/liquid/prompt.ts b/src/content-render/liquid/prompt.ts index d241786e6b..dbba2b28b3 100644 --- a/src/content-render/liquid/prompt.ts +++ b/src/content-render/liquid/prompt.ts @@ -2,25 +2,25 @@ // Defines {% prompt %}…{% endprompt %} to wrap its content in and append the Copilot icon. import octicons from '@primer/octicons' +import type { TagToken, TopLevelToken } from 'liquidjs' import { generatePromptId } from '../lib/prompt-id' interface LiquidTag { type: 'block' - templates?: any[] // Note: Using 'any' because liquidjs doesn't provide proper types for template objects - // Note: Using 'any' for liquid-related parameters because liquidjs doesn't provide comprehensive TypeScript definitions - parse(tagToken: any, remainTokens: any): void - render(scope: any): Generator + templates?: unknown[] + parse(tagToken: TagToken, remainTokens: TopLevelToken[]): void + render(scope: unknown): Generator } export const Prompt: LiquidTag = { type: 'block', // Collect everything until {% endprompt %} - parse(tagToken: any, remainTokens: any): void { + parse(tagToken: TagToken, remainTokens: TopLevelToken[]): void { this.templates = [] const stream = this.liquid.parser.parseStream(remainTokens) stream - .on('template', (tpl: any) => this.templates.push(tpl)) + .on('template', (tpl: unknown) => this.templates.push(tpl)) .on('tag:endprompt', () => stream.stop()) .on('end', () => { throw new Error(`{% prompt %} tag not closed`) @@ -29,7 +29,7 @@ export const Prompt: LiquidTag = { }, // Render the inner Markdown, wrap in , then append the SVG - *render(scope: any): Generator { + *render(scope: unknown): Generator { const content = yield this.liquid.renderer.renderTemplates(this.templates, scope) const contentString = String(content) diff --git a/src/content-render/scripts/reusables-cli/find/unused.ts b/src/content-render/scripts/reusables-cli/find/unused.ts index 82feb590ab..1f7bf29e87 100644 --- a/src/content-render/scripts/reusables-cli/find/unused.ts +++ b/src/content-render/scripts/reusables-cli/find/unused.ts @@ -1,5 +1,7 @@ import fs from 'fs' import path from 'path' +import { TokenKind } from 'liquidjs' +import type { TagToken } from 'liquidjs' import { getLiquidTokens } from '@/content-linter/lib/helpers/liquid-utils' import { getAllContentFilePaths, @@ -21,7 +23,9 @@ export function findUnused({ absolute }: { absolute: boolean }) { for (let i = 0; i < totalFiles; i++) { const filePath = allFilePaths[i] const fileContents = fs.readFileSync(filePath, 'utf-8') - const liquidTokens = getLiquidTokens(fileContents) + const liquidTokens = getLiquidTokens(fileContents).filter( + (token): token is TagToken => token.kind === TokenKind.Tag, + ) for (const token of liquidTokens) { const { args, name } = token if ( diff --git a/src/content-render/scripts/reusables-cli/find/used.ts b/src/content-render/scripts/reusables-cli/find/used.ts index 24e1851a1a..589b87e12e 100644 --- a/src/content-render/scripts/reusables-cli/find/used.ts +++ b/src/content-render/scripts/reusables-cli/find/used.ts @@ -1,5 +1,7 @@ import fs from 'fs' import path from 'path' +import { TokenKind } from 'liquidjs' +import type { TagToken } from 'liquidjs' import { getLiquidTokens } from '@/content-linter/lib/helpers/liquid-utils' import { FilesWithLineNumbers, @@ -51,7 +53,9 @@ export function findTopUsed(numberOfMostUsedToFind: number, { absolute }: { abso const reusableCounts = new Map() for (const filePath of allFilePaths) { const fileContents = fs.readFileSync(filePath, 'utf-8') - const liquidTokens = getLiquidTokens(fileContents) + const liquidTokens = getLiquidTokens(fileContents).filter( + (token): token is TagToken => token.kind === TokenKind.Tag, + ) for (const token of liquidTokens) { const { args, name } = token if (name === 'data' && args.startsWith('reusables.')) { diff --git a/src/content-render/scripts/reusables-cli/shared.ts b/src/content-render/scripts/reusables-cli/shared.ts index 1df03be81a..792f48828b 100644 --- a/src/content-render/scripts/reusables-cli/shared.ts +++ b/src/content-render/scripts/reusables-cli/shared.ts @@ -1,6 +1,7 @@ import walk from 'walk-sync' import path from 'path' -import { TokenizationError } from 'liquidjs' +import { TokenizationError, TokenKind } from 'liquidjs' +import type { TagToken } from 'liquidjs' import { getLiquidTokens } from '@/content-linter/lib/helpers/liquid-utils' const __dirname = path.dirname(new URL(import.meta.url).pathname) @@ -56,7 +57,10 @@ export function getReusableLiquidString(reusablePath: string): string { export function getIndicesOfLiquidVariable(liquidVariable: string, fileContents: string): number[] { const indices: number[] = [] try { - for (const token of getLiquidTokens(fileContents)) { + const tokens = getLiquidTokens(fileContents).filter( + (token): token is TagToken => token.kind === TokenKind.Tag, + ) + for (const token of tokens) { if (token.name === 'data' && token.args.trim() === liquidVariable) { indices.push(token.begin) } diff --git a/src/content-render/types.ts b/src/content-render/types.ts index 9fc352ce9a..1f01894583 100644 --- a/src/content-render/types.ts +++ b/src/content-render/types.ts @@ -11,13 +11,13 @@ export interface Context { currentVersion?: string currentProduct?: string markdownRequested?: boolean - pages?: any - redirects?: any + pages?: Record + redirects?: Record page?: { fullPath: string - [key: string]: any + [key: string]: unknown } - [key: string]: any + [key: string]: unknown } /** @@ -27,20 +27,20 @@ export interface RenderOptions { cache?: boolean | ((template: string, context: Context) => string | null) filename?: string textOnly?: boolean - [key: string]: any + [key: string]: unknown } /** * Unified processor plugin function type */ -export type UnifiedPlugin = (context?: Context) => any +export type UnifiedPlugin = (context?: Context) => unknown /** * VFile interface for unified processing */ export interface VFile { toString(): string - [key: string]: any + [key: string]: unknown } /** @@ -48,5 +48,5 @@ export interface VFile { */ export interface UnifiedProcessor { process(content: string): Promise - use(plugin: any, ...args: any[]): UnifiedProcessor + use(plugin: unknown, ...args: unknown[]): UnifiedProcessor } diff --git a/src/content-render/unified/processor.ts b/src/content-render/unified/processor.ts index 39e9d1ff49..2544b201f7 100644 --- a/src/content-render/unified/processor.ts +++ b/src/content-render/unified/processor.ts @@ -40,21 +40,21 @@ export function createProcessor(context: Context): UnifiedProcessor { .use(gfm) // Markdown AST below vvv .use(parseInfoString) - // Using 'as any' because rewriteLocalLinks is a factory function that takes context + // Using type assertion because rewriteLocalLinks is a factory function that takes context // and returns a transformer, but TypeScript's unified plugin types don't handle this pattern - .use(rewriteLocalLinks as any, context) + .use(rewriteLocalLinks as unknown as (ctx: Context) => void, context) .use(emoji) // Markdown AST above ^^^ .use(remark2rehype, { allowDangerousHtml: true }) // HTML AST below vvv .use(slug) // useEnglishHeadings plugin requires context with englishHeadings property - .use(useEnglishHeadings as any, context || {}) + .use(useEnglishHeadings as unknown as (ctx: Context) => void, context || {}) .use(headingLinks) .use(codeHeader) .use(annotate, context) - // Using 'as any' for highlight plugin due to complex type mismatch between unified and rehype-highlight - .use(highlight as any, { + // Using type assertion for highlight plugin due to complex type mismatch between unified and rehype-highlight + .use(highlight as unknown as (options: unknown) => void, { languages: { ...common, graphql, dockerfile, http, groovy, erb, powershell }, subset: false, aliases: { @@ -82,9 +82,9 @@ export function createProcessor(context: Context): UnifiedProcessor { .use(rewriteImgSources) .use(rewriteAssetImgTags) // alerts plugin requires context with alertTitles property - .use(alerts as any, context || {}) + .use(alerts as unknown as (ctx: Context) => void, context || {}) // HTML AST above ^^^ - .use(html) as UnifiedProcessor // String below vvv + .use(html) as unknown as UnifiedProcessor // String below vvv ) } @@ -93,10 +93,10 @@ export function createMarkdownOnlyProcessor(context: Context): UnifiedProcessor unified() .use(remarkParse) .use(gfm) - // Using 'as any' because rewriteLocalLinks is a factory function that takes context + // Using type assertion because rewriteLocalLinks is a factory function that takes context // and returns a transformer, but TypeScript's unified plugin types don't handle this pattern - .use(rewriteLocalLinks as any, context) - .use(remarkStringify) as UnifiedProcessor + .use(rewriteLocalLinks as unknown as (ctx: Context) => void, context) + .use(remarkStringify) as unknown as UnifiedProcessor ) } @@ -105,12 +105,12 @@ export function createMinimalProcessor(context: Context): UnifiedProcessor { unified() .use(remarkParse) .use(gfm) - // Using 'as any' because rewriteLocalLinks is a factory function that takes context + // Using type assertion because rewriteLocalLinks is a factory function that takes context // and returns a transformer, but TypeScript's unified plugin types don't handle this pattern - .use(rewriteLocalLinks as any, context) + .use(rewriteLocalLinks as unknown as (ctx: Context) => void, context) .use(remark2rehype, { allowDangerousHtml: true }) .use(slug) .use(raw) - .use(html) as UnifiedProcessor + .use(html) as unknown as UnifiedProcessor ) } diff --git a/src/content-render/unified/use-english-headings.ts b/src/content-render/unified/use-english-headings.ts index 47b90c4e63..0109be6be7 100644 --- a/src/content-render/unified/use-english-headings.ts +++ b/src/content-render/unified/use-english-headings.ts @@ -2,14 +2,10 @@ import GithubSlugger from 'github-slugger' import { encode } from 'html-entities' import { toString } from 'hast-util-to-string' import { visit } from 'unist-util-visit' +import type { Element, Root } from 'hast' const slugger = new GithubSlugger() -// Note: Using 'any' for node because the unist/hast type system is complex and -// the visit function's type constraints don't easily allow for proper element typing -// without extensive type gymnastics. The runtime check ensures type safety. -const matcher = (node: any) => node.type === 'element' && ['h2', 'h3', 'h4'].includes(node.tagName) - interface UseEnglishHeadingsOptions { englishHeadings?: Record } @@ -17,12 +13,9 @@ interface UseEnglishHeadingsOptions { // replace translated IDs and links in headings with English export default function useEnglishHeadings({ englishHeadings }: UseEnglishHeadingsOptions) { if (!englishHeadings) return - // Note: Using 'any' for tree because unified's AST types are complex and - // this function works with different tree types depending on the processor - return (tree: any) => { - // Note: Using 'any' for node because visit() callback typing is restrictive - // and doesn't easily allow for proper element typing without complex generics - visit(tree, matcher, (node: any) => { + return (tree: Root) => { + visit(tree, 'element', (node: Element) => { + if (!['h2', 'h3', 'h4'].includes(node.tagName)) return slugger.reset() // Get the plain text content of the heading node const text: string = toString(node) diff --git a/src/data-directory/scripts/find-orphaned-features/find.ts b/src/data-directory/scripts/find-orphaned-features/find.ts index bce2bb69b6..e43169e816 100644 --- a/src/data-directory/scripts/find-orphaned-features/find.ts +++ b/src/data-directory/scripts/find-orphaned-features/find.ts @@ -32,7 +32,8 @@ import fs from 'fs' import path from 'path' import chalk from 'chalk' -import { TokenizationError } from 'liquidjs' +import { TokenizationError, TokenKind } from 'liquidjs' +import type { TagToken } from 'liquidjs' import type { Page } from '@/types' import warmServer from '@/frame/lib/warm-server' @@ -246,7 +247,10 @@ function checkString( // a LOT of different strings in and the cache will fill up rapidly // when testing every possible string in every possible language for // every page. - for (const token of getLiquidTokens(string, { noCache: true })) { + const tokens = getLiquidTokens(string, { noCache: true }).filter( + (token): token is TagToken => token.kind === TokenKind.Tag, + ) + for (const token of tokens) { if (token.name === 'ifversion' || token.name === 'elsif') { for (const arg of token.args.split(/\s+/)) { if (IGNORE_ARGS.has(arg)) continue diff --git a/src/frame/pages/app.tsx b/src/frame/pages/app.tsx index 272b5d358d..2f4aa2e767 100644 --- a/src/frame/pages/app.tsx +++ b/src/frame/pages/app.tsx @@ -18,6 +18,7 @@ import { import { useTheme } from '@/color-schemes/components/useTheme' import { SharedUIContextProvider } from '@/frame/components/context/SharedUIContext' import { CTAPopoverProvider } from '@/frame/components/context/CTAContext' +import type { ExtendedRequest } from '@/types' type MyAppProps = AppProps & { isDotComAuthenticated: boolean @@ -158,7 +159,7 @@ MyApp.getInitialProps = async (appContext: AppContext) => { const { ctx } = appContext // calls page's `getInitialProps` and fills `appProps.pageProps` const appProps = await App.getInitialProps(appContext) - const req: any = ctx.req + const req = ctx.req as unknown as ExtendedRequest // Have to define the type manually here because `req.context.languages` // comes from Node JS and is not type-aware. @@ -188,11 +189,14 @@ MyApp.getInitialProps = async (appContext: AppContext) => { } } } - const stagingName = req.headers['x-ong-external-url']?.match(/staging-(\w+)\./)?.[1] + const headerValue = req.headers['x-ong-external-url'] + const stagingName = (typeof headerValue === 'string' ? headerValue : headerValue?.[0])?.match( + /staging-(\w+)\./, + )?.[1] return { ...appProps, languagesContext, - stagingName: stagingNames.has(stagingName) ? stagingName : undefined, + stagingName: stagingName && stagingNames.has(stagingName) ? stagingName : undefined, } } diff --git a/src/ghes-releases/scripts/create-enterprise-issue.ts b/src/ghes-releases/scripts/create-enterprise-issue.ts index 73eda33257..5490543bd7 100644 --- a/src/ghes-releases/scripts/create-enterprise-issue.ts +++ b/src/ghes-releases/scripts/create-enterprise-issue.ts @@ -190,7 +190,7 @@ async function createIssue( body, labels, }) - } catch (error: any) { + } catch (error: unknown) { console.log(`#ERROR# ${error}\nšŸ›‘ There was an error creating the issue.`) throw error } @@ -223,7 +223,7 @@ async function updateIssue( body, labels, }) - } catch (error: any) { + } catch (error: unknown) { console.log( `#ERROR# ${error}\nšŸ›‘ There was an error updating issue ${issueNumber} in ${fullRepo}.`, ) @@ -244,8 +244,13 @@ async function addRepoLabels(fullRepo: string, labels: string[]) { repo, name, }) - } catch (error: any) { - if (error.status === 404) { + } catch (error: unknown) { + if ( + typeof error === 'object' && + error !== null && + 'status' in error && + (error as { status: number }).status === 404 + ) { labelsToAdd.push(name) } else { console.log(`#ERROR# ${error}\nšŸ›‘ There was an error getting the label ${name}.`) @@ -260,7 +265,7 @@ async function addRepoLabels(fullRepo: string, labels: string[]) { repo, name, }) - } catch (error: any) { + } catch (error: unknown) { console.log(`#ERROR# ${error}\nšŸ›‘ There was an error adding the label ${name}.`) throw error } diff --git a/src/observability/tests/logger-integration.ts b/src/observability/tests/logger-integration.ts index 8ea2f7a221..038eb2b943 100644 --- a/src/observability/tests/logger-integration.ts +++ b/src/observability/tests/logger-integration.ts @@ -1,4 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import type { Request, Response } from 'express' import { createLogger } from '@/observability/logger' import { initLoggerContext, updateLoggerContext } from '@/observability/logger/lib/logger-context' @@ -8,7 +9,7 @@ describe('logger integration tests', () => { let originalConsoleError: typeof console.error let originalEnv: typeof process.env const consoleLogs: string[] = [] - const consoleErrors: any[] = [] + const consoleErrors: unknown[] = [] beforeEach(() => { // Store original console methods and environment @@ -20,7 +21,7 @@ describe('logger integration tests', () => { console.log = vi.fn((message: string) => { consoleLogs.push(message) }) - console.error = vi.fn((error: any) => { + console.error = vi.fn((error: unknown) => { consoleErrors.push(error) }) @@ -78,9 +79,9 @@ describe('logger integration tests', () => { 'accept-language': 'en-US,en;q=0.9', }, query: { filter: 'active' }, - } as any + } as unknown as Request - const mockRes = {} as any + const mockRes = {} as unknown as Response // Use a Promise to handle the async local storage execution const result = await new Promise((resolve, reject) => { diff --git a/src/release-notes/pages/release-notes.tsx b/src/release-notes/pages/release-notes.tsx index 7fc7cd70be..949476c357 100644 --- a/src/release-notes/pages/release-notes.tsx +++ b/src/release-notes/pages/release-notes.tsx @@ -1,6 +1,8 @@ import { GetServerSideProps } from 'next' import { Liquid } from 'liquidjs' import pick from 'lodash/pick' +import get from 'lodash/get' +import type { Response } from 'express' import { MainContextT, @@ -11,6 +13,7 @@ import { import { DefaultLayout } from '@/frame/components/DefaultLayout' import { GHESReleaseNotes } from '@/release-notes/components/GHESReleaseNotes' import { GHESReleaseNotesContextT } from '@/release-notes/components/types' +import type { ExtendedRequest } from '@/types' const liquid = new Liquid() type Props = { @@ -33,22 +36,30 @@ export default function ReleaseNotes({ mainContext, ghesContext }: Props) { ) } -export const getServerSideProps: GetServerSideProps = async (context) => { - const req = context.req as any - const res = context.res as any +export const getServerSideProps: GetServerSideProps = async ( + context, +): Promise<{ props: Props }> => { + const req = context.req as unknown as ExtendedRequest + const res = context.res as unknown as Response // The `req.context.allVersion[X]` entries contains more keys (and values) // than we need so only pick out the keys that are actually needed // explicitly in the components served from these props. - const currentVersion = pick(req.context.allVersions[req.context.currentVersion], [ + const currentVersion = pick(req.context!.allVersions?.[req.context!.currentVersion!] || {}, [ 'plan', 'planTitle', 'versionTitle', 'currentRelease', 'releases', - ]) + ]) as { + plan?: string + planTitle?: string + versionTitle?: string + currentRelease?: string + releases?: string[] + } - const { latestPatch = '', latestRelease = '' } = req.context + const { latestPatch = '', latestRelease = '' } = req.context! const mainContext = await getMainContext(req, res) addUINamespaces(req, mainContext.data.ui, ['release_notes']) @@ -58,28 +69,39 @@ export const getServerSideProps: GetServerSideProps = async (context) => mainContext, ghesContext: currentVersion.plan === 'enterprise-server' - ? { + ? ({ currentVersion, latestPatch, latestRelease, - releaseNotes: req.context.ghesReleaseNotes, - releases: req.context.ghesReleases, + releaseNotes: req.context!.ghesReleaseNotes || [], + releases: req.context!.ghesReleases || [], message: { ghes_release_notes_upgrade_patch_only: liquid.parseAndRenderSync( - req.context.site.data.ui.header.notices.ghes_release_notes_upgrade_patch_only, + get( + req.context, + 'site.data.ui.header.notices.ghes_release_notes_upgrade_patch_only', + '', + ) as string, { latestPatch, latestRelease }, ), ghes_release_notes_upgrade_release_only: liquid.parseAndRenderSync( - req.context.site.data.ui.header.notices.ghes_release_notes_upgrade_release_only, + get( + req.context, + 'site.data.ui.header.notices.ghes_release_notes_upgrade_release_only', + '', + ) as string, { latestPatch, latestRelease }, ), ghes_release_notes_upgrade_patch_and_release: liquid.parseAndRenderSync( - req.context.site.data.ui.header.notices - .ghes_release_notes_upgrade_patch_and_release, + get( + req.context, + 'site.data.ui.header.notices.ghes_release_notes_upgrade_patch_and_release', + '', + ) as string, { latestPatch, latestRelease }, ), }, - } + } as unknown as GHESReleaseNotesContextT) : null, }, } diff --git a/src/search/scripts/scrape/lib/build-records.ts b/src/search/scripts/scrape/lib/build-records.ts index 59b0560fd5..d5a3b0336b 100644 --- a/src/search/scripts/scrape/lib/build-records.ts +++ b/src/search/scripts/scrape/lib/build-records.ts @@ -130,12 +130,15 @@ export default async function buildRecords( }) .on('error', (err) => { // Track the failure - const url = (err as any).url - const relativePath = (err as any).relativePath + const url = (err as unknown as { url?: string }).url + const relativePath = (err as unknown as { relativePath?: string }).relativePath // Check for HTTPError by name since it may come from a different module - if ((err instanceof HTTPError || err?.name === 'HTTPError') && (err as any).response) { - const httpErr = err as any + if ( + (err instanceof HTTPError || err?.name === 'HTTPError') && + (err as unknown as HTTPError).response + ) { + const httpErr = err as unknown as HTTPError failedPages.push({ url: httpErr.request?.requestUrl?.pathname || url, relativePath, @@ -146,7 +149,7 @@ export default async function buildRecords( if (!noMarkers) process.stdout.write(chalk.red('āœ—')) } else if (err instanceof Error) { // Enhanced error handling for timeout and network errors - const errorType = (err.cause as any)?.code || err.name + const errorType = (err.cause as unknown as { code?: string })?.code || err.name const isTimeout = errorType === 'UND_ERR_HEADERS_TIMEOUT' || errorType === 'UND_ERR_CONNECT_TIMEOUT' || diff --git a/src/versions/lib/get-applicable-versions.ts b/src/versions/lib/get-applicable-versions.ts index fdaf58e342..e814c1ab1b 100644 --- a/src/versions/lib/get-applicable-versions.ts +++ b/src/versions/lib/get-applicable-versions.ts @@ -14,8 +14,14 @@ interface GetApplicableVersionsOptions { includeNextVersion?: boolean } -// Using any for feature data as it's dynamically loaded from YAML files -let featureData: any = null +interface FeatureData { + [featureName: string]: { + versions: VersionsObject + } +} + +// Feature data is dynamically loaded from YAML files +let featureData: FeatureData | null = null const allVersionKeys = Object.keys(allVersions) @@ -55,13 +61,13 @@ function getApplicableVersions( ? {} : reduce( versionsObj, - (result: any, value, key) => { + (result: VersionsObject, value, key) => { if (key === 'feature') { if (typeof value === 'string') { - Object.assign(result, { ...featureData[value]?.versions }) + Object.assign(result, { ...featureData?.[value]?.versions }) } else if (Array.isArray(value)) { for (const str of value) { - Object.assign(result, { ...featureData[str].versions }) + Object.assign(result, { ...featureData?.[str]?.versions }) } } delete result[key] diff --git a/src/workflows/lib/in-liquid.ts b/src/workflows/lib/in-liquid.ts index d8350a0f75..a8648e4292 100644 --- a/src/workflows/lib/in-liquid.ts +++ b/src/workflows/lib/in-liquid.ts @@ -1,17 +1,27 @@ import { getLiquidTokens } from '@/content-linter/lib/helpers/liquid-utils' +import type { TagToken } from 'liquidjs' +import { TokenKind } from 'liquidjs' -type Token = { - name?: string - args?: string +// Type guard to check if a token is a TagToken +function isTagToken(token: unknown): token is TagToken { + return ( + token !== null && + typeof token === 'object' && + 'kind' in token && + token.kind === TokenKind.Tag && + 'name' in token && + typeof token.name === 'string' && + 'args' in token + ) } -const parsedLiquidTokensCache = new Map() +const parsedLiquidTokensCache = new Map() export function inLiquid(filePath: string, fileContents: string, needle: string) { if (!parsedLiquidTokensCache.has(filePath)) { - parsedLiquidTokensCache.set(filePath, getLiquidTokens(fileContents)) + parsedLiquidTokensCache.set(filePath, getLiquidTokens(fileContents).filter(isTagToken)) } - const tokens = parsedLiquidTokensCache.get(filePath) as Token[] + const tokens = parsedLiquidTokensCache.get(filePath)! for (const token of tokens) { if (token.name === 'data') { const { args } = token