code-gauge 10.1.0 → 10.2.1

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.
Files changed (42) hide show
  1. package/README.md +37 -15
  2. package/THIRD-PARTY-NOTICES.txt +8 -8
  3. package/dist/checkCommand.cjs +1 -1
  4. package/dist/checkCommand.cjs.map +1 -1
  5. package/dist/checkCommand.js +1 -1
  6. package/dist/checkCommand.js.map +1 -1
  7. package/dist/crossFileDuplication.cjs +1 -1
  8. package/dist/crossFileDuplication.cjs.map +1 -1
  9. package/dist/crossFileDuplication.d.ts +3 -2
  10. package/dist/crossFileDuplication.js +1 -1
  11. package/dist/crossFileDuplication.js.map +1 -1
  12. package/dist/crossFileNearMiss.cjs +1 -1
  13. package/dist/crossFileNearMiss.cjs.map +1 -1
  14. package/dist/crossFileNearMiss.d.ts +9 -4
  15. package/dist/crossFileNearMiss.js +1 -1
  16. package/dist/crossFileNearMiss.js.map +1 -1
  17. package/dist/duplication.cjs +1 -1
  18. package/dist/duplication.cjs.map +1 -1
  19. package/dist/duplication.d.ts +28 -7
  20. package/dist/duplication.js +1 -1
  21. package/dist/duplication.js.map +1 -1
  22. package/dist/thresholdCheck.cjs +1 -1
  23. package/dist/thresholdCheck.cjs.map +1 -1
  24. package/dist/thresholdCheck.d.ts +2 -0
  25. package/dist/thresholdCheck.js +1 -1
  26. package/dist/thresholdCheck.js.map +1 -1
  27. package/dist/thresholds.cjs.map +1 -1
  28. package/dist/thresholds.d.ts +2 -3
  29. package/dist/thresholds.js.map +1 -1
  30. package/dist/types.d.ts +20 -8
  31. package/native/Cargo.lock +16 -16
  32. package/native/Cargo.toml +8 -8
  33. package/native/code-gauge.wasm +0 -0
  34. package/native/src/complexity.rs +10 -1
  35. package/native/src/container.rs +158 -7
  36. package/native/src/dep_degree.rs +13 -14
  37. package/native/src/duplication.rs +302 -137
  38. package/native/src/functions.rs +112 -24
  39. package/native/src/languages.rs +5 -0
  40. package/native/src/measure.rs +9 -4
  41. package/native/src/near_miss.rs +303 -52
  42. 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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"}
@@ -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 between the matched parts of an\n * edited copy. A block matched as a whole by a near-miss copy counts all its code lines, its\n * edited ones included.\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,EASa,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"}
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"}
@@ -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 between the matched parts of an
34
- * edited copy. A block matched as a whole by a near-miss copy counts all its code lines, its
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;
@@ -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 between the matched parts of an\n * edited copy. A block matched as a whole by a near-miss copy counts all its code lines, its\n * edited ones included.\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,EASa,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"}
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
- * Absent for a function written as a value (a lambda, a closure, a function expression), which
82
- * keeps the name of what it is bound to, for a function nested in another function, for a
83
- * member of an anonymous class or of an object literal, and for a function local to an
84
- * initializer block. So a method of a class declared inside a function has it, and a closure
85
- * inside a method does not.
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: code lines carrying matched
161
- * tokens. Exposed so consumers combining within-file and cross-file coverage can union exact
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,9 @@ 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, and what lies between the matched cores of a near-miss block.
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, unless a part of it matched
192
+ * exactly reaches them.
181
193
  */
182
194
  lineNumbers: number[];
183
195
  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.0.5"
487
+ version = "1.4.3"
488
488
  source = "registry+https://github.com/rust-lang/crates.io-index"
489
- checksum = "7c6ca3007235ade028b793d6090a5ea16618ea3ab9b53427d1f782b15bf64064"
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 = "1.1.0"
500
+ version = "4.0.2"
501
501
  source = "registry+https://github.com/rust-lang/crates.io-index"
502
- checksum = "5cee6f1a4e0f64f337d28aeaed73879d7e8a87d77c287afb989ce29adea54700"
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 = "1.0.3"
510
+ version = "3.0.1"
511
511
  source = "registry+https://github.com/rust-lang/crates.io-index"
512
- checksum = "99b224dcd59cda9b24b588557cca3827abaccaf10da72d125c31e7cdb2e6f7fb"
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 = "1.1.1"
520
+ version = "2.0.2"
521
521
  source = "registry+https://github.com/rust-lang/crates.io-index"
522
- checksum = "72b92a9fcb0dc27bd4af18e06fe6c085dd14d91f009f60ecd92c0308b9cbd068"
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 = "1.0.3"
530
+ version = "4.0.2"
531
531
  source = "registry+https://github.com/rust-lang/crates.io-index"
532
- checksum = "e6c40d41b303b525194daa503a71ec94d8269fe8d1648b2d9138fe902623beff"
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 = "1.4.0"
540
+ version = "4.0.2"
541
541
  source = "registry+https://github.com/rust-lang/crates.io-index"
542
- checksum = "529ae914939b559754e6dd8b34d2705f04d6dcb73f09cb52f024b49eda9b5b88"
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 = "1.1.1"
550
+ version = "2.0.2"
551
551
  source = "registry+https://github.com/rust-lang/crates.io-index"
552
- checksum = "324c9f18cfc29e4da7335ad8cb76beb6ac1ae91a8037a44724bc55fce04f26f7"
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 = "1.1.1"
560
+ version = "7.0.0"
561
561
  source = "registry+https://github.com/rust-lang/crates.io-index"
562
- checksum = "f8c51b6c9e10d6ae34c2f84c4d44fff79a0210890ede63fda558a6a0f042eb28"
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 = "=1.1.0" }
25
- tree-sitter-c-sharp = { package = "willbooster-tree-sitter-c-sharp", version = "=1.0.3" }
26
- tree-sitter-cpp = { package = "willbooster-tree-sitter-cpp", version = "=1.1.1" }
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 = "=1.0.3" }
30
- tree-sitter-kotlin = { package = "willbooster-tree-sitter-kotlin", version = "=1.4.0" }
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 = "=1.1.1" }
34
- tree-sitter-typescript = { package = "willbooster-tree-sitter-typescript", version = "=1.1.1" }
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]
Binary file
@@ -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`), and Rust guards inside `match_pattern`. A Python comprehension filter shares
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()
@@ -1,7 +1,11 @@
1
1
  use rustc_hash::FxHashSet;
2
2
  use tree_sitter::Node;
3
3
 
4
- use crate::functions::next_declarator;
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 declared function is: the nearest class-like
92
- /// declaration enclosing it, or the owner the function names itself (a Go receiver, a C++ qualified
93
- /// declarator, the object of a Ruby singleton method).
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
- // A function written as a value belongs to whatever it is passed or assigned to, which the
101
- // syntax cannot follow reliably; it keeps the name of what it is bound to and has no owner.
102
- if !is_function_declaration(node) {
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> {