core/analyzers/definition.analyzer.ts
core/analyzers/definition.analyzer.ts is a file in GovLab Patterns. 130 lines of code and 38 definitions.
import type { CodeFinding, CodeSymbol } from "#types/code.types";
import { DEAD_DETAIL, FINDING_KINDS, REMEDY, duplicateDetail } from "#configuration/strings/code.strings";
import { HIGH_SEVERITY, MEDIUM_SEVERITY, MODULE_SCOPE } from "#configuration/constants/report.constants";
import { ROLE_DEFINITION } from "#core/classifiers/syntax.classifier";
import { identifierNames } from "#core/analyzers/syntax.analyzer";
import { increment } from "#core/counters/base.counter";
const REL_DEAD = 9;
const REL_DUP = 9;
const REFERENCE_MIN = 2;
const MIN_DUP_FILES = 2;
const DUP_MAX_FILES = 4;
const MIN_DUP_SIZE = 160;
const CONFIDENCE = "high";
const GO_EXT = ".go";
const SHELL_EXT = ".sh";
const METHOD_KIND = "method";
const GENERATED_MARKER = ".generated.";
const GO_ENTRY: ReadonlySet<string> = new Set(["main", "init"]);
const SCHEMA_EXTS: readonly string[] = [".sql", ".graphql", ".gql", ".prisma"];
const TYPE_DECL_KINDS: ReadonlySet<string> = new Set(["interface_declaration", "type_alias_declaration"]);
const MALFORMED_CHARS: ReadonlySet<string> = new Set([" ", "{", "}", "(", ")", ",", ".", '"', "'", "[", "]"]);
const startsUpper = function startsUpper(name: string): boolean {
const first = name.charAt(0);
return first !== "" && first === first.toUpperCase() && first !== first.toLowerCase();
};
const isExported = function isExported(symbol: CodeSymbol): boolean {
return symbol.exported || (symbol.file.endsWith(GO_EXT) && (startsUpper(symbol.name) || GO_ENTRY.has(symbol.name)));
};
const isCleanName = function isCleanName(name: string): boolean {
for (const char of name) {
if (MALFORMED_CHARS.has(char)) {
return false;
}
}
return name.length > 0;
};
const isModuleDefinition = function isModuleDefinition(symbol: CodeSymbol): boolean {
return symbol.role === ROLE_DEFINITION && symbol.enclosing === MODULE_SCOPE;
};
const isNamedDefinition = function isNamedDefinition(symbol: CodeSymbol): boolean {
return isModuleDefinition(symbol) && isCleanName(symbol.name);
};
const isUnreferenced = function isUnreferenced(
symbol: CodeSymbol,
used: ReadonlySet<string>,
testUses: ReadonlySet<string>,
): boolean {
return !used.has(symbol.name) && !testUses.has(symbol.name);
};
const isHandWritten = function isHandWritten(symbol: CodeSymbol): boolean {
return !symbol.kind.includes(METHOD_KIND) && !symbol.file.includes(GENERATED_MARKER);
};
const isDiagnosableSource = function isDiagnosableSource(symbol: CodeSymbol): boolean {
return (
!symbol.file.endsWith(SHELL_EXT) &&
!SCHEMA_EXTS.some((ext) => symbol.file.endsWith(ext)) &&
!TYPE_DECL_KINDS.has(symbol.kind)
);
};
const usedNames = function usedNames(records: readonly Record<string, unknown>[]): Set<string> {
const counts = new Map<string, number>();
for (const name of identifierNames(records)) {
increment(counts, name);
}
return new Set([...counts].filter(([, count]) => count >= REFERENCE_MIN).map(([name]) => name));
};
const deadFinding = function deadFinding(def: CodeSymbol): CodeFinding {
return {
confidence: CONFIDENCE,
detail: DEAD_DETAIL,
file: def.file,
kind: FINDING_KINDS.deadCode,
line: def.line,
members: [],
name: def.name,
relevance: REL_DEAD,
remedy: REMEDY.get(FINDING_KINDS.deadCode) ?? "",
severity: MEDIUM_SEVERITY,
};
};
export const deadFindings = function deadFindings(
symbols: readonly CodeSymbol[],
records: readonly Record<string, unknown>[],
testUses: ReadonlySet<string>,
): CodeFinding[] {
const used = usedNames(records);
const dead = symbols
.filter((symbol) => isNamedDefinition(symbol) && isDiagnosableSource(symbol) && !isExported(symbol))
.filter((symbol) => isUnreferenced(symbol, used, testUses));
return [...new Map(dead.map((def) => [`${def.file}:${def.line}`, def])).values()].map(deadFinding);
};
const duplicateGroups = function duplicateGroups(name: string, defs: readonly CodeSymbol[]): CodeFinding[] {
const byHash = new Map<string, CodeSymbol[]>();
for (const def of defs) {
byHash.set(def.hash, [...(byHash.get(def.hash) ?? []), def]);
}
return [...byHash.values()].flatMap((group): CodeFinding[] => {
const files = [...new Set(group.map((def) => def.file))].sort((a, b) => a.localeCompare(b));
const [first] = group;
const qualifies =
files.length >= MIN_DUP_FILES && files.length <= DUP_MAX_FILES && (first?.size ?? 0) >= MIN_DUP_SIZE;
if (!qualifies || first === undefined) {
return [];
}
return [
{
confidence: CONFIDENCE,
detail: duplicateDetail(files.length, files.join(", ")),
file: first.file,
kind: FINDING_KINDS.duplicate,
line: first.line,
members: files,
name,
relevance: REL_DUP,
remedy: REMEDY.get(FINDING_KINDS.duplicate) ?? "",
severity: HIGH_SEVERITY,
},
];
});
};
export const duplicateFindings = function duplicateFindings(symbols: readonly CodeSymbol[]): CodeFinding[] {
const byName = new Map<string, CodeSymbol[]>();
const exported = symbols.filter(
(symbol) => isNamedDefinition(symbol) && isExported(symbol) && isHandWritten(symbol),
);
for (const def of exported) {
byName.set(def.name, [...(byName.get(def.name) ?? []), def]);
}
return [...byName].flatMap(([name, defs]) => duplicateGroups(name, defs));
};