code-gauge 10.1.0 → 10.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +33 -14
- package/THIRD-PARTY-NOTICES.txt +8 -8
- package/dist/checkCommand.cjs +1 -1
- package/dist/checkCommand.cjs.map +1 -1
- package/dist/checkCommand.js +1 -1
- package/dist/checkCommand.js.map +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +3 -2
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.d.ts +6 -4
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.d.ts +22 -7
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/thresholdCheck.cjs +1 -1
- package/dist/thresholdCheck.cjs.map +1 -1
- package/dist/thresholdCheck.d.ts +2 -0
- package/dist/thresholdCheck.js +1 -1
- package/dist/thresholdCheck.js.map +1 -1
- package/dist/thresholds.cjs.map +1 -1
- package/dist/thresholds.d.ts +2 -3
- package/dist/thresholds.js.map +1 -1
- package/dist/types.d.ts +19 -8
- package/native/Cargo.lock +16 -16
- package/native/Cargo.toml +8 -8
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +10 -1
- package/native/src/container.rs +158 -7
- package/native/src/dep_degree.rs +13 -14
- package/native/src/duplication.rs +188 -75
- package/native/src/functions.rs +112 -24
- package/native/src/languages.rs +5 -0
- package/native/src/measure.rs +9 -4
- package/native/src/near_miss.rs +303 -52
- package/package.json +8 -8
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thresholdCheck.js","names":[],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport type {\n CrossFileDuplicateBlockGroup,\n CrossFileDuplicateOccurrence,\n CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\n/** The duplication of the measured files, in which duplicated blocks and their copies are found. */\nexport interface CheckedDuplication {\n crossFile?: CrossFileDuplicationMetrics;\n /** The detection's `minSimilarityPercent`, the share of a block a copy of it holds at least. */\n minSimilarityPercent: number;\n}\n\nexport interface ExceededLimit {\n metric: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\n/** One copy of a clone group. */\ninterface CloneOccurrence extends BlockLocation {\n /**\n * Its code lines matched in another copy, whose number is the length of a duplicated block. Its\n * line span would also count the lines between the matched parts of one copy, which the copies\n * do not share; a block matched as a whole by a near-miss copy holds all its code lines.\n */\n lineNumbers: readonly number[];\n tokenCount: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most matched tokens first, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst cognitiveComplexityMetric = 'functionCognitiveComplexity';\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n duplication: CheckedDuplication,\n limitsOf: (language: string) => LimitsByLevel\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(duplication.crossFile, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, duplication)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [{ metric: metricNameOf(threshold), value, level, limit: limits[level][threshold.key] as number }]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that hold at least the duplicated lines of a level (with `hunks`, only those\n * overlapping added lines), with overlapping occurrences merged into one region.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n { minSimilarityPercent }: CheckedDuplication\n): Violation[] {\n const findExceededLines = (duplicatedLineCount: number): ExceededLimit[] =>\n findExceeded(duplicationThreshold, { duplicatedLineCount }, limits, (value, limit) => value >= limit);\n const groups: CloneOccurrence[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences.map((occurrence) => toCloneOccurrence(occurrence))),\n ];\n const isReported = (block: CloneOccurrence): boolean =>\n block.file === file &&\n findExceededLines(block.lineNumbers.length).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const groupByBlock = new Map<CloneOccurrence, CloneOccurrence[]>();\n for (const group of groups) {\n for (const block of group.filter((occurrence) => isReported(occurrence))) {\n groupByBlock.set(block, group);\n }\n }\n return mergeOverlapping([...groupByBlock.keys()]).map(({ merged, sources }) => {\n const listCopies = (minTokenPercent: number): BlockLocation[] =>\n mergeOverlapping(\n sources.flatMap((source) => {\n // The region merges every clone that overlaps it, so a copy of an occurrence lying\n // within a larger one is measured against the larger one.\n const tokenCount = Math.max(\n ...sources.filter((outer) => contains(outer, source)).map((outer) => outer.tokenCount)\n );\n return (groupByBlock.get(source) ?? []).filter(\n (copy) => copy.tokenCount * 100 >= tokenCount * minTokenPercent\n );\n })\n )\n // The region's own occurrences are among the copies of the groups it belongs to.\n .filter((partner) => !overlaps(partner.merged, merged))\n // The largest say the most about what to share.\n .toSorted((left, right) => sumTokens(right.sources) - sumTokens(left.sources))\n .map((partner) => partner.merged);\n // A group joins a block with every place repeating a part of it: a function another file\n // copies whole is grouped with each file holding a few of its lines. Such a fragment is not a\n // copy of the block: sharing it would not remove the block, and a report listing it reads as\n // one more copy. Sizes are compared in tokens, since the same code may be laid out over any\n // number of lines.\n const copies = listCopies(minSimilarityPercent);\n return {\n ...toViolation(\n merged,\n 'duplication',\n // Occurrences that overlap share lines, which count once.\n findExceededLines(new Set(sources.flatMap((source) => source.lineNumbers)).size)\n ),\n // A group does not always hold the copy a block was matched with as a whole: within a file,\n // a copy that already belongs to an exact group is listed as its part in that group. The\n // fragments then are all that locates the block's copies.\n partners: copies.length > 0 ? copies : listCopies(0),\n };\n });\n}\n\nfunction toCloneOccurrence(occurrence: CrossFileDuplicateOccurrence): CloneOccurrence {\n const { lineNumbers } = occurrence;\n assert.ok(lineNumbers, `No matched lines for ${occurrence.file}, a scanned file.`);\n return { ...occurrence, lineNumbers };\n}\n\nfunction sumTokens(occurrences: readonly CloneOccurrence[]): number {\n return occurrences.reduce((sum, occurrence) => sum + occurrence.tokenCount, 0);\n}\n\n/** The locations with those overlapping in the same file merged, ordered by file, then line. */\nfunction mergeOverlapping(\n locations: readonly CloneOccurrence[]\n): { merged: BlockLocation; sources: CloneOccurrence[] }[] {\n const regions: { merged: BlockLocation; sources: CloneOccurrence[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n if (last && overlaps(last.merged, location)) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\n/** Whether `outer` spans every line of `inner`; a block contains itself. */\nfunction contains(outer: BlockLocation, inner: BlockLocation): boolean {\n return outer.file === inner.file && outer.startLine <= inner.startLine && inner.endLine <= outer.endLine;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"thresholdCheck.js","names":[],"sources":["../src/thresholdCheck.ts"],"sourcesContent":["import assert from 'node:assert';\nimport type {\n CrossFileDuplicateBlockGroup,\n CrossFileDuplicateOccurrence,\n CrossFileDuplicationMetrics,\n} from './crossFileDuplication.js';\nimport type { LineHunk } from './git.js';\nimport {\n duplicationThreshold,\n fileThresholds,\n functionThresholds,\n levels,\n metricNameOf,\n type Level,\n type LimitsByLevel,\n type Threshold,\n} from './thresholds.js';\nimport type { CodeMetrics, CognitiveBlock, FunctionMetrics } from './types.js';\n\n/** A measured file to check, under the path cross-file duplication knows it by. */\nexport interface CheckedFile {\n file: string;\n metrics: CodeMetrics;\n /** The change to the file; when set, only what the change touches is checked. */\n hunks?: readonly LineHunk[];\n}\n\n/** The duplication of the measured files, in which duplicated blocks and their copies are found. */\nexport interface CheckedDuplication {\n crossFile?: CrossFileDuplicationMetrics;\n /** The detection's `minSimilarityPercent`, the share of a block a copy of it holds at least. */\n minSimilarityPercent: number;\n}\n\nexport interface ExceededLimit {\n metric: string;\n /** What reports call the metric (`cognitive complexity`, `duplicated lines`). */\n label: string;\n value: number;\n /** The most severe level whose limit the value violates. */\n level: Level;\n /** That level's limit: the value is allowed up to it; a duplicated block is allowed below it. */\n limit: number;\n}\n\nexport interface BlockLocation {\n file: string;\n startLine: number;\n endLine: number;\n}\n\n/** One copy of a clone group. */\ninterface CloneOccurrence extends BlockLocation {\n /**\n * Its code lines matched in another copy, whose number is the length of a duplicated block. Its\n * line span would also count the lines the copies do not share.\n */\n lineNumbers: readonly number[];\n tokenCount: number;\n}\n\nexport interface Violation extends BlockLocation {\n kind: 'function' | 'file' | 'duplication';\n /** The most severe level among `exceeded`. */\n level: Level;\n /** The function's name, qualified with the class it is a member of, for kind `function`. */\n name?: string;\n exceeded: ExceededLimit[];\n /**\n * The parts adding the most to the cognitive complexity, largest first, for a function that\n * exceeds a limit of it and has such parts: operator sequences and jumps outside any branching\n * construct add to the complexity without being one.\n */\n largestBlocks?: CognitiveBlock[];\n /** The other copies of the block, most matched tokens first, for kind `duplication`. */\n partners?: BlockLocation[];\n}\n\nexport interface CheckResult {\n violations: Violation[];\n checkedFunctionCount: number;\n}\n\nconst violationKinds: readonly Violation['kind'][] = ['file', 'function', 'duplication'];\n\nconst cognitiveComplexityMetric = 'functionCognitiveComplexity';\n\nconst nestedInclusiveMetrics = new Set(\n functionThresholds\n .filter(({ includesNestedFunctions }) => includesNestedFunctions)\n .map((threshold) => metricNameOf(threshold))\n);\n\n/** Every limit the files exceed, errors first, then ordered by path, then line. */\nexport function checkThresholds(\n files: readonly CheckedFile[],\n duplication: CheckedDuplication,\n limitsOf: (language: string) => LimitsByLevel\n): CheckResult {\n const crossFileGroupsByFile = indexGroupsByFile(duplication.crossFile, new Set(files.map(({ file }) => file)));\n const violations: Violation[] = [];\n let checkedFunctionCount = 0;\n for (const checkedFile of files) {\n const { file, metrics, hunks } = checkedFile;\n const limits = limitsOf(metrics.language);\n // A rename or a mode change lists the file as changed without touching any of its lines.\n const fileExceeded = hunks?.length === 0 ? [] : collectExceeded(fileThresholds, metrics, limits);\n if (fileExceeded.length > 0) {\n violations.push(toViolation({ file, startLine: 1, endLine: metrics.lines.total }, 'file', fileExceeded));\n }\n const checkedFunctions = listCheckedFunctions(checkedFile);\n checkedFunctionCount += checkedFunctions.length;\n const exceedingFunctions = checkedFunctions\n .map((fn) => ({ fn, exceeded: collectExceeded(functionThresholds, fn, limits) }))\n .filter(({ exceeded }) => exceeded.length > 0);\n for (const { fn, exceeded } of exceedingFunctions) {\n const ownExceeded = exceeded.filter((limit) => !isReportedByEnclosingFunction(limit, fn, exceedingFunctions));\n if (ownExceeded.length > 0) {\n violations.push(toFunctionViolation(file, fn, ownExceeded));\n }\n }\n violations.push(\n ...collectDuplicationViolations(checkedFile, crossFileGroupsByFile.get(file) ?? [], limits, duplication)\n );\n }\n return { violations: violations.toSorted(compareViolations), checkedFunctionCount };\n}\n\n// The kind is compared before the end line because a file violation's end line is corrected after\n// this ordering (to the last line that exists), and the order must not depend on that correction.\nfunction compareViolations(left: Violation, right: Violation): number {\n return (\n levels.indexOf(right.level) - levels.indexOf(left.level) ||\n compareStrings(left.file, right.file) ||\n left.startLine - right.startLine ||\n violationKinds.indexOf(left.kind) - violationKinds.indexOf(right.kind) ||\n left.endLine - right.endLine\n );\n}\n\n/** The file's functions or, for a changed file, those the change touches. */\nfunction listCheckedFunctions({ metrics, hunks }: CheckedFile): FunctionMetrics[] {\n const endsByIndentation = metrics.language === 'python';\n return hunks\n ? metrics.functions.filter((fn) =>\n hunks.some((hunk) => touchesSpan(hunk, fn.startLine, fn.endLine, endsByIndentation))\n )\n : metrics.functions;\n}\n\n/**\n * Whether a function enclosing `fn` exceeds the same limit with a value that covers `fn`, so\n * reporting it for `fn` too would count the same code twice. The enclosing value is at least that\n * of `fn` and both share the file's limits, so its level is never milder.\n */\nfunction isReportedByEnclosingFunction(\n limit: ExceededLimit,\n fn: FunctionMetrics,\n exceedingFunctions: readonly { fn: FunctionMetrics; exceeded: ExceededLimit[] }[]\n): boolean {\n if (!nestedInclusiveMetrics.has(limit.metric)) {\n return false;\n }\n return exceedingFunctions.some(\n (outer) => outer.fn !== fn && encloses(outer.fn, fn) && outer.exceeded.some(({ metric }) => metric === limit.metric)\n );\n}\n\nfunction encloses(outer: FunctionMetrics, inner: FunctionMetrics): boolean {\n const startsBefore =\n outer.startLine < inner.startLine ||\n (outer.startLine === inner.startLine && outer.startColumn <= inner.startColumn);\n const endsAfter =\n outer.endLine > inner.endLine || (outer.endLine === inner.endLine && outer.endColumn >= inner.endColumn);\n return startsBefore && endsAfter;\n}\n\nfunction toFunctionViolation(file: string, fn: FunctionMetrics, exceeded: ExceededLimit[]): Violation {\n const { startLine, endLine } = fn;\n const exceedsCognitiveComplexity = exceeded.some(({ metric }) => metric === cognitiveComplexityMetric);\n return {\n ...toViolation({ file, startLine, endLine }, 'function', exceeded),\n name: qualifyFunctionName(fn),\n ...(exceedsCognitiveComplexity && fn.cognitiveBlocks.length > 0 && { largestBlocks: fn.cognitiveBlocks }),\n };\n}\n\n/** How reports name a function: with the type or module it is a member of, so that it can be found. */\nexport function qualifyFunctionName({ name = '<anonymous>', containerName }: FunctionMetrics): string {\n return containerName ? `${containerName}.${name}` : name;\n}\n\nfunction toViolation(location: BlockLocation, kind: Violation['kind'], exceeded: ExceededLimit[]): Violation {\n const level = exceeded.some((limit) => limit.level === 'error') ? 'error' : 'warning';\n return { kind, level, ...location, exceeded };\n}\n\nfunction collectExceeded<Subject>(\n thresholds: readonly Threshold<Subject>[],\n subject: Subject,\n limits: LimitsByLevel\n): ExceededLimit[] {\n return thresholds.flatMap((threshold) => findExceeded(threshold, subject, limits, (value, limit) => value > limit));\n}\n\n/** The most severe violated limit of the threshold, if any. */\nfunction findExceeded<Subject>(\n threshold: Threshold<Subject>,\n subject: Subject,\n limits: LimitsByLevel,\n violates: (value: number, limit: number) => boolean\n): ExceededLimit[] {\n const value = threshold.measure(subject);\n const level = levels.findLast((candidate) => violates(value, limits[candidate][threshold.key] as number));\n return level\n ? [\n {\n metric: metricNameOf(threshold),\n label: threshold.label,\n value,\n level,\n limit: limits[level][threshold.key] as number,\n },\n ]\n : [];\n}\n\n/**\n * Whether the hunk changes the head lines [startLine, endLine]. A hunk without head lines (a pure\n * deletion) sits between its headStart line and the next one. Where indentation ends a span, its\n * last line is a statement rather than a closing delimiter, so a deletion right after that line may\n * have removed the span's tail; `endsByIndentation` counts it as touching, since the head alone\n * cannot tell it from a deletion after the span.\n */\nfunction touchesSpan(hunk: LineHunk, startLine: number, endLine: number, endsByIndentation = false): boolean {\n return hunk.headCount === 0\n ? hunk.headStart >= startLine && (hunk.headStart < endLine || (endsByIndentation && hunk.headStart === endLine))\n : hunk.headStart <= endLine && hunk.headStart + hunk.headCount > startLine;\n}\n\n/** The cross-file groups with an occurrence in each of the given files. */\nfunction indexGroupsByFile(\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n files: ReadonlySet<string>\n): Map<string, CrossFileDuplicateBlockGroup[]> {\n const groupsByFile = new Map<string, CrossFileDuplicateBlockGroup[]>();\n for (const group of crossFileDuplication?.groups ?? []) {\n for (const file of group.files) {\n if (files.has(file)) {\n const groups = groupsByFile.get(file) ?? [];\n groups.push(group);\n groupsByFile.set(file, groups);\n }\n }\n }\n return groupsByFile;\n}\n\n/**\n * One violation per duplicated region of the file: its clone occurrences, within-file and\n * cross-file, that hold at least the duplicated lines of a level (with `hunks`, only those\n * overlapping added lines), with overlapping occurrences merged into one region unless they are\n * copies of each other.\n */\nfunction collectDuplicationViolations(\n { file, metrics, hunks }: CheckedFile,\n crossFileGroups: readonly CrossFileDuplicateBlockGroup[],\n limits: LimitsByLevel,\n { minSimilarityPercent }: CheckedDuplication\n): Violation[] {\n const findExceededLines = (duplicatedLineCount: number): ExceededLimit[] =>\n findExceeded(duplicationThreshold, { duplicatedLineCount }, limits, (value, limit) => value >= limit);\n const groups: CloneOccurrence[][] = [\n ...metrics.duplication.duplicateBlockGroups.map((group) => group.map((occurrence) => ({ ...occurrence, file }))),\n ...crossFileGroups.map((group) => group.occurrences.map((occurrence) => toCloneOccurrence(occurrence))),\n ];\n const isReported = (block: CloneOccurrence): boolean =>\n block.file === file &&\n findExceededLines(block.lineNumbers.length).length > 0 &&\n (!hunks || hunks.some((hunk) => hunk.headCount > 0 && touchesSpan(hunk, block.startLine, block.endLine)));\n const groupOf = new Map(groups.flatMap((group) => group.map((occurrence) => [occurrence, group] as const)));\n // Copies of one group that share a line (the branches of an `else if` chain, the entries of a\n // list) stay apart: merged, they would be one block without a copy. One lying within another is\n // a repetition inside that block, not a block beside it.\n const areCopies = (left: CloneOccurrence, right: CloneOccurrence): boolean =>\n groupOf.get(left) === groupOf.get(right) && !contains(left, right) && !contains(right, left);\n const reportedBlocks = groups.flat().filter((occurrence) => isReported(occurrence));\n return mergeOverlapping(reportedBlocks, areCopies).map(({ merged, sources }) => {\n const listCopies = (minTokenPercent: number): BlockLocation[] =>\n mergeOverlapping(\n sources.flatMap((source) => {\n // The region merges every clone that overlaps it, so a copy of an occurrence lying\n // within a larger one is measured against the larger one.\n const tokenCount = Math.max(\n ...sources.filter((outer) => contains(outer, source)).map((outer) => outer.tokenCount)\n );\n return (groupOf.get(source) ?? []).filter(\n (copy) =>\n copy.tokenCount * 100 >= tokenCount * minTokenPercent &&\n // The region's own occurrences are among the copies of the groups it belongs to, and\n // so may be an occurrence within one of them that is not reported itself: a\n // repetition inside the block, not another place.\n !sources.some((outer) => contains(outer, copy))\n );\n }),\n areCopies\n )\n // The largest say the most about what to share.\n .toSorted((left, right) => sumTokens(right.sources) - sumTokens(left.sources))\n .map((partner) => partner.merged);\n // A group joins a block with every place repeating a part of it: a function another file\n // copies whole is grouped with each file holding a few of its lines. Such a fragment is not a\n // copy of the block: sharing it would not remove the block, and a report listing it reads as\n // one more copy. Sizes are compared in tokens, since the same code may be laid out over any\n // number of lines.\n const copies = listCopies(minSimilarityPercent);\n return {\n ...toViolation(\n merged,\n 'duplication',\n // Occurrences that overlap share lines, which count once.\n findExceededLines(new Set(sources.flatMap((source) => source.lineNumbers)).size)\n ),\n // A block whose parts were matched with different places has no copy of its size. The\n // places then are all that locates what it repeats.\n partners: copies.length > 0 ? copies : listCopies(0),\n };\n });\n}\n\nfunction toCloneOccurrence(occurrence: CrossFileDuplicateOccurrence): CloneOccurrence {\n const { lineNumbers } = occurrence;\n assert.ok(lineNumbers, `No matched lines for ${occurrence.file}, a scanned file.`);\n return { ...occurrence, lineNumbers };\n}\n\nfunction sumTokens(occurrences: readonly CloneOccurrence[]): number {\n return occurrences.reduce((sum, occurrence) => sum + occurrence.tokenCount, 0);\n}\n\n/**\n * The locations with those overlapping in the same file merged, except those `areCopies` holds\n * for, ordered by file, then line.\n */\nfunction mergeOverlapping(\n locations: readonly CloneOccurrence[],\n areCopies: (left: CloneOccurrence, right: CloneOccurrence) => boolean\n): { merged: BlockLocation; sources: CloneOccurrence[] }[] {\n const regions: { merged: BlockLocation; sources: CloneOccurrence[] }[] = [];\n const ordered = locations.toSorted(\n (left, right) => compareStrings(left.file, right.file) || left.startLine - right.startLine\n );\n for (const location of ordered) {\n const last = regions.at(-1);\n // A block containing the location holds it whatever it is a copy of.\n const joins =\n last &&\n overlaps(last.merged, location) &&\n (last.sources.some((source) => contains(source, location)) ||\n !last.sources.some((source) => areCopies(source, location)));\n if (joins) {\n last.merged.endLine = Math.max(last.merged.endLine, location.endLine);\n last.sources.push(location);\n } else {\n const { file, startLine, endLine } = location;\n regions.push({ merged: { file, startLine, endLine }, sources: [location] });\n }\n }\n return regions;\n}\n\n/** Whether `outer` spans every line of `inner`; a block contains itself. */\nfunction contains(outer: BlockLocation, inner: BlockLocation): boolean {\n return outer.file === inner.file && outer.startLine <= inner.startLine && inner.endLine <= outer.endLine;\n}\n\nfunction overlaps(left: BlockLocation, right: BlockLocation): boolean {\n return left.file === right.file && left.startLine <= right.endLine && right.startLine <= left.endLine;\n}\n\n/** Code-unit order, so reports do not depend on the locale. */\nfunction compareStrings(left: string, right: string): number {\n return left < right ? -1 : left > right ? 1 : 0;\n}\n"],"mappings":"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"}
|
package/dist/thresholds.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thresholds.cjs","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** The severities of a violation, mildest first. */\nexport const levels = ['warning', 'error'] as const;\nexport type Level = (typeof levels)[number];\n\n/**\n * One threshold of `code-gauge check`. Its `key` is the config setting, the per-language override\n * setting, and (kebab-cased after a level prefix) the command-line options, so a new entry here is\n * all a new thresholded metric needs.\n */\nexport interface Threshold<Subject = never> {\n key: string;\n /** How the report names the measured value. */\n label: string;\n /** `Infinity` leaves the level unchecked unless the config or the command line sets a limit. */\n defaultLimits: Readonly<Record<Level, number>>;\n /** What to change in the code when the limit is exceeded. */\n hint: string;\n measure: (subject: Subject) => number;\n /**\n * The value of a function is at least that of each function nested in it, whose code it counts\n * too. Not so for Halstead difficulty and effort: a ratio can be higher for the nested part alone.\n */\n includesNestedFunctions?: true;\n}\n\n/** Limits by threshold key; `Infinity` disables a limit. */\nexport type Limits = Readonly<Record<string, number>>;\nexport type LimitsByLevel = Readonly<Record<Level, Limits>>;\n\nexport const functionThresholds: readonly Threshold<FunctionMetrics>[] = [\n {\n key: 'maxFunctionCognitiveComplexity',\n label: 'cognitive complexity',\n defaultLimits: { warning: 15, error: 30 },\n hint: 'flatten nested branching with early returns and extract nested blocks into named functions.',\n measure: (fn) => fn.cognitiveComplexity,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionCyclomaticComplexity',\n label: 'cyclomatic complexity',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function has too many independent paths; split it by decision or replace condition chains with a lookup table.',\n measure: (fn) => fn.cyclomaticComplexity,\n },\n {\n key: 'maxFunctionNcss',\n label: 'NCSS',\n defaultLimits: { warning: 30, error: 60 },\n hint: 'move each group of statements that forms one step into a named function.',\n measure: (fn) => fn.ncss,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'replace nested conditions with guard clauses or move inner blocks into functions.',\n measure: (fn) => fn.nestingDepth,\n },\n {\n key: 'maxFunctionParameterCount',\n label: 'parameters',\n defaultLimits: { warning: 6, error: Infinity },\n hint: 'group related parameters into one object or split the function.',\n measure: (fn) => fn.parameterCount,\n },\n {\n key: 'maxFunctionHalsteadVolume',\n label: 'Halstead volume',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function holds too many operators and operands; move cohesive parts into helper functions.',\n measure: (fn) => fn.halstead.volume,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionHalsteadDifficulty',\n label: 'Halstead difficulty',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function reuses the same operands many times; split it so each part handles fewer values.',\n measure: (fn) => fn.halstead.difficulty,\n },\n {\n key: 'maxFunctionHalsteadEffort',\n label: 'Halstead effort',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function is both large and dense; split it into smaller functions with fewer values each.',\n measure: (fn) => fn.halstead.effort,\n },\n {\n key: 'maxFunctionDepDegree',\n label: 'DepDegree',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'too many values flow between the variables of the function; split it so each part works on fewer variables.',\n measure: (fn) => fn.depDegree,\n includesNestedFunctions: true,\n },\n];\n\nexport const fileThresholds: readonly Threshold<CodeMetrics>[] = [\n {\n key: 'maxFileNcss',\n label: 'file NCSS',\n defaultLimits: { warning: 400, error: 1000 },\n hint: 'split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/**\n * A duplicated block violates when it holds at least this many duplicated lines, its code lines\n * matched in another copy: comment-only and blank lines do not count, so a copy reads the same\n * length whether or not it is commented, and neither do the lines
|
|
1
|
+
{"version":3,"file":"thresholds.cjs","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** The severities of a violation, mildest first. */\nexport const levels = ['warning', 'error'] as const;\nexport type Level = (typeof levels)[number];\n\n/**\n * One threshold of `code-gauge check`. Its `key` is the config setting, the per-language override\n * setting, and (kebab-cased after a level prefix) the command-line options, so a new entry here is\n * all a new thresholded metric needs.\n */\nexport interface Threshold<Subject = never> {\n key: string;\n /** How the report names the measured value. */\n label: string;\n /** `Infinity` leaves the level unchecked unless the config or the command line sets a limit. */\n defaultLimits: Readonly<Record<Level, number>>;\n /** What to change in the code when the limit is exceeded. */\n hint: string;\n measure: (subject: Subject) => number;\n /**\n * The value of a function is at least that of each function nested in it, whose code it counts\n * too. Not so for Halstead difficulty and effort: a ratio can be higher for the nested part alone.\n */\n includesNestedFunctions?: true;\n}\n\n/** Limits by threshold key; `Infinity` disables a limit. */\nexport type Limits = Readonly<Record<string, number>>;\nexport type LimitsByLevel = Readonly<Record<Level, Limits>>;\n\nexport const functionThresholds: readonly Threshold<FunctionMetrics>[] = [\n {\n key: 'maxFunctionCognitiveComplexity',\n label: 'cognitive complexity',\n defaultLimits: { warning: 15, error: 30 },\n hint: 'flatten nested branching with early returns and extract nested blocks into named functions.',\n measure: (fn) => fn.cognitiveComplexity,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionCyclomaticComplexity',\n label: 'cyclomatic complexity',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function has too many independent paths; split it by decision or replace condition chains with a lookup table.',\n measure: (fn) => fn.cyclomaticComplexity,\n },\n {\n key: 'maxFunctionNcss',\n label: 'NCSS',\n defaultLimits: { warning: 30, error: 60 },\n hint: 'move each group of statements that forms one step into a named function.',\n measure: (fn) => fn.ncss,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'replace nested conditions with guard clauses or move inner blocks into functions.',\n measure: (fn) => fn.nestingDepth,\n },\n {\n key: 'maxFunctionParameterCount',\n label: 'parameters',\n defaultLimits: { warning: 6, error: Infinity },\n hint: 'group related parameters into one object or split the function.',\n measure: (fn) => fn.parameterCount,\n },\n {\n key: 'maxFunctionHalsteadVolume',\n label: 'Halstead volume',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function holds too many operators and operands; move cohesive parts into helper functions.',\n measure: (fn) => fn.halstead.volume,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionHalsteadDifficulty',\n label: 'Halstead difficulty',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function reuses the same operands many times; split it so each part handles fewer values.',\n measure: (fn) => fn.halstead.difficulty,\n },\n {\n key: 'maxFunctionHalsteadEffort',\n label: 'Halstead effort',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function is both large and dense; split it into smaller functions with fewer values each.',\n measure: (fn) => fn.halstead.effort,\n },\n {\n key: 'maxFunctionDepDegree',\n label: 'DepDegree',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'too many values flow between the variables of the function; split it so each part works on fewer variables.',\n measure: (fn) => fn.depDegree,\n includesNestedFunctions: true,\n },\n];\n\nexport const fileThresholds: readonly Threshold<CodeMetrics>[] = [\n {\n key: 'maxFileNcss',\n label: 'file NCSS',\n defaultLimits: { warning: 400, error: 1000 },\n hint: 'split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/**\n * A duplicated block violates when it holds at least this many duplicated lines, its code lines\n * matched in another copy: comment-only and blank lines do not count, so a copy reads the same\n * length whether or not it is commented, and neither do the lines the copies do not share, those\n * between the matched parts of an edited copy and those a near-miss copy adds or rewrites.\n */\nexport const duplicationThreshold: Threshold<{ duplicatedLineCount: number }> = {\n key: 'minDuplicateLines',\n label: 'duplicated lines',\n defaultLimits: { warning: 15, error: Infinity },\n hint: 'extract the repeated code into one shared function or module and call it from every location.',\n measure: (block) => block.duplicatedLineCount,\n};\n\nexport const thresholds: readonly Threshold[] = [...functionThresholds, ...fileThresholds, duplicationThreshold];\n\n/** The name a report gives a threshold's measured value: its key without the `max`/`min` prefix. */\nexport function metricNameOf(threshold: Threshold): string {\n const name = threshold.key.slice(3);\n return name.charAt(0).toLowerCase() + name.slice(1);\n}\n\n/** One level of the `thresholds` config section; a missing key falls back to the less specific layer. */\nexport interface LevelThresholdsConfig {\n limits: Limits;\n /** Overrides for the files of one language, keyed by language name. */\n languages: Readonly<Record<string, Limits>>;\n}\n\nexport type ThresholdsConfig = Readonly<Partial<Record<Level, LevelThresholdsConfig>>>;\n\n/**\n * The limits of each level applying to files of each language, with precedence command-line\n * options > the config's per-language overrides > the config's limits > built-in defaults.\n */\nexport function resolveLimits(\n cliLimits: LimitsByLevel,\n config: ThresholdsConfig | undefined\n): (language: string) => LimitsByLevel {\n const resolveLevel = (level: Level, language: string): Limits => {\n const levelConfig = config?.[level];\n return {\n ...Object.fromEntries(thresholds.map(({ key, defaultLimits }) => [key, defaultLimits[level]])),\n ...levelConfig?.limits,\n // Object.hasOwn: a language named like an Object.prototype member must not read an inherited value.\n ...(levelConfig && Object.hasOwn(levelConfig.languages, language) ? levelConfig.languages[language] : undefined),\n ...cliLimits[level],\n };\n };\n return (language) => ({ warning: resolveLevel('warning', language), error: resolveLevel('error', language) });\n}\n"],"mappings":"aAGA,MAAa,EAAS,CAAC,UAAW,OAAO,EA4B5B,EAA4D,CACvE,CACE,IAAK,iCACL,MAAO,uBACP,cAAe,CAAE,QAAS,GAAI,MAAO,EAAG,EACxC,KAAM,8FACN,QAAU,GAAO,EAAG,oBACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,kCACL,MAAO,wBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,qHACN,QAAU,GAAO,EAAG,oBACtB,EACA,CACE,IAAK,kBACL,MAAO,OACP,cAAe,CAAE,QAAS,GAAI,MAAO,EAAG,EACxC,KAAM,2EACN,QAAU,GAAO,EAAG,KACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,0BACL,MAAO,gBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,oFACN,QAAU,GAAO,EAAG,YACtB,EACA,CACE,IAAK,4BACL,MAAO,aACP,cAAe,CAAE,QAAS,EAAG,MAAO,GAAS,EAC7C,KAAM,kEACN,QAAU,GAAO,EAAG,cACtB,EACA,CACE,IAAK,4BACL,MAAO,kBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,iGACN,QAAU,GAAO,EAAG,SAAS,OAC7B,wBAAyB,EAC3B,EACA,CACE,IAAK,gCACL,MAAO,sBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,UAC/B,EACA,CACE,IAAK,4BACL,MAAO,kBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,MAC/B,EACA,CACE,IAAK,uBACL,MAAO,YACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,8GACN,QAAU,GAAO,EAAG,UACpB,wBAAyB,EAC3B,CACF,EAEa,EAAoD,CAC/D,CACE,IAAK,cACL,MAAO,YACP,cAAe,CAAE,QAAS,IAAK,MAAO,GAAK,EAC3C,KAAM,gKACN,QAAU,GAAS,EAAK,SAC1B,CACF,EAQa,EAAmE,CAC9E,IAAK,oBACL,MAAO,mBACP,cAAe,CAAE,QAAS,GAAI,MAAO,GAAS,EAC9C,KAAM,gGACN,QAAU,GAAU,EAAM,mBAC5B,EAEa,EAAmC,CAAC,GAAG,EAAoB,GAAG,EAAgB,CAAoB,EAG/G,SAAgB,EAAa,EAA8B,CACzD,IAAM,EAAO,EAAU,IAAI,MAAM,CAAC,EAClC,OAAO,EAAK,OAAO,CAAC,CAAC,CAAC,YAAY,EAAI,EAAK,MAAM,CAAC,CACpD,CAeA,SAAgB,EACd,EACA,EACqC,CACrC,IAAM,GAAgB,EAAc,IAA6B,CAC/D,IAAM,EAAc,IAAS,GAC7B,MAAO,CACL,GAAG,OAAO,YAAY,EAAW,KAAK,CAAE,MAAK,mBAAoB,CAAC,EAAK,EAAc,EAAM,CAAC,CAAC,EAC7F,GAAG,GAAa,OAEhB,GAAI,GAAe,OAAO,OAAO,EAAY,UAAW,CAAQ,EAAI,EAAY,UAAU,GAAY,IAAA,GACtG,GAAG,EAAU,EACf,CACF,EACA,MAAQ,KAAc,CAAE,QAAS,EAAa,UAAW,CAAQ,EAAG,MAAO,EAAa,QAAS,CAAQ,CAAE,EAC7G"}
|
package/dist/thresholds.d.ts
CHANGED
|
@@ -30,9 +30,8 @@ export declare const fileThresholds: readonly Threshold<CodeMetrics>[];
|
|
|
30
30
|
/**
|
|
31
31
|
* A duplicated block violates when it holds at least this many duplicated lines, its code lines
|
|
32
32
|
* matched in another copy: comment-only and blank lines do not count, so a copy reads the same
|
|
33
|
-
* length whether or not it is commented, and neither do the lines
|
|
34
|
-
*
|
|
35
|
-
* edited ones included.
|
|
33
|
+
* length whether or not it is commented, and neither do the lines the copies do not share, those
|
|
34
|
+
* between the matched parts of an edited copy and those a near-miss copy adds or rewrites.
|
|
36
35
|
*/
|
|
37
36
|
export declare const duplicationThreshold: Threshold<{
|
|
38
37
|
duplicatedLineCount: number;
|
package/dist/thresholds.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"thresholds.js","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** The severities of a violation, mildest first. */\nexport const levels = ['warning', 'error'] as const;\nexport type Level = (typeof levels)[number];\n\n/**\n * One threshold of `code-gauge check`. Its `key` is the config setting, the per-language override\n * setting, and (kebab-cased after a level prefix) the command-line options, so a new entry here is\n * all a new thresholded metric needs.\n */\nexport interface Threshold<Subject = never> {\n key: string;\n /** How the report names the measured value. */\n label: string;\n /** `Infinity` leaves the level unchecked unless the config or the command line sets a limit. */\n defaultLimits: Readonly<Record<Level, number>>;\n /** What to change in the code when the limit is exceeded. */\n hint: string;\n measure: (subject: Subject) => number;\n /**\n * The value of a function is at least that of each function nested in it, whose code it counts\n * too. Not so for Halstead difficulty and effort: a ratio can be higher for the nested part alone.\n */\n includesNestedFunctions?: true;\n}\n\n/** Limits by threshold key; `Infinity` disables a limit. */\nexport type Limits = Readonly<Record<string, number>>;\nexport type LimitsByLevel = Readonly<Record<Level, Limits>>;\n\nexport const functionThresholds: readonly Threshold<FunctionMetrics>[] = [\n {\n key: 'maxFunctionCognitiveComplexity',\n label: 'cognitive complexity',\n defaultLimits: { warning: 15, error: 30 },\n hint: 'flatten nested branching with early returns and extract nested blocks into named functions.',\n measure: (fn) => fn.cognitiveComplexity,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionCyclomaticComplexity',\n label: 'cyclomatic complexity',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function has too many independent paths; split it by decision or replace condition chains with a lookup table.',\n measure: (fn) => fn.cyclomaticComplexity,\n },\n {\n key: 'maxFunctionNcss',\n label: 'NCSS',\n defaultLimits: { warning: 30, error: 60 },\n hint: 'move each group of statements that forms one step into a named function.',\n measure: (fn) => fn.ncss,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'replace nested conditions with guard clauses or move inner blocks into functions.',\n measure: (fn) => fn.nestingDepth,\n },\n {\n key: 'maxFunctionParameterCount',\n label: 'parameters',\n defaultLimits: { warning: 6, error: Infinity },\n hint: 'group related parameters into one object or split the function.',\n measure: (fn) => fn.parameterCount,\n },\n {\n key: 'maxFunctionHalsteadVolume',\n label: 'Halstead volume',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function holds too many operators and operands; move cohesive parts into helper functions.',\n measure: (fn) => fn.halstead.volume,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionHalsteadDifficulty',\n label: 'Halstead difficulty',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function reuses the same operands many times; split it so each part handles fewer values.',\n measure: (fn) => fn.halstead.difficulty,\n },\n {\n key: 'maxFunctionHalsteadEffort',\n label: 'Halstead effort',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function is both large and dense; split it into smaller functions with fewer values each.',\n measure: (fn) => fn.halstead.effort,\n },\n {\n key: 'maxFunctionDepDegree',\n label: 'DepDegree',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'too many values flow between the variables of the function; split it so each part works on fewer variables.',\n measure: (fn) => fn.depDegree,\n includesNestedFunctions: true,\n },\n];\n\nexport const fileThresholds: readonly Threshold<CodeMetrics>[] = [\n {\n key: 'maxFileNcss',\n label: 'file NCSS',\n defaultLimits: { warning: 400, error: 1000 },\n hint: 'split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/**\n * A duplicated block violates when it holds at least this many duplicated lines, its code lines\n * matched in another copy: comment-only and blank lines do not count, so a copy reads the same\n * length whether or not it is commented, and neither do the lines
|
|
1
|
+
{"version":3,"file":"thresholds.js","names":[],"sources":["../src/thresholds.ts"],"sourcesContent":["import type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** The severities of a violation, mildest first. */\nexport const levels = ['warning', 'error'] as const;\nexport type Level = (typeof levels)[number];\n\n/**\n * One threshold of `code-gauge check`. Its `key` is the config setting, the per-language override\n * setting, and (kebab-cased after a level prefix) the command-line options, so a new entry here is\n * all a new thresholded metric needs.\n */\nexport interface Threshold<Subject = never> {\n key: string;\n /** How the report names the measured value. */\n label: string;\n /** `Infinity` leaves the level unchecked unless the config or the command line sets a limit. */\n defaultLimits: Readonly<Record<Level, number>>;\n /** What to change in the code when the limit is exceeded. */\n hint: string;\n measure: (subject: Subject) => number;\n /**\n * The value of a function is at least that of each function nested in it, whose code it counts\n * too. Not so for Halstead difficulty and effort: a ratio can be higher for the nested part alone.\n */\n includesNestedFunctions?: true;\n}\n\n/** Limits by threshold key; `Infinity` disables a limit. */\nexport type Limits = Readonly<Record<string, number>>;\nexport type LimitsByLevel = Readonly<Record<Level, Limits>>;\n\nexport const functionThresholds: readonly Threshold<FunctionMetrics>[] = [\n {\n key: 'maxFunctionCognitiveComplexity',\n label: 'cognitive complexity',\n defaultLimits: { warning: 15, error: 30 },\n hint: 'flatten nested branching with early returns and extract nested blocks into named functions.',\n measure: (fn) => fn.cognitiveComplexity,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionCyclomaticComplexity',\n label: 'cyclomatic complexity',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function has too many independent paths; split it by decision or replace condition chains with a lookup table.',\n measure: (fn) => fn.cyclomaticComplexity,\n },\n {\n key: 'maxFunctionNcss',\n label: 'NCSS',\n defaultLimits: { warning: 30, error: 60 },\n hint: 'move each group of statements that forms one step into a named function.',\n measure: (fn) => fn.ncss,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionNestingDepth',\n label: 'nesting depth',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'replace nested conditions with guard clauses or move inner blocks into functions.',\n measure: (fn) => fn.nestingDepth,\n },\n {\n key: 'maxFunctionParameterCount',\n label: 'parameters',\n defaultLimits: { warning: 6, error: Infinity },\n hint: 'group related parameters into one object or split the function.',\n measure: (fn) => fn.parameterCount,\n },\n {\n key: 'maxFunctionHalsteadVolume',\n label: 'Halstead volume',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function holds too many operators and operands; move cohesive parts into helper functions.',\n measure: (fn) => fn.halstead.volume,\n includesNestedFunctions: true,\n },\n {\n key: 'maxFunctionHalsteadDifficulty',\n label: 'Halstead difficulty',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function reuses the same operands many times; split it so each part handles fewer values.',\n measure: (fn) => fn.halstead.difficulty,\n },\n {\n key: 'maxFunctionHalsteadEffort',\n label: 'Halstead effort',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'the function is both large and dense; split it into smaller functions with fewer values each.',\n measure: (fn) => fn.halstead.effort,\n },\n {\n key: 'maxFunctionDepDegree',\n label: 'DepDegree',\n defaultLimits: { warning: Infinity, error: Infinity },\n hint: 'too many values flow between the variables of the function; split it so each part works on fewer variables.',\n measure: (fn) => fn.depDegree,\n includesNestedFunctions: true,\n },\n];\n\nexport const fileThresholds: readonly Threshold<CodeMetrics>[] = [\n {\n key: 'maxFileNcss',\n label: 'file NCSS',\n defaultLimits: { warning: 400, error: 1000 },\n hint: 'split the file into modules with one responsibility each, for example by moving a class or the helpers that serve only one function into a file of their own.',\n measure: (file) => file.ncssCount,\n },\n];\n\n/**\n * A duplicated block violates when it holds at least this many duplicated lines, its code lines\n * matched in another copy: comment-only and blank lines do not count, so a copy reads the same\n * length whether or not it is commented, and neither do the lines the copies do not share, those\n * between the matched parts of an edited copy and those a near-miss copy adds or rewrites.\n */\nexport const duplicationThreshold: Threshold<{ duplicatedLineCount: number }> = {\n key: 'minDuplicateLines',\n label: 'duplicated lines',\n defaultLimits: { warning: 15, error: Infinity },\n hint: 'extract the repeated code into one shared function or module and call it from every location.',\n measure: (block) => block.duplicatedLineCount,\n};\n\nexport const thresholds: readonly Threshold[] = [...functionThresholds, ...fileThresholds, duplicationThreshold];\n\n/** The name a report gives a threshold's measured value: its key without the `max`/`min` prefix. */\nexport function metricNameOf(threshold: Threshold): string {\n const name = threshold.key.slice(3);\n return name.charAt(0).toLowerCase() + name.slice(1);\n}\n\n/** One level of the `thresholds` config section; a missing key falls back to the less specific layer. */\nexport interface LevelThresholdsConfig {\n limits: Limits;\n /** Overrides for the files of one language, keyed by language name. */\n languages: Readonly<Record<string, Limits>>;\n}\n\nexport type ThresholdsConfig = Readonly<Partial<Record<Level, LevelThresholdsConfig>>>;\n\n/**\n * The limits of each level applying to files of each language, with precedence command-line\n * options > the config's per-language overrides > the config's limits > built-in defaults.\n */\nexport function resolveLimits(\n cliLimits: LimitsByLevel,\n config: ThresholdsConfig | undefined\n): (language: string) => LimitsByLevel {\n const resolveLevel = (level: Level, language: string): Limits => {\n const levelConfig = config?.[level];\n return {\n ...Object.fromEntries(thresholds.map(({ key, defaultLimits }) => [key, defaultLimits[level]])),\n ...levelConfig?.limits,\n // Object.hasOwn: a language named like an Object.prototype member must not read an inherited value.\n ...(levelConfig && Object.hasOwn(levelConfig.languages, language) ? levelConfig.languages[language] : undefined),\n ...cliLimits[level],\n };\n };\n return (language) => ({ warning: resolveLevel('warning', language), error: resolveLevel('error', language) });\n}\n"],"mappings":"AAGA,MAAa,EAAS,CAAC,UAAW,OAAO,EA4B5B,EAA4D,CACvE,CACE,IAAK,iCACL,MAAO,uBACP,cAAe,CAAE,QAAS,GAAI,MAAO,EAAG,EACxC,KAAM,8FACN,QAAU,GAAO,EAAG,oBACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,kCACL,MAAO,wBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,qHACN,QAAU,GAAO,EAAG,oBACtB,EACA,CACE,IAAK,kBACL,MAAO,OACP,cAAe,CAAE,QAAS,GAAI,MAAO,EAAG,EACxC,KAAM,2EACN,QAAU,GAAO,EAAG,KACpB,wBAAyB,EAC3B,EACA,CACE,IAAK,0BACL,MAAO,gBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,oFACN,QAAU,GAAO,EAAG,YACtB,EACA,CACE,IAAK,4BACL,MAAO,aACP,cAAe,CAAE,QAAS,EAAG,MAAO,GAAS,EAC7C,KAAM,kEACN,QAAU,GAAO,EAAG,cACtB,EACA,CACE,IAAK,4BACL,MAAO,kBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,iGACN,QAAU,GAAO,EAAG,SAAS,OAC7B,wBAAyB,EAC3B,EACA,CACE,IAAK,gCACL,MAAO,sBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,UAC/B,EACA,CACE,IAAK,4BACL,MAAO,kBACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,gGACN,QAAU,GAAO,EAAG,SAAS,MAC/B,EACA,CACE,IAAK,uBACL,MAAO,YACP,cAAe,CAAE,QAAS,IAAU,MAAO,GAAS,EACpD,KAAM,8GACN,QAAU,GAAO,EAAG,UACpB,wBAAyB,EAC3B,CACF,EAEa,EAAoD,CAC/D,CACE,IAAK,cACL,MAAO,YACP,cAAe,CAAE,QAAS,IAAK,MAAO,GAAK,EAC3C,KAAM,gKACN,QAAU,GAAS,EAAK,SAC1B,CACF,EAQa,EAAmE,CAC9E,IAAK,oBACL,MAAO,mBACP,cAAe,CAAE,QAAS,GAAI,MAAO,GAAS,EAC9C,KAAM,gGACN,QAAU,GAAU,EAAM,mBAC5B,EAEa,EAAmC,CAAC,GAAG,EAAoB,GAAG,EAAgB,CAAoB,EAG/G,SAAgB,EAAa,EAA8B,CACzD,IAAM,EAAO,EAAU,IAAI,MAAM,CAAC,EAClC,OAAO,EAAK,OAAO,CAAC,CAAC,CAAC,YAAY,EAAI,EAAK,MAAM,CAAC,CACpD,CAeA,SAAgB,EACd,EACA,EACqC,CACrC,IAAM,GAAgB,EAAc,IAA6B,CAC/D,IAAM,EAAc,IAAS,GAC7B,MAAO,CACL,GAAG,OAAO,YAAY,EAAW,KAAK,CAAE,MAAK,mBAAoB,CAAC,EAAK,EAAc,EAAM,CAAC,CAAC,EAC7F,GAAG,GAAa,OAEhB,GAAI,GAAe,OAAO,OAAO,EAAY,UAAW,CAAQ,EAAI,EAAY,UAAU,GAAY,IAAA,GACtG,GAAG,EAAU,EACf,CACF,EACA,MAAQ,KAAc,CAAE,QAAS,EAAa,UAAW,CAAQ,EAAG,MAAO,EAAa,QAAS,CAAQ,CAAE,EAC7G"}
|
package/dist/types.d.ts
CHANGED
|
@@ -78,11 +78,21 @@ export interface FunctionMetrics {
|
|
|
78
78
|
* find a like-named member of it (`void gauge::f()` inside `namespace gauge` holding a
|
|
79
79
|
* `struct gauge`).
|
|
80
80
|
*
|
|
81
|
-
*
|
|
82
|
-
*
|
|
83
|
-
*
|
|
84
|
-
*
|
|
85
|
-
*
|
|
81
|
+
* A function written as a value (a lambda, a closure, a function expression) is a member of
|
|
82
|
+
* what the declaration binding it is declared in, as a declared function would be: the class of
|
|
83
|
+
* a field it initializes, the declared namespace or module of a constant (a file is neither, so
|
|
84
|
+
* a constant at its top level has no owner). A member of a JavaScript or TypeScript object literal is reached through the path
|
|
85
|
+
* the object is bound to: `api` for `const api = { load() {} }`, `Rules.handlers` for the field
|
|
86
|
+
* of a class, `api.admin` for the value of a property, `module.exports` for an assignment.
|
|
87
|
+
*
|
|
88
|
+
* Absent for a function without a name, for a function expression with a name of its own (which
|
|
89
|
+
* is no member of what binds the expression), for one passed as an argument or assigned to a member,
|
|
90
|
+
* for a function written as a value inside another function or bound to a plain name in a Ruby
|
|
91
|
+
* class body (a local variable of that body), for a member of an anonymous class
|
|
92
|
+
* or of an object literal bound to no name, and for a function local to an initializer block. So
|
|
93
|
+
* a method of a class declared inside a function has it, as has a member of an object literal a
|
|
94
|
+
* function binds to a local name (`api` for `const api = { run() {} }`), and a closure inside a
|
|
95
|
+
* method does not.
|
|
86
96
|
*/
|
|
87
97
|
containerName?: string;
|
|
88
98
|
/**
|
|
@@ -157,8 +167,8 @@ export interface DuplicationMetrics {
|
|
|
157
167
|
/** Number of distinct lines covered by any counted duplicate occurrence (originals included). */
|
|
158
168
|
duplicateLineCount: number;
|
|
159
169
|
/**
|
|
160
|
-
* The 1-based lines behind duplicateLineCount, sorted ascending:
|
|
161
|
-
*
|
|
170
|
+
* The 1-based lines behind duplicateLineCount, sorted ascending: the union of the occurrences'
|
|
171
|
+
* `lineNumbers`. Exposed so consumers combining within-file and cross-file coverage can union exact
|
|
162
172
|
* line sets instead of over-counting from block bounding ranges (which include the unmatched gap
|
|
163
173
|
* of a merged clone and comment/blank lines).
|
|
164
174
|
*/
|
|
@@ -177,7 +187,8 @@ export interface DuplicateBlockOccurrence {
|
|
|
177
187
|
/**
|
|
178
188
|
* The 1-based code lines carrying the occurrence's matched tokens, sorted ascending: the lines
|
|
179
189
|
* from startLine to endLine without comment-only and blank lines, the unmatched gap of a merged
|
|
180
|
-
* clone,
|
|
190
|
+
* clone, what lies between the matched cores of a near-miss block, and the lines of a near-miss
|
|
191
|
+
* block or core no more than half of whose tokens its copies match.
|
|
181
192
|
*/
|
|
182
193
|
lineNumbers: number[];
|
|
183
194
|
startLine: number;
|
package/native/Cargo.lock
CHANGED
|
@@ -484,9 +484,9 @@ checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
|
|
484
484
|
|
|
485
485
|
[[package]]
|
|
486
486
|
name = "willbooster-tree-sitter"
|
|
487
|
-
version = "1.
|
|
487
|
+
version = "1.4.3"
|
|
488
488
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
489
|
-
checksum = "
|
|
489
|
+
checksum = "334d00dd58bc471ce142f80bd05c8dde5d6a5961d23527aa4b3ad640793550ef"
|
|
490
490
|
dependencies = [
|
|
491
491
|
"cc",
|
|
492
492
|
"regex",
|
|
@@ -497,9 +497,9 @@ dependencies = [
|
|
|
497
497
|
|
|
498
498
|
[[package]]
|
|
499
499
|
name = "willbooster-tree-sitter-c"
|
|
500
|
-
version = "
|
|
500
|
+
version = "4.0.2"
|
|
501
501
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
502
|
-
checksum = "
|
|
502
|
+
checksum = "5eb6af34735987dd02518fced2bd8f6a0b81aff19220290020787c1e12c19e7f"
|
|
503
503
|
dependencies = [
|
|
504
504
|
"cc",
|
|
505
505
|
"tree-sitter-language",
|
|
@@ -507,9 +507,9 @@ dependencies = [
|
|
|
507
507
|
|
|
508
508
|
[[package]]
|
|
509
509
|
name = "willbooster-tree-sitter-c-sharp"
|
|
510
|
-
version = "
|
|
510
|
+
version = "3.0.1"
|
|
511
511
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
512
|
-
checksum = "
|
|
512
|
+
checksum = "8c5aa3c4b1bfcdad92d0703c77964c8b805800d3ee8087dddae1d336da96e495"
|
|
513
513
|
dependencies = [
|
|
514
514
|
"cc",
|
|
515
515
|
"tree-sitter-language",
|
|
@@ -517,9 +517,9 @@ dependencies = [
|
|
|
517
517
|
|
|
518
518
|
[[package]]
|
|
519
519
|
name = "willbooster-tree-sitter-cpp"
|
|
520
|
-
version = "
|
|
520
|
+
version = "2.0.2"
|
|
521
521
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
522
|
-
checksum = "
|
|
522
|
+
checksum = "fff5f9f592d51e52f1701438c8ee8b91376e15f42584e4e01f4ecb1120b1f354"
|
|
523
523
|
dependencies = [
|
|
524
524
|
"cc",
|
|
525
525
|
"tree-sitter-language",
|
|
@@ -527,9 +527,9 @@ dependencies = [
|
|
|
527
527
|
|
|
528
528
|
[[package]]
|
|
529
529
|
name = "willbooster-tree-sitter-javascript"
|
|
530
|
-
version = "
|
|
530
|
+
version = "4.0.2"
|
|
531
531
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
532
|
-
checksum = "
|
|
532
|
+
checksum = "60a035c947e2036a42ef8060872a213dc76489b3a82f2e4730b6ed54bd10a7bb"
|
|
533
533
|
dependencies = [
|
|
534
534
|
"cc",
|
|
535
535
|
"tree-sitter-language",
|
|
@@ -537,9 +537,9 @@ dependencies = [
|
|
|
537
537
|
|
|
538
538
|
[[package]]
|
|
539
539
|
name = "willbooster-tree-sitter-kotlin"
|
|
540
|
-
version = "
|
|
540
|
+
version = "4.0.2"
|
|
541
541
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
542
|
-
checksum = "
|
|
542
|
+
checksum = "58fb49a0177bad0032df8ff897e5c6b66f3ed044e3133a81ebc1583303f6c7aa"
|
|
543
543
|
dependencies = [
|
|
544
544
|
"cc",
|
|
545
545
|
"tree-sitter-language",
|
|
@@ -547,9 +547,9 @@ dependencies = [
|
|
|
547
547
|
|
|
548
548
|
[[package]]
|
|
549
549
|
name = "willbooster-tree-sitter-rust"
|
|
550
|
-
version = "
|
|
550
|
+
version = "2.0.2"
|
|
551
551
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
552
|
-
checksum = "
|
|
552
|
+
checksum = "740d80b0a761326575e52bf678749e1d08430e988b45d18f5db71bc20ad3a128"
|
|
553
553
|
dependencies = [
|
|
554
554
|
"cc",
|
|
555
555
|
"tree-sitter-language",
|
|
@@ -557,9 +557,9 @@ dependencies = [
|
|
|
557
557
|
|
|
558
558
|
[[package]]
|
|
559
559
|
name = "willbooster-tree-sitter-typescript"
|
|
560
|
-
version = "
|
|
560
|
+
version = "7.0.0"
|
|
561
561
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
562
|
-
checksum = "
|
|
562
|
+
checksum = "289945de1713b7f13625e2d9646e138f37be74d38d8f58de0cfe669d9fe9ba0c"
|
|
563
563
|
dependencies = [
|
|
564
564
|
"cc",
|
|
565
565
|
"tree-sitter-language",
|
package/native/Cargo.toml
CHANGED
|
@@ -20,18 +20,18 @@ rustc-hash = "2"
|
|
|
20
20
|
# `willbooster-*` crates are WillBooster's forks of the runtime and grammars. Every grammar crate
|
|
21
21
|
# depends only on `tree-sitter-language`, never on upstream's `tree-sitter` crate, whose
|
|
22
22
|
# `links = "tree-sitter"` would conflict with the forked runtime's.
|
|
23
|
-
tree-sitter = { package = "willbooster-tree-sitter", version = "1" }
|
|
24
|
-
tree-sitter-c = { package = "willbooster-tree-sitter-c", version = "=
|
|
25
|
-
tree-sitter-c-sharp = { package = "willbooster-tree-sitter-c-sharp", version = "=
|
|
26
|
-
tree-sitter-cpp = { package = "willbooster-tree-sitter-cpp", version = "=
|
|
23
|
+
tree-sitter = { package = "willbooster-tree-sitter", version = "1.4.3" }
|
|
24
|
+
tree-sitter-c = { package = "willbooster-tree-sitter-c", version = "=4.0.2" }
|
|
25
|
+
tree-sitter-c-sharp = { package = "willbooster-tree-sitter-c-sharp", version = "=3.0.1" }
|
|
26
|
+
tree-sitter-cpp = { package = "willbooster-tree-sitter-cpp", version = "=2.0.2" }
|
|
27
27
|
tree-sitter-go = "=0.25.0"
|
|
28
28
|
tree-sitter-java = "=0.23.5"
|
|
29
|
-
tree-sitter-javascript = { package = "willbooster-tree-sitter-javascript", version = "=
|
|
30
|
-
tree-sitter-kotlin = { package = "willbooster-tree-sitter-kotlin", version = "=
|
|
29
|
+
tree-sitter-javascript = { package = "willbooster-tree-sitter-javascript", version = "=4.0.2" }
|
|
30
|
+
tree-sitter-kotlin = { package = "willbooster-tree-sitter-kotlin", version = "=4.0.2" }
|
|
31
31
|
tree-sitter-python = "=0.25.0"
|
|
32
32
|
tree-sitter-ruby = "=0.23.1"
|
|
33
|
-
tree-sitter-rust = { package = "willbooster-tree-sitter-rust", version = "=
|
|
34
|
-
tree-sitter-typescript = { package = "willbooster-tree-sitter-typescript", version = "=
|
|
33
|
+
tree-sitter-rust = { package = "willbooster-tree-sitter-rust", version = "=2.0.2" }
|
|
34
|
+
tree-sitter-typescript = { package = "willbooster-tree-sitter-typescript", version = "=7.0.0" }
|
|
35
35
|
|
|
36
36
|
# The WebAssembly build (src/wasm.rs) exports a plain C ABI instead of N-API.
|
|
37
37
|
[target.'cfg(not(target_family = "wasm"))'.dependencies]
|
package/native/code-gauge.wasm
CHANGED
|
Binary file
|
package/native/src/complexity.rs
CHANGED
|
@@ -639,7 +639,8 @@ fn find_boolean_operator_text<'a>(binary_node: Node<'_>, code: &Source<'a>) -> O
|
|
|
639
639
|
}
|
|
640
640
|
|
|
641
641
|
/// Java `guard`, C# `when_clause`, Ruby `if_guard`, a Python `case` guard (an `if_clause` under a
|
|
642
|
-
/// `case_clause`),
|
|
642
|
+
/// `case_clause`), a Kotlin guard (the `guard` field of a `when_entry`), and Rust guards inside
|
|
643
|
+
/// `match_pattern`. A Python comprehension filter shares
|
|
643
644
|
/// the `if_clause` type but is a per-element predicate of one expression, not an extra execution
|
|
644
645
|
/// path: Sonar ports differ on it (complexipy charges it), and code-gauge does not, unlike
|
|
645
646
|
/// ternaries and conditions, which are charged. A C# exception filter (`catch (E e) when (...)`, a `catch_filter_clause`) is
|
|
@@ -656,6 +657,14 @@ fn is_pattern_guard(node: Node<'_>, parent: Option<Node<'_>>) -> bool {
|
|
|
656
657
|
if kind == "if_clause" {
|
|
657
658
|
return parent.is_some_and(|parent| parent.kind_name() == "case_clause");
|
|
658
659
|
}
|
|
660
|
+
if parent.is_some_and(|parent| {
|
|
661
|
+
parent.kind_name() == "when_entry"
|
|
662
|
+
&& parent
|
|
663
|
+
.child_by_field_name("guard")
|
|
664
|
+
.is_some_and(|guard| guard.id() == node.id())
|
|
665
|
+
}) {
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
659
668
|
kind == "match_pattern"
|
|
660
669
|
&& all_children(node)
|
|
661
670
|
.iter()
|
package/native/src/container.rs
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
use rustc_hash::FxHashSet;
|
|
2
2
|
use tree_sitter::Node;
|
|
3
3
|
|
|
4
|
-
use crate::functions::
|
|
4
|
+
use crate::functions::{
|
|
5
|
+
find_pair_key_name, find_parallel_assignment_target, find_single_assignment_target,
|
|
6
|
+
find_string_literal_content, find_value_binding, is_value_group, next_declarator,
|
|
7
|
+
unwrap_transparent_value_wrappers,
|
|
8
|
+
};
|
|
5
9
|
use crate::tree_index::NodeExt;
|
|
6
10
|
use crate::util::{named_children, node_text, Source};
|
|
7
11
|
|
|
@@ -88,20 +92,57 @@ fn explicit_ruby_owner(object: Option<Node<'_>>, code: &Source<'_>) -> Option<St
|
|
|
88
92
|
(text != "self").then(|| text.to_string())
|
|
89
93
|
}
|
|
90
94
|
|
|
91
|
-
/// The name of the declaration whose member a
|
|
92
|
-
///
|
|
93
|
-
/// declarator, the object of a Ruby singleton
|
|
95
|
+
/// The name of the declaration whose member a function is: the nearest class-like declaration
|
|
96
|
+
/// enclosing it or, for a function written as a value, the declaration binding it; the owner the
|
|
97
|
+
/// function names itself (a Go receiver, a C++ qualified declarator, the object of a Ruby singleton
|
|
98
|
+
/// method); or the path a JavaScript object literal holding it is bound to.
|
|
94
99
|
pub fn find_container_name(
|
|
95
100
|
node: Node<'_>,
|
|
96
101
|
function_nodes: &FxHashSet<&'static str>,
|
|
97
102
|
code: &Source<'_>,
|
|
98
103
|
inline_namespaces: &InlineNamespaces<'_>,
|
|
99
104
|
) -> Option<String> {
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
|
|
105
|
+
let is_declared = is_function_declaration(node);
|
|
106
|
+
// A function expression with a name of its own (`pick = function impl() {}`) is reported
|
|
107
|
+
// under that name, which is no member of what binds the expression.
|
|
108
|
+
if !is_declared && node.child_by_field_name("name").is_some() {
|
|
103
109
|
return None;
|
|
104
110
|
}
|
|
111
|
+
let (holder, bound) = find_value_binding(node, code)?;
|
|
112
|
+
let member = if holder.kind_name() == "pair" {
|
|
113
|
+
holder
|
|
114
|
+
} else {
|
|
115
|
+
bound
|
|
116
|
+
};
|
|
117
|
+
if let Some(object) = member
|
|
118
|
+
.parent_node()
|
|
119
|
+
.filter(|parent| parent.kind_name() == "object")
|
|
120
|
+
{
|
|
121
|
+
return object_owner(object, function_nodes, code, inline_namespaces);
|
|
122
|
+
}
|
|
123
|
+
// A function written as a value belongs to whatever it is passed or assigned to, which the
|
|
124
|
+
// syntax can follow only where a declaration binds it (a class field, a constant of a
|
|
125
|
+
// namespace); elsewhere it keeps the name of what it is bound to and has no owner.
|
|
126
|
+
if !is_declared {
|
|
127
|
+
// A parallel assignment binds the value to the target at its position.
|
|
128
|
+
if is_value_group(holder) {
|
|
129
|
+
let (assignment, target) = find_parallel_assignment_target(bound)?;
|
|
130
|
+
return assigns_attribute(assignment, target)
|
|
131
|
+
.then(|| enclosing_owner(assignment, function_nodes, code, inline_namespaces))
|
|
132
|
+
.flatten();
|
|
133
|
+
}
|
|
134
|
+
if !declares_binding(holder, bound) {
|
|
135
|
+
return None;
|
|
136
|
+
}
|
|
137
|
+
// A C++ variable defined outside its class (`int (*Rules::pick)(int) = [] {}`) spells
|
|
138
|
+
// its owner, as a function defined there does.
|
|
139
|
+
return match find_qualified_declarator(holder) {
|
|
140
|
+
Some(qualified) => {
|
|
141
|
+
cpp_spelled_owner(holder, qualified, function_nodes, code, inline_namespaces)
|
|
142
|
+
}
|
|
143
|
+
None => enclosing_owner(holder, function_nodes, code, inline_namespaces),
|
|
144
|
+
};
|
|
145
|
+
}
|
|
105
146
|
if let Some(receiver) = node.child_by_field_name("receiver") {
|
|
106
147
|
return go_receiver_type(receiver, code);
|
|
107
148
|
}
|
|
@@ -113,6 +154,116 @@ pub fn find_container_name(
|
|
|
113
154
|
.or_else(|| enclosing_owner(node, function_nodes, code, inline_namespaces))
|
|
114
155
|
}
|
|
115
156
|
|
|
157
|
+
/// Whether `holder` declares the name its value `bound` is bound to, as a field, a property, or a
|
|
158
|
+
/// variable does.
|
|
159
|
+
fn declares_binding(holder: Node<'_>, bound: Node<'_>) -> bool {
|
|
160
|
+
let kind = holder.kind_name();
|
|
161
|
+
if kind == "assignment" {
|
|
162
|
+
return find_single_assignment_target(holder)
|
|
163
|
+
.is_some_and(|target| assigns_attribute(holder, target))
|
|
164
|
+
&& holder
|
|
165
|
+
.child_by_field_name("right")
|
|
166
|
+
.is_some_and(|value| value.id() == bound.id());
|
|
167
|
+
}
|
|
168
|
+
kind.ends_with("_declarator")
|
|
169
|
+
|| kind.ends_with("_declaration")
|
|
170
|
+
|| kind.ends_with("_definition")
|
|
171
|
+
|| matches!(kind, "const_item" | "static_item")
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/// Whether an assignment declares `target` as an attribute of the class around it: a Python class
|
|
175
|
+
/// does by assigning to a plain name and a Ruby one a constant, while a plain name assigned in a
|
|
176
|
+
/// Ruby class body is a local variable of that body.
|
|
177
|
+
fn assigns_attribute(assignment: Node<'_>, target: Node<'_>) -> bool {
|
|
178
|
+
match target.kind_name() {
|
|
179
|
+
// Only Python wraps an assignment in an `expression_statement`, a chain (`e = f = ...`)
|
|
180
|
+
// in that of its outermost assignment.
|
|
181
|
+
"identifier" => {
|
|
182
|
+
let mut outermost = assignment;
|
|
183
|
+
while let Some(outer) = outermost
|
|
184
|
+
.parent_node()
|
|
185
|
+
.filter(|outer| outer.kind_name() == "assignment")
|
|
186
|
+
{
|
|
187
|
+
outermost = outer;
|
|
188
|
+
}
|
|
189
|
+
outermost
|
|
190
|
+
.parent_node()
|
|
191
|
+
.is_some_and(|statement| statement.kind_name() == "expression_statement")
|
|
192
|
+
}
|
|
193
|
+
"constant" => true,
|
|
194
|
+
_ => false,
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/// The path a JavaScript object literal is bound to, which its members are reached through:
|
|
199
|
+
/// `api` for `const api = { ... }`, `Rules.handlers` for a field of a class, `api.admin` for the
|
|
200
|
+
/// value of a property, `module.exports` for an assignment. An object passed or returned is bound
|
|
201
|
+
/// to nothing the syntax names.
|
|
202
|
+
fn object_owner(
|
|
203
|
+
object: Node<'_>,
|
|
204
|
+
function_nodes: &FxHashSet<&'static str>,
|
|
205
|
+
code: &Source<'_>,
|
|
206
|
+
inline_namespaces: &InlineNamespaces<'_>,
|
|
207
|
+
) -> Option<String> {
|
|
208
|
+
let bound = unwrap_transparent_value_wrappers(object);
|
|
209
|
+
let holder = bound.parent_node()?;
|
|
210
|
+
let is_value = |field: &str| {
|
|
211
|
+
holder
|
|
212
|
+
.child_by_field_name(field)
|
|
213
|
+
.is_some_and(|value| value.id() == bound.id())
|
|
214
|
+
};
|
|
215
|
+
match holder.kind_name() {
|
|
216
|
+
"variable_declarator" | "field_definition" | "public_field_definition"
|
|
217
|
+
if is_value("value") =>
|
|
218
|
+
{
|
|
219
|
+
let name = holder
|
|
220
|
+
.child_by_field_name("name")
|
|
221
|
+
.or_else(|| holder.child_by_field_name("property"))?;
|
|
222
|
+
// A field may be named by a literal (`"handlers" = {}`); a computed name or a
|
|
223
|
+
// destructuring pattern names nothing.
|
|
224
|
+
let name = match name.kind_name() {
|
|
225
|
+
"string" => find_string_literal_content(name, code)?,
|
|
226
|
+
"number" => node_text(name, code).to_string(),
|
|
227
|
+
kind if kind.ends_with("identifier") => node_text(name, code).to_string(),
|
|
228
|
+
_ => return None,
|
|
229
|
+
};
|
|
230
|
+
Some(
|
|
231
|
+
match enclosing_owner(holder, function_nodes, code, inline_namespaces) {
|
|
232
|
+
Some(owner) => format!("{owner}.{name}"),
|
|
233
|
+
None => name,
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
}
|
|
237
|
+
"pair" if is_value("value") => {
|
|
238
|
+
let outer = holder
|
|
239
|
+
.parent_node()
|
|
240
|
+
.filter(|outer| outer.kind_name() == "object")?;
|
|
241
|
+
let owner = object_owner(outer, function_nodes, code, inline_namespaces)?;
|
|
242
|
+
Some(format!("{owner}.{}", find_pair_key_name(holder, code)?))
|
|
243
|
+
}
|
|
244
|
+
"assignment_expression" if is_value("right") => holder
|
|
245
|
+
.child_by_field_name("left")
|
|
246
|
+
.and_then(|target| member_path(target, code)),
|
|
247
|
+
_ => None,
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/// A name or a chain of property accesses on one (`exports`, `Rules.prototype`, `this.handlers`),
|
|
252
|
+
/// spelled without the comments and line breaks its source may hold.
|
|
253
|
+
fn member_path(node: Node<'_>, code: &Source<'_>) -> Option<String> {
|
|
254
|
+
match node.kind_name() {
|
|
255
|
+
"identifier" | "this" => Some(node_text(node, code).to_string()),
|
|
256
|
+
"member_expression" => {
|
|
257
|
+
let object = member_path(node.child_by_field_name("object")?, code)?;
|
|
258
|
+
let property = node
|
|
259
|
+
.child_by_field_name("property")
|
|
260
|
+
.filter(|property| property.kind_name() == "property_identifier")?;
|
|
261
|
+
Some(format!("{object}.{}", node_text(property, code)))
|
|
262
|
+
}
|
|
263
|
+
_ => None,
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
116
267
|
/// A Go method is declared outside its type and names it as its receiver, `(r *Rules)` or
|
|
117
268
|
/// `(r Rules[T])`.
|
|
118
269
|
fn go_receiver_type(receiver: Node<'_>, code: &Source<'_>) -> Option<String> {
|