code-gauge 4.7.1 → 4.8.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 +1 -1
- package/THIRD-PARTY-NOTICES.txt +595 -29
- package/dist/cli.cjs +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- 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.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/diffCommand.cjs +1 -1
- package/dist/diffCommand.cjs.map +1 -1
- package/dist/diffCommand.js +1 -1
- package/dist/diffCommand.js.map +1 -1
- package/dist/duplicateSelection.cjs +1 -1
- package/dist/duplicateSelection.cjs.map +1 -1
- package/dist/duplicateSelection.js +1 -1
- package/dist/duplicateSelection.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/git.cjs +1 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.js +1 -1
- package/dist/git.js.map +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.js +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/regressionGate.cjs +1 -1
- package/dist/regressionGate.cjs.map +1 -1
- package/dist/regressionGate.js +1 -1
- package/dist/regressionGate.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/native/Cargo.lock +47 -15
- package/native/Cargo.toml +9 -9
- package/native/code-gauge.wasm +0 -0
- package/native/src/complexity.rs +26 -33
- package/native/src/dep_degree.rs +43 -32
- package/native/src/duplication.rs +55 -40
- package/native/src/functions.rs +20 -62
- package/native/src/languages.rs +28 -7
- package/native/src/measure.rs +16 -27
- package/native/src/ncss.rs +32 -23
- package/native/src/tree_index.rs +9 -0
- package/native/src/util.rs +12 -57
- package/package.json +10 -10
package/dist/scan.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{detectLanguage as t}from"./languages.js";import{NativeAddonError as n}from"./nativeMetrics.js";import{measureCode as r,measureCodeWithCrossFileDataAsync as i}from"./metrics.js";import{lstat as a,readFile as o,readdir as s,realpath as c,stat as l}from"node:fs/promises";import u from"node:path";import d from"node:os";const f
|
|
1
|
+
import{measureCrossFileDuplication as e}from"./crossFileDuplication.js";import{detectLanguage as t}from"./languages.js";import{NativeAddonError as n}from"./nativeMetrics.js";import{measureCode as r,measureCodeWithCrossFileDataAsync as i}from"./metrics.js";import{lstat as a,readFile as o,readdir as s,realpath as c,stat as l}from"node:fs/promises";import u from"node:path";import d from"node:os";const f=/* @__PURE__ */ new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`obj`,`target`,`test-fixtures`,`vendor`,`venv`]),p=/* @__PURE__ */ new Set([`__tests__`,`test`,`tests`,`spec`]),m=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,h=/Tests?\.(?:java|kt|cs)$/u;function g(e){return e===`~`?d.homedir():e.startsWith(`~/`)?u.join(d.homedir(),e.slice(2)):u.resolve(e)}async function _(e){try{return(await l(e)).isDirectory()?e:u.dirname(e)}catch{return u.dirname(e)}}const v=d.availableParallelism()*2;async function y(e,t){let n=e;try{n=await c(e)}catch{}let r=u.dirname(n),i;try{i=await l(n)}catch(e){let t=`${R(n,r)}: ${V(e)}`;return{displayRoot:r,files:[],errors:[t],warnings:[],fatalError:t}}if(i.isFile()){let e=u.dirname(n),r=L(n,t,!0);if(!r){let t=`${R(n,e)}: unsupported file type`;return{displayRoot:e,files:[],errors:[t],warnings:[],fatalError:t}}let i=S(t,e);return await k(n,r,`single-file`,i,n),x(i,e)}let a=S(t,n);return await E(n,a),x(a,n)}async function b(e,t,n){let r=S(n,e);for(let i of t){if(r.fatalSeen)break;let t=I(i,n)?L(i,n):void 0;if(!t)continue;let o=u.join(e,i);(await a(o).catch(()=>{}))?.isSymbolicLink()||await k(o,t,`directory`,r)}return x(r,e)}async function x(e,t){let n=[],r=[],i=[];for(let a of e.outcomes){let e=await a;if(`fatal`in e){let a=V(e.fatal);return{displayRoot:t,files:n,errors:[...r,a],warnings:i,fatalError:a}}if(`error`in e){r.push(e.error);continue}n.push(e.file),e.warning!==void 0&&i.push(e.warning)}return{displayRoot:t,files:n,errors:r,warnings:i}}function S(e,t){return{options:e,outcomes:[],inFlight:/* @__PURE__ */ new Set,fatalSeen:!1,visitedDirectories:/* @__PURE__ */ new Set,visitedFiles:/* @__PURE__ */ new Set,rootDirectory:t}}function C(e,t,n){e.outcomes.push({error:`${R(t,e.rootDirectory)}: ${V(n)}`})}async function w(e,t,n){try{return await e()}catch(e){C(n,t,e);return}}async function T(e,t){let n=await w(()=>c(e),e,t);return n!==void 0&&F(n,t.rootDirectory)?n:void 0}async function E(e,t){let n=await T(e,t);if(n===void 0||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r=await w(()=>s(e,{withFileTypes:!0}),e,t);if(r!==void 0)for(let n of r){if(t.fatalSeen)return;let r=u.join(e,n.name);if(n.isSymbolicLink()){await D(n.name,r,t);continue}if(n.isDirectory()){if(P(n.name,t.options))continue;await E(r,t);continue}n.isFile()&&await O(r,t)}}async function D(e,t,n){let r=await T(t,n);if(r===void 0)return;let i=await w(()=>l(t),t,n);if(i!==void 0){if(i.isDirectory()){if(P(e,n.options)||P(u.basename(r),n.options))return;await E(t,n);return}i.isFile()&&await O(t,n,r,r)}}async function O(e,t,n=e,r){let i=L(n,t.options);i&&await k(e,i,`directory`,t,r)}async function k(e,t,n,r,i){if(r.fatalSeen)return;let a;try{a=i??await c(e)}catch(t){C(r,e,t);return}if(r.visitedFiles.has(a))return;for(r.visitedFiles.add(a);r.inFlight.size>=v;)await Promise.race(r.inFlight);let o=A(e,t,n,r);r.inFlight.add(o),o.then(()=>r.inFlight.delete(o)),r.outcomes.push(o)}async function A(e,t,i,a){try{let n=await o(e,`utf8`),s={language:t,duplication:a.options.duplication};if(i===`single-file`)return{file:{file:e,metrics:r(n,s)}};let{metrics:c,crossFileData:l,crossFileError:u}=await j(n,s);return{file:{file:e,metrics:c,duplicationCandidates:l},warning:u===void 0?void 0:`${R(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${u}`}}catch(t){return t instanceof n?(a.fatalSeen=!0,{fatal:t}):{error:`${R(e,a.rootDirectory)}: ${V(t)}`}}}async function j(e,t){try{return await i(e,t)}catch(i){if(i instanceof n)throw i;return{metrics:r(e,t),crossFileError:V(i)}}}function M(t,n){if(t.fatalError||t.files.length<2)return;let r=t.files.flatMap(({file:e,duplicationCandidates:n})=>n?[{file:R(e,t.displayRoot),...n}]:[]);r.length<2||(t.crossFileDuplication=e(r,n.duplication))}function N(e,t,n){let r=new Set(e?.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function P(e,t){return f.has(e)?!0:!t.includeTests&&p.has(e)}function F(e,t){let n=u.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${u.sep}`)&&!u.isAbsolute(n)}function I(e,t){let n=e.split(`/`);for(let e of n.slice(0,-1))if(f.has(e)||!t.includeTests&&p.has(e))return!1;return L(e,t)!==void 0}function L(e,n,r=!1){let i=e.toLowerCase();if(!(!r&&(i.endsWith(`.d.ts`)||i.endsWith(`.d.mts`)||i.endsWith(`.d.cts`)||i.endsWith(`.min.js`)||i.endsWith(`.pnp.cjs`)))&&(r||n.includeTests||!m.test(u.basename(e))&&!h.test(u.basename(e))))return t(e)}function R(e,t){return u.relative(t,e)||u.basename(e)}function z(e){process.stdout.write(e)}function B(e){process.stderr.write(e)}function V(e){return e instanceof Error?e.message:String(e)}export{M as addCrossFileDuplication,N as collectDuplicatedLineNumbers,_ as configSearchDirectory,V as formatError,R as formatPath,L as getLanguage,I as isScannedPath,j as measureWithCrossFileData,g as resolveTarget,b as scanListedFiles,y as scanTarget,B as writeStderr,z as writeStdout};
|
|
2
2
|
//# sourceMappingURL=scan.js.map
|
package/dist/scan.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"scan.js","names":[],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileDataAsync } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n /**\n * Outcomes in walk order. Files are measured concurrently, so a measurement is recorded as a\n * promise here and applied in this order once the walk ends, keeping results deterministic.\n */\n outcomes: (ScanOutcome | Promise<ScanOutcome>)[];\n /** Measurements in flight, bounded so file contents and payloads do not pile up during the walk. */\n inFlight: Set<Promise<ScanOutcome>>;\n /** Set once a measurement fails fatally; the walk then starts no further work. */\n fatalSeen: boolean;\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\n/**\n * A missing native addon fails every file identically, so it ends the scan as one fatal error\n * instead of one \"skipped\" entry per file behind a successful exit code.\n */\ntype ScanOutcome = { file: FileMetrics; warning?: string } | { error: string } | { fatal: NativeAddonError };\n\n// Twice the addon's worker count keeps its pool busy while finished payloads are parsed.\nconst maxMeasurementsInFlight = os.availableParallelism() * 2;\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files: [], errors: [fatalError], warnings: [], fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files: [], errors: [fatalError], warnings: [], fatalError };\n }\n\n const context = makeScanContext(options, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return settleScan(context, displayRoot);\n }\n\n const context = makeScanContext(options, canonicalTarget);\n await scanDirectory(canonicalTarget, context);\n return settleScan(context, canonicalTarget);\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const context = makeScanContext(options, rootDirectory);\n for (const relativePath of relativePaths) {\n if (context.fatalSeen) {\n break;\n }\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n await measureFile(absolutePath, language, 'directory', context);\n }\n return settleScan(context, rootDirectory);\n}\n\n/** Applies the scan's outcomes in walk order once every measurement has settled. */\nasync function settleScan(context: ScanContext, displayRoot: string): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n for (const pending of context.outcomes) {\n const outcome = await pending;\n if ('fatal' in outcome) {\n const fatalError = formatError(outcome.fatal);\n // Errors the walk recorded before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n }\n if ('error' in outcome) {\n errors.push(outcome.error);\n continue;\n }\n files.push(outcome.file);\n if (outcome.warning !== undefined) {\n warnings.push(outcome.warning);\n }\n }\n return { displayRoot, files, errors, warnings };\n}\n\nfunction makeScanContext(options: ScanOptions, rootDirectory: string): ScanContext {\n return {\n options,\n outcomes: [],\n inFlight: new Set(),\n fatalSeen: false,\n visitedDirectories: new Set(),\n visitedFiles: new Set(),\n rootDirectory,\n };\n}\n\nfunction recordError(context: ScanContext, target: string, error: unknown): void {\n context.outcomes.push({ error: `${formatPath(target, context.rootDirectory)}: ${formatError(error)}` });\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n recordError(context, target, error);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n if (context.fatalSeen) {\n return;\n }\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\n/**\n * Resolves and deduplicates the file in walk order, then starts measuring it concurrently; its\n * outcome is recorded in walk order (see ScanContext.outcomes).\n */\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n if (context.fatalSeen) {\n return;\n }\n let resolvedFile;\n try {\n resolvedFile = realFile ?? (await realpath(file));\n } catch (error) {\n recordError(context, file, error);\n return;\n }\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n // Any settled measurement frees its slot (removed by the callback below, which runs before the\n // race resumes), so one slow file never idles the pool.\n while (context.inFlight.size >= maxMeasurementsInFlight) {\n await Promise.race(context.inFlight);\n }\n const outcome = readAndMeasureFile(file, language, mode, context);\n context.inFlight.add(outcome);\n void outcome.then(() => context.inFlight.delete(outcome));\n context.outcomes.push(outcome);\n}\n\nasync function readAndMeasureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext\n): Promise<ScanOutcome> {\n try {\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n return { file: { file, metrics: measureCode(code, measureOptions) } };\n }\n const { metrics, crossFileData, crossFileError } = await measureWithCrossFileData(code, measureOptions);\n return {\n file: { file, metrics, duplicationCandidates: crossFileData },\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n warning:\n crossFileError === undefined\n ? undefined\n : `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`,\n };\n } catch (error) {\n if (error instanceof NativeAddonError) {\n context.fatalSeen = true;\n return { fatal: error };\n }\n return { error: `${formatPath(file, context.rootDirectory)}: ${formatError(error)}` };\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport async function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string }> {\n try {\n return await measureCodeWithCrossFileDataAsync(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: formatError(error) };\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"scan.js","names":[],"sources":["../src/scan.ts"],"sourcesContent":["import { lstat, readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { detectLanguage } from './languages.js';\nimport { measureCode, measureCodeWithCrossFileDataAsync } from './metrics.js';\nimport { NativeAddonError } from './nativeMetrics.js';\nimport type { CodeMetrics, DuplicationOptions, LanguageName, MeasureOptions } from './types.js';\n\n/** The scan settings shared by every command (a structural subset of each command's options). */\nexport interface ScanOptions {\n duplication: Required<DuplicationOptions>;\n includeTests: boolean;\n}\n\nexport interface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\nexport interface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n // .NET SDK intermediate output (generated sources such as `*.GlobalUsings.g.cs`).\n 'obj',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit (Java/Kotlin) and xUnit/NUnit (C#) tests use case-sensitive `Test`/`Tests` class-name\n// suffixes; case-insensitive matching would catch production files like `contest.java`.\nconst suffixTestFilePattern = /Tests?\\.(?:java|kt|cs)$/u;\n\nexport function resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nexport async function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ScanOptions;\n /**\n * Outcomes in walk order. Files are measured concurrently, so a measurement is recorded as a\n * promise here and applied in this order once the walk ends, keeping results deterministic.\n */\n outcomes: (ScanOutcome | Promise<ScanOutcome>)[];\n /** Measurements in flight, bounded so file contents and payloads do not pile up during the walk. */\n inFlight: Set<Promise<ScanOutcome>>;\n /** Set once a measurement fails fatally; the walk then starts no further work. */\n fatalSeen: boolean;\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\n/**\n * A missing native addon fails every file identically, so it ends the scan as one fatal error\n * instead of one \"skipped\" entry per file behind a successful exit code.\n */\ntype ScanOutcome = { file: FileMetrics; warning?: string } | { error: string } | { fatal: NativeAddonError };\n\n// Twice the addon's worker count keeps its pool busy while finished payloads are parsed.\nconst maxMeasurementsInFlight = os.availableParallelism() * 2;\n\nexport async function scanTarget(target: string, options: ScanOptions): Promise<ScanResult> {\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files: [], errors: [fatalError], warnings: [], fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files: [], errors: [fatalError], warnings: [], fatalError };\n }\n\n const context = makeScanContext(options, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return settleScan(context, displayRoot);\n }\n\n const context = makeScanContext(options, canonicalTarget);\n await scanDirectory(canonicalTarget, context);\n return settleScan(context, canonicalTarget);\n}\n\n/**\n * Measures an explicit list of repository-relative files (the diff gate's git-visible allowlist)\n * instead of walking the directory tree, so ignored artifact directories are never parsed. Paths\n * outside the scan scope (ignored/test directories, unsupported or test file names) are skipped\n * with the same rules as the walk.\n */\nexport async function scanListedFiles(\n rootDirectory: string,\n relativePaths: Iterable<string>,\n options: ScanOptions\n): Promise<ScanResult> {\n const context = makeScanContext(options, rootDirectory);\n for (const relativePath of relativePaths) {\n if (context.fatalSeen) {\n break;\n }\n const language = isScannedPath(relativePath, options) ? getLanguage(relativePath, options) : undefined;\n if (!language) {\n continue;\n }\n const absolutePath = path.join(rootDirectory, relativePath);\n // Symbolic links are not source files: git stores only their target string, so measuring\n // through them would diverge from what any revision of the repository actually contains.\n const stats = await lstat(absolutePath).catch(() => {});\n if (stats?.isSymbolicLink()) {\n continue;\n }\n await measureFile(absolutePath, language, 'directory', context);\n }\n return settleScan(context, rootDirectory);\n}\n\n/** Applies the scan's outcomes in walk order once every measurement has settled. */\nasync function settleScan(context: ScanContext, displayRoot: string): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n for (const pending of context.outcomes) {\n const outcome = await pending;\n if ('fatal' in outcome) {\n const fatalError = formatError(outcome.fatal);\n // Errors the walk recorded before the fatal failure stay reported alongside it.\n return { displayRoot, files, errors: [...errors, fatalError], warnings, fatalError };\n }\n if ('error' in outcome) {\n errors.push(outcome.error);\n continue;\n }\n files.push(outcome.file);\n if (outcome.warning !== undefined) {\n warnings.push(outcome.warning);\n }\n }\n return { displayRoot, files, errors, warnings };\n}\n\nfunction makeScanContext(options: ScanOptions, rootDirectory: string): ScanContext {\n return {\n options,\n outcomes: [],\n inFlight: new Set(),\n fatalSeen: false,\n visitedDirectories: new Set(),\n visitedFiles: new Set(),\n rootDirectory,\n };\n}\n\nfunction recordError(context: ScanContext, target: string, error: unknown): void {\n context.outcomes.push({ error: `${formatPath(target, context.rootDirectory)}: ${formatError(error)}` });\n}\n\n/** Runs a filesystem operation, recording a scan error and returning undefined when it fails. */\nasync function tryFileSystem<T>(\n operation: () => Promise<T>,\n target: string,\n context: ScanContext\n): Promise<T | undefined> {\n try {\n return await operation();\n } catch (error) {\n recordError(context, target, error);\n return undefined;\n }\n}\n\n/** Resolves the path (recording errors); undefined when that fails or the result escapes the root. */\nasync function resolveWithinRoot(target: string, context: ScanContext): Promise<string | undefined> {\n const resolved = await tryFileSystem(() => realpath(target), target, context);\n return resolved !== undefined && isWithinDirectory(resolved, context.rootDirectory) ? resolved : undefined;\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n const resolvedDirectory = await resolveWithinRoot(directory, context);\n if (resolvedDirectory === undefined || context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n const entries = await tryFileSystem(() => readdir(directory, { withFileTypes: true }), directory, context);\n if (entries === undefined) {\n return;\n }\n\n for (const entry of entries) {\n if (context.fatalSeen) {\n return;\n }\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n const resolvedPath = await resolveWithinRoot(entryPath, context);\n if (resolvedPath === undefined) {\n return;\n }\n\n const entryStat = await tryFileSystem(() => stat(entryPath), entryPath, context);\n if (entryStat === undefined) {\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\n/**\n * Resolves and deduplicates the file in walk order, then starts measuring it concurrently; its\n * outcome is recorded in walk order (see ScanContext.outcomes).\n */\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n if (context.fatalSeen) {\n return;\n }\n let resolvedFile;\n try {\n resolvedFile = realFile ?? (await realpath(file));\n } catch (error) {\n recordError(context, file, error);\n return;\n }\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n // Any settled measurement frees its slot (removed by the callback below, which runs before the\n // race resumes), so one slow file never idles the pool.\n while (context.inFlight.size >= maxMeasurementsInFlight) {\n await Promise.race(context.inFlight);\n }\n const outcome = readAndMeasureFile(file, language, mode, context);\n context.inFlight.add(outcome);\n void outcome.then(() => context.inFlight.delete(outcome));\n context.outcomes.push(outcome);\n}\n\nasync function readAndMeasureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext\n): Promise<ScanOutcome> {\n try {\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n // Only directory scans compare files against each other; a single-file target has no peers.\n if (mode === 'single-file') {\n return { file: { file, metrics: measureCode(code, measureOptions) } };\n }\n const { metrics, crossFileData, crossFileError } = await measureWithCrossFileData(code, measureOptions);\n return {\n file: { file, metrics, duplicationCandidates: crossFileData },\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n warning:\n crossFileError === undefined\n ? undefined\n : `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${crossFileError}`,\n };\n } catch (error) {\n if (error instanceof NativeAddonError) {\n context.fatalSeen = true;\n return { fatal: error };\n }\n return { error: `${formatPath(file, context.rootDirectory)}: ${formatError(error)}` };\n }\n}\n\n/**\n * Measures a file together with its cross-file contribution from one parse. The contribution is\n * auxiliary: if collecting it fails where plain measurement succeeds (e.g. a payload too large to\n * cross the addon boundary), the metrics are still returned with the failure message, which\n * callers report as a warning rather than an error.\n */\nexport async function measureWithCrossFileData(\n code: string,\n measureOptions: MeasureOptions\n): Promise<{ metrics: CodeMetrics; crossFileData?: CrossFileDuplicationFileData; crossFileError?: string }> {\n try {\n return await measureCodeWithCrossFileDataAsync(code, measureOptions);\n } catch (error) {\n if (error instanceof NativeAddonError) {\n throw error;\n }\n return { metrics: measureCode(code, measureOptions), crossFileError: formatError(error) };\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nexport function addCrossFileDuplication(result: ScanResult, options: ScanOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and a ratio derived over code lines can never exceed 1.\n */\nexport function collectDuplicatedLineNumbers(\n metrics: CodeMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics?.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction shouldSkipDirectory(name: string, options: ScanOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\n/**\n * Whether a repository-relative path would be scanned: no ignored or excluded-test directory\n * segment and a supported, non-test file name. The diff gate uses this for base-revision\n * eligibility, so code renamed into scan scope gates as new code instead of ratcheting against\n * a blob the scanner would never have measured.\n */\nexport function isScannedPath(relativePath: string, options: ScanOptions): boolean {\n const segments = relativePath.split('/');\n for (const segment of segments.slice(0, -1)) {\n if (ignoredDirectoryNames.has(segment) || (!options.includeTests && testDirectoryNames.has(segment))) {\n return false;\n }\n }\n return getLanguage(relativePath, options) !== undefined;\n}\n\nexport function getLanguage(file: string, options: ScanOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || suffixTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n return detectLanguage(file);\n}\n\nexport function formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nexport function writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nexport function writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nexport function formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
package/native/Cargo.lock
CHANGED
|
@@ -19,9 +19,13 @@ checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
|
|
|
19
19
|
|
|
20
20
|
[[package]]
|
|
21
21
|
name = "cc"
|
|
22
|
-
version = "1.
|
|
22
|
+
version = "1.4.7"
|
|
23
23
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
24
|
-
checksum = "
|
|
24
|
+
checksum = "54413ede23c2daf518f35156dfde027feb2374004d63bd497f983c8db9c0e313"
|
|
25
|
+
dependencies = [
|
|
26
|
+
"find-msvc-tools",
|
|
27
|
+
"shlex",
|
|
28
|
+
]
|
|
25
29
|
|
|
26
30
|
[[package]]
|
|
27
31
|
name = "cfg-if"
|
|
@@ -48,11 +52,11 @@ dependencies = [
|
|
|
48
52
|
"tree-sitter-go",
|
|
49
53
|
"tree-sitter-java",
|
|
50
54
|
"tree-sitter-javascript",
|
|
51
|
-
"tree-sitter-kotlin",
|
|
52
55
|
"tree-sitter-python",
|
|
53
56
|
"tree-sitter-ruby",
|
|
54
57
|
"tree-sitter-rust",
|
|
55
58
|
"tree-sitter-typescript",
|
|
59
|
+
"willbooster-tree-sitter-kotlin",
|
|
56
60
|
]
|
|
57
61
|
|
|
58
62
|
[[package]]
|
|
@@ -76,6 +80,12 @@ version = "1.0.2"
|
|
|
76
80
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
77
81
|
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
|
|
78
82
|
|
|
83
|
+
[[package]]
|
|
84
|
+
name = "find-msvc-tools"
|
|
85
|
+
version = "0.1.13"
|
|
86
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
87
|
+
checksum = "ef25905e51abafe4dcea6c15fec58c57b601cdbd0ee53d22ea1d3016c587d39b"
|
|
88
|
+
|
|
79
89
|
[[package]]
|
|
80
90
|
name = "futures"
|
|
81
91
|
version = "0.3.33"
|
|
@@ -366,6 +376,7 @@ version = "1.0.150"
|
|
|
366
376
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
367
377
|
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
|
|
368
378
|
dependencies = [
|
|
379
|
+
"indexmap",
|
|
369
380
|
"itoa",
|
|
370
381
|
"memchr",
|
|
371
382
|
"serde",
|
|
@@ -373,12 +384,24 @@ dependencies = [
|
|
|
373
384
|
"zmij",
|
|
374
385
|
]
|
|
375
386
|
|
|
387
|
+
[[package]]
|
|
388
|
+
name = "shlex"
|
|
389
|
+
version = "2.0.1"
|
|
390
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
391
|
+
checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
|
|
392
|
+
|
|
376
393
|
[[package]]
|
|
377
394
|
name = "slab"
|
|
378
395
|
version = "0.4.12"
|
|
379
396
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
380
397
|
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
|
|
381
398
|
|
|
399
|
+
[[package]]
|
|
400
|
+
name = "streaming-iterator"
|
|
401
|
+
version = "0.1.9"
|
|
402
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
403
|
+
checksum = "2b2231b7c3057d5e4ad0156fb3dc807d900806020c5ffa3ee6ff2c8c76fb8520"
|
|
404
|
+
|
|
382
405
|
[[package]]
|
|
383
406
|
name = "syn"
|
|
384
407
|
version = "2.0.119"
|
|
@@ -403,12 +426,16 @@ dependencies = [
|
|
|
403
426
|
|
|
404
427
|
[[package]]
|
|
405
428
|
name = "tree-sitter"
|
|
406
|
-
version = "0.
|
|
429
|
+
version = "0.25.10"
|
|
407
430
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
408
|
-
checksum = "
|
|
431
|
+
checksum = "78f873475d258561b06f1c595d93308a7ed124d9977cb26b148c2084a4a3cc87"
|
|
409
432
|
dependencies = [
|
|
410
433
|
"cc",
|
|
411
434
|
"regex",
|
|
435
|
+
"regex-syntax",
|
|
436
|
+
"serde_json",
|
|
437
|
+
"streaming-iterator",
|
|
438
|
+
"tree-sitter-language",
|
|
412
439
|
]
|
|
413
440
|
|
|
414
441
|
[[package]]
|
|
@@ -463,23 +490,19 @@ dependencies = [
|
|
|
463
490
|
|
|
464
491
|
[[package]]
|
|
465
492
|
name = "tree-sitter-javascript"
|
|
466
|
-
version = "0.
|
|
493
|
+
version = "0.25.0"
|
|
467
494
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
468
|
-
checksum = "
|
|
495
|
+
checksum = "68204f2abc0627a90bdf06e605f5c470aa26fdcb2081ea553a04bdad756693f5"
|
|
469
496
|
dependencies = [
|
|
470
497
|
"cc",
|
|
471
|
-
"tree-sitter",
|
|
498
|
+
"tree-sitter-language",
|
|
472
499
|
]
|
|
473
500
|
|
|
474
501
|
[[package]]
|
|
475
|
-
name = "tree-sitter-
|
|
476
|
-
version = "0.
|
|
502
|
+
name = "tree-sitter-language"
|
|
503
|
+
version = "0.1.8"
|
|
477
504
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
478
|
-
checksum = "
|
|
479
|
-
dependencies = [
|
|
480
|
-
"cc",
|
|
481
|
-
"tree-sitter",
|
|
482
|
-
]
|
|
505
|
+
checksum = "ca0d1bf6fdd806e43ae5198f82f527056d359def39e54e67a0f478ac09dac081"
|
|
483
506
|
|
|
484
507
|
[[package]]
|
|
485
508
|
name = "tree-sitter-python"
|
|
@@ -533,6 +556,15 @@ version = "1.13.3"
|
|
|
533
556
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
534
557
|
checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8"
|
|
535
558
|
|
|
559
|
+
[[package]]
|
|
560
|
+
name = "willbooster-tree-sitter-kotlin"
|
|
561
|
+
version = "0.0.0"
|
|
562
|
+
source = "git+https://github.com/WillBooster/tree-sitter-kotlin?tag=v1.1.0#a05912e6f07e2cedd44bfb117940361089d4c3e3"
|
|
563
|
+
dependencies = [
|
|
564
|
+
"cc",
|
|
565
|
+
"tree-sitter-language",
|
|
566
|
+
]
|
|
567
|
+
|
|
536
568
|
[[package]]
|
|
537
569
|
name = "windows-link"
|
|
538
570
|
version = "0.2.1"
|
package/native/Cargo.toml
CHANGED
|
@@ -16,20 +16,20 @@ regex = "1"
|
|
|
16
16
|
rustc-hash = "2"
|
|
17
17
|
|
|
18
18
|
# Grammar crates are pinned exactly: every metric (and the golden-based test suite) is calibrated
|
|
19
|
-
# against these grammar versions, so upgrades are deliberate, test-verified changes. The
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
tree-sitter = "=0.
|
|
19
|
+
# against these grammar versions, so upgrades are deliberate, test-verified changes. The Kotlin
|
|
20
|
+
# grammar is WillBooster's fork, which is not on crates.io, pinned to a release tag. Core 0.25 is
|
|
21
|
+
# the first to load that grammar's ABI 15 parser, and tree-sitter-javascript 0.21.x pins
|
|
22
|
+
# `cc ~1.0`, which conflicts with the core's `cc ^1.2`, hence 0.25.0. The core reports no field for
|
|
23
|
+
# extra (error-recovery) children in ts_node_field_name_for_child, which
|
|
24
|
+
# util::find_children_by_field_name compensates for.
|
|
25
|
+
tree-sitter = "=0.25.10"
|
|
26
26
|
tree-sitter-c = "=0.21.4"
|
|
27
27
|
tree-sitter-c-sharp = "=0.21.3"
|
|
28
28
|
tree-sitter-cpp = "=0.22.3"
|
|
29
29
|
tree-sitter-go = "=0.21.2"
|
|
30
30
|
tree-sitter-java = "=0.21.0"
|
|
31
|
-
tree-sitter-kotlin = "=
|
|
32
|
-
tree-sitter-javascript = "=0.
|
|
31
|
+
tree-sitter-kotlin = { package = "willbooster-tree-sitter-kotlin", git = "https://github.com/WillBooster/tree-sitter-kotlin", tag = "v1.1.0" }
|
|
32
|
+
tree-sitter-javascript = "=0.25.0"
|
|
33
33
|
tree-sitter-python = "=0.21.0"
|
|
34
34
|
tree-sitter-ruby = "=0.21.0"
|
|
35
35
|
# 0.21.0 breaks MSVC (its build script passes the GCC-only -Wno-unused-parameter
|
package/native/code-gauge.wasm
CHANGED
|
Binary file
|
package/native/src/complexity.rs
CHANGED
|
@@ -11,6 +11,7 @@ pub struct LanguageSets {
|
|
|
11
11
|
pub nesting_nodes: FxHashSet<&'static str>,
|
|
12
12
|
pub ncss_nodes: FxHashSet<&'static str>,
|
|
13
13
|
pub ncss_containers: FxHashSet<&'static str>,
|
|
14
|
+
pub ncss_bare_body_parents: FxHashSet<&'static str>,
|
|
14
15
|
}
|
|
15
16
|
|
|
16
17
|
impl LanguageSets {
|
|
@@ -21,6 +22,11 @@ impl LanguageSets {
|
|
|
21
22
|
nesting_nodes: language.nesting_node_types.iter().copied().collect(),
|
|
22
23
|
ncss_nodes: language.ncss_node_types.iter().copied().collect(),
|
|
23
24
|
ncss_containers: language.ncss_container_node_types.iter().copied().collect(),
|
|
25
|
+
ncss_bare_body_parents: language
|
|
26
|
+
.ncss_bare_body_parent_node_types
|
|
27
|
+
.iter()
|
|
28
|
+
.copied()
|
|
29
|
+
.collect(),
|
|
24
30
|
}
|
|
25
31
|
}
|
|
26
32
|
}
|
|
@@ -31,8 +37,6 @@ const BOOLEAN_OPERATOR_PARENT_TYPES: &[&str] = &[
|
|
|
31
37
|
"binary_expression",
|
|
32
38
|
"binary",
|
|
33
39
|
"boolean_operator",
|
|
34
|
-
"conjunction_expression",
|
|
35
|
-
"disjunction_expression",
|
|
36
40
|
// C# pattern combinators (`is > 0 and <= 10`) sequence like `&&`/`||` (SonarC#).
|
|
37
41
|
"and_pattern",
|
|
38
42
|
"or_pattern",
|
|
@@ -306,6 +310,7 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
306
310
|
parent,
|
|
307
311
|
&self.sets.ncss_nodes,
|
|
308
312
|
&self.sets.ncss_containers,
|
|
313
|
+
&self.sets.ncss_bare_body_parents,
|
|
309
314
|
);
|
|
310
315
|
let ncss_frame = self.top_frame();
|
|
311
316
|
ncss_frame.ncss += own_ncss;
|
|
@@ -313,6 +318,9 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
313
318
|
ncss_frame.has_own_ncss_contribution = true;
|
|
314
319
|
}
|
|
315
320
|
|
|
321
|
+
// Kotlin accessors hang off their property, yet are members of the class body.
|
|
322
|
+
let is_charged_property =
|
|
323
|
+
inside_charged_class_body && current.kind_name() == "property_declaration";
|
|
316
324
|
for child in all_children(current) {
|
|
317
325
|
self.visit(
|
|
318
326
|
child,
|
|
@@ -324,7 +332,8 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
324
332
|
} else {
|
|
325
333
|
inside_nested_region
|
|
326
334
|
},
|
|
327
|
-
is_charged_class_body
|
|
335
|
+
is_charged_class_body
|
|
336
|
+
|| (is_charged_property && matches!(child.kind_name(), "getter" | "setter")),
|
|
328
337
|
);
|
|
329
338
|
}
|
|
330
339
|
|
|
@@ -358,7 +367,7 @@ impl FunctionBodyPass<'_, '_, '_> {
|
|
|
358
367
|
}
|
|
359
368
|
|
|
360
369
|
/// Plain else branches attached to `current`: an `else_clause`/Ruby `else` whose branch is not an
|
|
361
|
-
/// `else if` continuation, or a bare Java/Go `alternative:`
|
|
370
|
+
/// `else if` continuation, or a bare Java/Go/Kotlin `alternative:` branch without a clause wrapper.
|
|
362
371
|
fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64 {
|
|
363
372
|
if !current.is_named() {
|
|
364
373
|
return 0;
|
|
@@ -379,11 +388,6 @@ fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64
|
|
|
379
388
|
if kind != "if_statement" && kind != "if_expression" {
|
|
380
389
|
return 0;
|
|
381
390
|
}
|
|
382
|
-
// Kotlin's `else` is a bare keyword token followed by the branch body (no clause wrapper and
|
|
383
|
-
// no grammar field); an `else if` continuation is charged on the nested if instead.
|
|
384
|
-
if let Some(else_body) = crate::util::kotlin_else_body(current) {
|
|
385
|
-
return u64::from(!crate::util::is_kotlin_else_if_body(else_body));
|
|
386
|
-
}
|
|
387
391
|
// Extras (comments) inherit the preceding sibling's field in find_children_by_field_name, so a
|
|
388
392
|
// comment between an `elif_clause` and `else_clause` must not be miscounted as a bare branch.
|
|
389
393
|
crate::util::find_children_by_field_name(current, "alternative")
|
|
@@ -397,7 +401,10 @@ fn count_plain_else_branches(current: Node<'_>, parent: Option<Node<'_>>) -> u64
|
|
|
397
401
|
.count() as u64
|
|
398
402
|
}
|
|
399
403
|
|
|
400
|
-
/// goto, and break/continue that jump to a label
|
|
404
|
+
/// goto, and break/continue that jump to a label.
|
|
405
|
+
// Inlined into the recursive visit, its field lookup enlarges every frame enough to cut the depth the
|
|
406
|
+
// Workers runtime's stack can measure by ~5%.
|
|
407
|
+
#[inline(never)]
|
|
401
408
|
fn is_flow_breaking_jump(node: Node<'_>) -> bool {
|
|
402
409
|
if !node.is_named() {
|
|
403
410
|
return false;
|
|
@@ -405,19 +412,13 @@ fn is_flow_breaking_jump(node: Node<'_>) -> bool {
|
|
|
405
412
|
if node.kind_name() == "goto_statement" {
|
|
406
413
|
return true;
|
|
407
414
|
}
|
|
408
|
-
// Rust jumps are expressions; `break value` carries a named expression child,
|
|
409
|
-
// explicit `label` child marks a labeled jump.
|
|
415
|
+
// Rust and Kotlin jumps are expressions; a Rust `break value` carries a named expression child,
|
|
416
|
+
// so only an explicit Rust `label` child or Kotlin `label` field marks a labeled jump.
|
|
410
417
|
if node.kind_name() == "break_expression" || node.kind_name() == "continue_expression" {
|
|
411
|
-
return
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
// Kotlin folds every jump into `jump_expression`; the grammar tokenizes a labeled break or
|
|
416
|
-
// continue as `break@`/`continue@` followed by the label (`return@label` is a plain return).
|
|
417
|
-
if node.kind_name() == "jump_expression" {
|
|
418
|
-
return node.child(0).is_some_and(|keyword| {
|
|
419
|
-
keyword.kind_name() == "break@" || keyword.kind_name() == "continue@"
|
|
420
|
-
});
|
|
418
|
+
return node.child_by_field_name("label").is_some()
|
|
419
|
+
|| crate::util::named_children(node)
|
|
420
|
+
.iter()
|
|
421
|
+
.any(|child| child.kind_name() == "label" || child.kind_name() == "loop_label");
|
|
421
422
|
}
|
|
422
423
|
// Comments are named children too (`break /* done */;`), so only non-comment children mark a
|
|
423
424
|
// label.
|
|
@@ -519,14 +520,8 @@ fn is_flat_chain_continuation(node: Node<'_>, parent: Option<Node<'_>>) -> bool
|
|
|
519
520
|
let Some(parent) = parent else {
|
|
520
521
|
return false;
|
|
521
522
|
};
|
|
522
|
-
//
|
|
523
|
-
|
|
524
|
-
return parent
|
|
525
|
-
.parent_node()
|
|
526
|
-
.and_then(crate::util::kotlin_else_body)
|
|
527
|
-
.is_some_and(|else_body| else_body.id() == parent.id());
|
|
528
|
-
}
|
|
529
|
-
// JS/C/C++/Rust/C# wrap `else if` in an else clause or put it directly in `alternative`.
|
|
523
|
+
// JS/C/C++/Rust/C# wrap `else if` in an else clause; Java/Go/Kotlin put it directly in
|
|
524
|
+
// `alternative`.
|
|
530
525
|
parent.kind_name() == "else_clause"
|
|
531
526
|
|| parent
|
|
532
527
|
.child_by_field_name("alternative")
|
|
@@ -638,9 +633,7 @@ fn is_default_switch_branch(node: Node<'_>, code: &Source<'_>) -> bool {
|
|
|
638
633
|
.is_some_and(|first| is_csharp_catch_all_pattern(*first));
|
|
639
634
|
}
|
|
640
635
|
if kind == "when_entry" {
|
|
641
|
-
return
|
|
642
|
-
.iter()
|
|
643
|
-
.any(|child| child.kind_name() == "when_condition");
|
|
636
|
+
return node.child_by_field_name("condition").is_none();
|
|
644
637
|
}
|
|
645
638
|
|
|
646
639
|
// Python arms with an irrefutable pattern are unconditional like `default`.
|
package/native/src/dep_degree.rs
CHANGED
|
@@ -3,13 +3,11 @@ use tree_sitter::Node;
|
|
|
3
3
|
|
|
4
4
|
use crate::complexity::is_function_boundary;
|
|
5
5
|
use crate::tree_index::NodeExt;
|
|
6
|
-
use crate::util::{
|
|
6
|
+
use crate::util::{node_text, Source};
|
|
7
7
|
|
|
8
8
|
/// Leaf node types treated as variable references by the def-use approximation.
|
|
9
9
|
const VARIABLE_NODE_TYPES: &[&str] = &[
|
|
10
10
|
"identifier",
|
|
11
|
-
"simple_identifier",
|
|
12
|
-
"interpolated_identifier",
|
|
13
11
|
"implicit_parameter",
|
|
14
12
|
"instance_variable",
|
|
15
13
|
"class_variable",
|
|
@@ -51,18 +49,10 @@ const DEFINITION_FIELD_BY_PARENT_TYPE: &[(&str, &str)] = &[
|
|
|
51
49
|
("from_clause", "name"),
|
|
52
50
|
];
|
|
53
51
|
|
|
54
|
-
/// Kotlin
|
|
52
|
+
/// Kotlin marks no binding fields: an identifier directly under one of these declares a binding
|
|
55
53
|
/// (`val x`, `for (x in xs)`, lambda parameters, and the `catch (e: T)` exception name).
|
|
56
54
|
const KOTLIN_DEFINITION_PARENT_TYPES: &[&str] = &["variable_declaration", "catch_block"];
|
|
57
55
|
|
|
58
|
-
/// Kotlin parameter nodes whose identifier child is the declared name; the parameter LIST nodes
|
|
59
|
-
/// (`function_value_parameters`) also hold default-value expressions, which are reads.
|
|
60
|
-
const KOTLIN_PARAMETER_TYPES: &[&str] = &[
|
|
61
|
-
"parameter",
|
|
62
|
-
"parameter_with_optional_type",
|
|
63
|
-
"class_parameter",
|
|
64
|
-
];
|
|
65
|
-
|
|
66
56
|
/// C# LINQ clauses that bind a range variable as their first identifier child (no grammar field):
|
|
67
57
|
/// `join y in ...`, `into ys`, `let z = ...`, and a query continuation `into g`.
|
|
68
58
|
const CSHARP_QUERY_BINDING_PARENT_TYPES: &[&str] = &[
|
|
@@ -133,7 +123,7 @@ pub fn measure_dep_degree(
|
|
|
133
123
|
let mut pairs = 0u64;
|
|
134
124
|
for index in 0..leaves.len() {
|
|
135
125
|
let leaf = &leaves[index];
|
|
136
|
-
if !is_variable_leaf(leaf
|
|
126
|
+
if !is_variable_leaf(leaf) {
|
|
137
127
|
continue;
|
|
138
128
|
}
|
|
139
129
|
let name = node_text(leaf.node, code);
|
|
@@ -161,11 +151,19 @@ pub fn measure_dep_degree(
|
|
|
161
151
|
|
|
162
152
|
/// A C++ member-pointer variable (`int C::* p`, also `int C::* arr[1]`) is declared as a
|
|
163
153
|
/// `type_identifier` under the `pointer_type_declarator` spelling `C::*`, possibly through further
|
|
164
|
-
/// declarator wrappers; every other `type_identifier` names a type, not a variable.
|
|
165
|
-
|
|
154
|
+
/// declarator wrappers; every other `type_identifier` names a type, not a variable. Kotlin spells
|
|
155
|
+
/// type names as identifiers: in a `user_type`, or as a type parameter's fieldless name (C# puts its
|
|
156
|
+
/// type parameter in a `name` field).
|
|
157
|
+
fn is_variable_leaf(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
158
|
+
let node = leaf.node;
|
|
159
|
+
if node.kind_name() == "type_identifier" {
|
|
160
|
+
return is_member_pointer_name(node);
|
|
161
|
+
}
|
|
166
162
|
VARIABLE_NODE_TYPES.contains(&node.kind_name())
|
|
167
|
-
|
|
168
|
-
|
|
163
|
+
&& !node.parent_node().is_some_and(|parent| {
|
|
164
|
+
parent.kind_name() == "user_type"
|
|
165
|
+
|| (parent.kind_name() == "type_parameter" && leaf.field_name.is_none())
|
|
166
|
+
})
|
|
169
167
|
}
|
|
170
168
|
|
|
171
169
|
fn is_member_pointer_name(node: Node<'_>) -> bool {
|
|
@@ -232,11 +230,11 @@ fn collect_dep_degree_leaves<'t>(
|
|
|
232
230
|
) {
|
|
233
231
|
if matches!(
|
|
234
232
|
node.kind_name(),
|
|
235
|
-
"comment" | "line_comment" | "block_comment"
|
|
233
|
+
"comment" | "line_comment" | "block_comment"
|
|
236
234
|
) {
|
|
237
235
|
return;
|
|
238
236
|
}
|
|
239
|
-
if
|
|
237
|
+
if node.child_count() == 0 {
|
|
240
238
|
leaves.push(DepDegreeLeaf {
|
|
241
239
|
node,
|
|
242
240
|
field_name,
|
|
@@ -297,7 +295,9 @@ fn is_structural_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
|
297
295
|
let Some(parent) = leaf.node.parent_node() else {
|
|
298
296
|
return false;
|
|
299
297
|
};
|
|
300
|
-
|
|
298
|
+
// C#'s `variable_declaration` holds its type in the `type` field; Kotlin's bound name has none.
|
|
299
|
+
if leaf.node.kind_name() == "identifier"
|
|
300
|
+
&& leaf.field_name.is_none()
|
|
301
301
|
&& KOTLIN_DEFINITION_PARENT_TYPES.contains(&parent.kind_name())
|
|
302
302
|
{
|
|
303
303
|
return true;
|
|
@@ -368,18 +368,6 @@ fn unwrap_declarator_wrappers<'t>(leaf: &DepDegreeLeaf<'t>) -> (Node<'t>, Option
|
|
|
368
368
|
/// (C/C++ function-pointer or array parameters) is a parameter-ish node, or the identifier
|
|
369
369
|
/// directly occupies a parameter field; type annotations and default values bind nothing.
|
|
370
370
|
fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
371
|
-
// Kotlin has no `type`/`value` fields to veto default values: a function parameter's default
|
|
372
|
-
// sits in the parameter list (`fun f(b: Int = a)`), a class parameter's inside the parameter
|
|
373
|
-
// node (`class A(val y: Int = a)`), so only a parameter node's first identifier child binds.
|
|
374
|
-
if leaf.node.kind_name() == "simple_identifier" {
|
|
375
|
-
return leaf.node.parent_node().is_some_and(|parent| {
|
|
376
|
-
KOTLIN_PARAMETER_TYPES.contains(&parent.kind_name())
|
|
377
|
-
&& crate::util::named_children(parent)
|
|
378
|
-
.into_iter()
|
|
379
|
-
.find(|child| child.kind_name() == "simple_identifier")
|
|
380
|
-
.is_some_and(|first| first.id() == leaf.node.id())
|
|
381
|
-
});
|
|
382
|
-
}
|
|
383
371
|
let mut current = leaf.node;
|
|
384
372
|
let mut depth = 0usize;
|
|
385
373
|
// Only the member pointer's own declared name may climb past its qualified name; a size or
|
|
@@ -390,6 +378,19 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
|
390
378
|
let Some(parent) = current.parent_node() else {
|
|
391
379
|
return false;
|
|
392
380
|
};
|
|
381
|
+
// Kotlin has no `type`/`value` fields to veto default values: a function parameter's default
|
|
382
|
+
// sits in the parameter list (`fun f(b: Int = a)`), a class parameter's inside the parameter
|
|
383
|
+
// node (`class A(val y: Int = a)`), so only such a node's first identifier child binds.
|
|
384
|
+
if parent.kind_name() == "function_value_parameters" {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
if is_kotlin_parameter(parent) {
|
|
388
|
+
return depth == 0
|
|
389
|
+
&& crate::util::named_children(parent)
|
|
390
|
+
.into_iter()
|
|
391
|
+
.find(|child| child.kind_name() == "identifier")
|
|
392
|
+
.is_some_and(|first| first.id() == leaf.node.id());
|
|
393
|
+
}
|
|
393
394
|
let parent_is_parameterish = parent.kind_name().contains("parameter");
|
|
394
395
|
// Beyond the grandparent, only declarator wrappers keep climbing, plus the
|
|
395
396
|
// `qualified_identifier` nodes that spell a member-pointer parameter's class (`int C::* q`,
|
|
@@ -427,6 +428,16 @@ fn is_parameter_definition(leaf: &DepDegreeLeaf<'_>) -> bool {
|
|
|
427
428
|
}
|
|
428
429
|
}
|
|
429
430
|
|
|
431
|
+
/// A Kotlin class parameter, or a setter's parameter list (`set(value)`; Ruby's `setter` is a method
|
|
432
|
+
/// name, never under a property).
|
|
433
|
+
fn is_kotlin_parameter(node: Node<'_>) -> bool {
|
|
434
|
+
node.kind_name() == "class_parameter"
|
|
435
|
+
|| (node.kind_name() == "setter"
|
|
436
|
+
&& node
|
|
437
|
+
.parent_node()
|
|
438
|
+
.is_some_and(|property| property.kind_name() == "property_declaration"))
|
|
439
|
+
}
|
|
440
|
+
|
|
430
441
|
/// Only called with small-arity parents (declarator wrappers, definition-list holders).
|
|
431
442
|
fn field_name_in_parent(node: Node<'_>, parent: Node<'_>) -> Option<&'static str> {
|
|
432
443
|
for index in 0..parent.child_count() {
|