@pythonidaer/complexity-report 1.0.2

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 (57) hide show
  1. package/CHANGELOG.md +122 -0
  2. package/LICENSE +21 -0
  3. package/README.md +103 -0
  4. package/assets/prettify.css +1 -0
  5. package/assets/prettify.js +2 -0
  6. package/assets/sort-arrow-sprite.png +0 -0
  7. package/complexity-breakdown.js +53 -0
  8. package/decision-points/ast-utils.js +127 -0
  9. package/decision-points/decision-type.js +92 -0
  10. package/decision-points/function-matching.js +185 -0
  11. package/decision-points/in-params.js +262 -0
  12. package/decision-points/index.js +6 -0
  13. package/decision-points/node-helpers.js +89 -0
  14. package/decision-points/parent-map.js +62 -0
  15. package/decision-points/parse-main.js +101 -0
  16. package/decision-points/ternary-multiline.js +86 -0
  17. package/export-generators/helpers.js +309 -0
  18. package/export-generators/index.js +143 -0
  19. package/export-generators/md-exports.js +160 -0
  20. package/export-generators/txt-exports.js +262 -0
  21. package/function-boundaries/arrow-brace-body.js +302 -0
  22. package/function-boundaries/arrow-helpers.js +93 -0
  23. package/function-boundaries/arrow-jsx.js +73 -0
  24. package/function-boundaries/arrow-object-literal.js +65 -0
  25. package/function-boundaries/arrow-single-expr.js +72 -0
  26. package/function-boundaries/brace-scanning.js +151 -0
  27. package/function-boundaries/index.js +67 -0
  28. package/function-boundaries/named-helpers.js +227 -0
  29. package/function-boundaries/parse-utils.js +456 -0
  30. package/function-extraction/ast-utils.js +112 -0
  31. package/function-extraction/extract-callback.js +65 -0
  32. package/function-extraction/extract-from-eslint.js +91 -0
  33. package/function-extraction/extract-name-ast.js +133 -0
  34. package/function-extraction/extract-name-regex.js +267 -0
  35. package/function-extraction/index.js +6 -0
  36. package/function-extraction/utils.js +29 -0
  37. package/function-hierarchy.js +427 -0
  38. package/html-generators/about.js +75 -0
  39. package/html-generators/file-boundary-builders.js +36 -0
  40. package/html-generators/file-breakdown.js +412 -0
  41. package/html-generators/file-data.js +50 -0
  42. package/html-generators/file-helpers.js +100 -0
  43. package/html-generators/file-javascript.js +430 -0
  44. package/html-generators/file-line-render.js +160 -0
  45. package/html-generators/file.css +370 -0
  46. package/html-generators/file.js +207 -0
  47. package/html-generators/folder.js +424 -0
  48. package/html-generators/index.js +6 -0
  49. package/html-generators/main-index.js +346 -0
  50. package/html-generators/shared.css +471 -0
  51. package/html-generators/utils.js +15 -0
  52. package/index.js +36 -0
  53. package/integration/eslint/index.js +94 -0
  54. package/integration/threshold/index.js +45 -0
  55. package/package.json +64 -0
  56. package/report/cli.js +58 -0
  57. package/report/index.js +559 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,122 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.2] - 2026-02-08
9
+
10
+ ### Changed
11
+ - **Package renamed to `@pythonidaer/complexity-report`** — The unscoped name `complexity-report` is already taken on npm by another package. Install with: `npm install --save-dev @pythonidaer/complexity-report`. The CLI command remains `complexity-report`.
12
+ - README and badges updated for scoped package name.
13
+
14
+ ## [1.0.1] - 2026-02-08
15
+
16
+ ### Fixed
17
+ - Repository URL normalized for npm (`git+https://` prefix)
18
+
19
+ ## [1.0.0] - 2026-02-08
20
+
21
+ ### Added
22
+ - Initial release as standalone npm package
23
+ - CLI tool via `npx complexity-report`
24
+ - Programmatic API with `generateComplexityReport()`
25
+ - AST-based decision point analysis (100% accuracy)
26
+ - Interactive HTML reports with syntax highlighting
27
+ - Folder and file-level complexity breakdowns
28
+ - Hierarchical function display for nested callbacks
29
+ - TXT/MD export formats
30
+ - Support for JavaScript and TypeScript
31
+ - ESLint 9+ flat config integration
32
+ - Configurable complexity thresholds
33
+ - Multiple CLI flags for customization
34
+ - Clean public API with utility functions
35
+
36
+ ### Features
37
+ - **100% Accurate** - Uses `@typescript-eslint/typescript-estree` for perfect ESLint alignment
38
+ - **Interactive Reports** - Sortable tables, filterable views, code annotations
39
+ - **Decision Point Breakdown** - See exactly which lines add complexity
40
+ - **Multiple Export Formats** - TXT and Markdown for documentation
41
+ - **Fast Analysis** - Processes hundreds of files in seconds
42
+ - **Comprehensive Testing** - 287 tests with 90%+ coverage
43
+
44
+ ### Migration
45
+ - Extracted from `new-years-project` scripts directory
46
+ - Now available as standalone package
47
+ - Maintains backward compatibility with previous script-based usage
48
+
49
+ ### Requirements
50
+ - Node.js >=18
51
+ - ESLint >=9.0.0 with flat config
52
+
53
+ ## [Unreleased]
54
+
55
+ ### Planned
56
+ - VS Code extension integration
57
+ - GitHub Action for CI/CD
58
+ - Trend analysis over time
59
+ - Comparison reports between branches
60
+ - Additional export formats (JSON, CSV)
61
+ - Performance optimizations for very large codebases
62
+
63
+ ---
64
+
65
+ ## Migration from Scripts
66
+
67
+ If you were using the complexity report from `new-years-project/scripts/`, follow these steps:
68
+
69
+ ### 1. Install the Package
70
+
71
+ ```bash
72
+ npm install --save-dev @pythonidaer/complexity-report
73
+ ```
74
+
75
+ ### 2. Update Scripts in package.json
76
+
77
+ Replace:
78
+ ```json
79
+ {
80
+ "scripts": {
81
+ "lint:complexity": "node scripts/report/index.js"
82
+ }
83
+ }
84
+ ```
85
+
86
+ With:
87
+ ```json
88
+ {
89
+ "scripts": {
90
+ "lint:complexity": "complexity-report"
91
+ }
92
+ }
93
+ ```
94
+ (The CLI command is still `complexity-report`; only the package name is scoped.)
95
+
96
+ ### 3. Keep Your Configuration
97
+
98
+ If you have `complexityReport.exportDir` in your `package.json`, keep it - the package reads this configuration.
99
+
100
+ ### 4. Remove Old Scripts (Optional)
101
+
102
+ Once verified working, you can remove the `scripts/` directory from your project.
103
+
104
+ ### What Changed?
105
+
106
+ - **Installation**: Now via npm instead of local scripts
107
+ - **Usage**: `complexity-report` instead of `node scripts/report/index.js`
108
+ - **Output**: Same location (`complexity/` by default)
109
+ - **Configuration**: Same (`eslint.config.js` and `package.json`)
110
+ - **Features**: All features preserved, plus new programmatic API
111
+
112
+ ### What Stayed the Same?
113
+
114
+ - HTML report format and features
115
+ - Decision point analysis accuracy
116
+ - Export formats and locations
117
+ - ESLint configuration integration
118
+ - Complexity threshold reading
119
+
120
+ ---
121
+
122
+ For more details, see [Migration Guide](./docs/MIGRATION.md)
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jonathan M. Hammond
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # @pythonidaer/complexity-report
2
+
3
+ > AST-based cyclomatic complexity analyzer with interactive HTML reports and detailed function breakdowns
4
+
5
+ [![npm version](https://img.shields.io/npm/v/@pythonidaer/complexity-report.svg)](https://www.npmjs.com/package/@pythonidaer/complexity-report)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
7
+
8
+ ## Features
9
+
10
+ - 🎯 **100% Accurate** - Uses ESLint's AST parser for perfect complexity calculations
11
+ - 📊 **Interactive HTML Reports** - Beautiful, sortable tables with file-by-file breakdowns
12
+ - 🔍 **Decision Point Analysis** - See exactly which lines contribute to complexity
13
+ - 📈 **Hierarchical Function Display** - Understand nested callback complexity
14
+ - 📝 **Multiple Export Formats** - TXT and Markdown exports for documentation
15
+ - ⚡ **Fast** - Analyzes hundreds of files in seconds
16
+ - 🎨 **Syntax Highlighting** - Code annotations with prettify.js
17
+
18
+ ## Installation
19
+
20
+ \`\`\`bash
21
+ npm install --save-dev @pythonidaer/complexity-report
22
+ \`\`\`
23
+
24
+ ## Requirements
25
+
26
+ - **Node.js**: >=18
27
+ - **ESLint**: >=9.0.0 with flat config (\`eslint.config.js\`)
28
+
29
+ Your project must have an ESLint flat config file. The tool will use your project's ESLint configuration to analyze complexity.
30
+
31
+ ## Quick Start
32
+
33
+ ### CLI Usage
34
+
35
+ Run from your project root:
36
+
37
+ \`\`\`bash
38
+ npx complexity-report
39
+ \`\`\`
40
+
41
+ This generates an interactive HTML report at \`complexity/index.html\`.
42
+
43
+ ### With npm Scripts
44
+
45
+ Add to your \`package.json\`:
46
+
47
+ \`\`\`json
48
+ {
49
+ "scripts": {
50
+ "complexity": "complexity-report",
51
+ "complexity:export": "complexity-report --export"
52
+ }
53
+ }
54
+ ```
55
+
56
+ Then run:
57
+
58
+ \`\`\`bash
59
+ npm run complexity
60
+ \`\`\`
61
+
62
+ ## CLI Options
63
+
64
+ \`\`\`bash
65
+ complexity-report [options]
66
+
67
+ Options:
68
+ --cwd <path> Project root directory (default: process.cwd())
69
+ --output-dir <path> Output directory (default: complexity)
70
+ --show-all Show all functions initially (not just over threshold)
71
+ --show-all-columns Show all breakdown columns initially
72
+ --hide-table Hide breakdown table initially
73
+ --no-lines Hide line numbers initially
74
+ --no-highlights Hide code highlights initially
75
+ --export Generate TXT/MD exports
76
+ \`\`\`
77
+
78
+ ## Programmatic API
79
+
80
+ \`\`\`javascript
81
+ import { generateComplexityReport } from '@pythonidaer/complexity-report';
82
+
83
+ const result = await generateComplexityReport({
84
+ cwd: '/path/to/project',
85
+ outputDir: 'reports/complexity',
86
+ showAllInitially: true,
87
+ shouldExport: true,
88
+ });
89
+
90
+ console.log(\`Generated report in: \${result.complexityDir}\`);
91
+ console.log(\`Total functions: \${result.stats.allFunctionsCount}\`);
92
+ \`\`\`
93
+
94
+ ## Documentation
95
+
96
+ - [Full API Documentation](./docs/API.md)
97
+ - [Developer Guide](./docs/DEVELOPER.md)
98
+ - [Migration Guide](./docs/MIGRATION.md)
99
+ - [Changelog](./CHANGELOG.md)
100
+
101
+ ## License
102
+
103
+ MIT © Johnny Hammond
@@ -0,0 +1 @@
1
+ .pln{color:#000}@media screen{.str{color:#080}.kwd{color:#008}.com{color:#800}.typ{color:#606}.lit{color:#066}.pun,.opn,.clo{color:#660}.tag{color:#008}.atn{color:#606}.atv{color:#080}.dec,.var{color:#606}.fun{color:red}}@media print,projection{.str{color:#060}.kwd{color:#006;font-weight:bold}.com{color:#600;font-style:italic}.typ{color:#404;font-weight:bold}.lit{color:#044}.pun,.opn,.clo{color:#440}.tag{color:#006;font-weight:bold}.atn{color:#404}.atv{color:#060}}pre.prettyprint{padding:2px;border:1px solid #888}ol.linenums{margin-top:0;margin-bottom:0}li.L0,li.L1,li.L2,li.L3,li.L5,li.L6,li.L7,li.L8{list-style-type:none}li.L1,li.L3,li.L5,li.L7,li.L9{background:#eee}
@@ -0,0 +1,2 @@
1
+ /* eslint-disable */
2
+ window.PR_SHOULD_USE_CONTINUATION=true;(function(){var h=["break,continue,do,else,for,if,return,while"];var u=[h,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"];var p=[u,"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"];var l=[p,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"];var x=[p,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"];var R=[x,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"];var r="all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes";var w=[p,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"];var s="caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END";var I=[h,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"];var f=[h,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"];var H=[h,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"];var A=[l,R,w,s+I,f,H];var e=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/;var C="str";var z="kwd";var j="com";var O="typ";var G="lit";var L="pun";var F="pln";var m="tag";var E="dec";var J="src";var P="atn";var n="atv";var N="nocode";var M="(?:^^\\.?|[+-]|\\!|\\!=|\\!==|\\#|\\%|\\%=|&|&&|&&=|&=|\\(|\\*|\\*=|\\+=|\\,|\\-=|\\->|\\/|\\/=|:|::|\\;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|\\?|\\@|\\[|\\^|\\^=|\\^\\^|\\^\\^=|\\{|\\||\\|=|\\|\\||\\|\\|=|\\~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\\s*";function k(Z){var ad=0;var S=false;var ac=false;for(var V=0,U=Z.length;V<U;++V){var ae=Z[V];if(ae.ignoreCase){ac=true}else{if(/[a-z]/i.test(ae.source.replace(/\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi,""))){S=true;ac=false;break}}}var Y={b:8,t:9,n:10,v:11,f:12,r:13};function ab(ah){var ag=ah.charCodeAt(0);if(ag!==92){return ag}var af=ah.charAt(1);ag=Y[af];if(ag){return ag}else{if("0"<=af&&af<="7"){return parseInt(ah.substring(1),8)}else{if(af==="u"||af==="x"){return parseInt(ah.substring(2),16)}else{return ah.charCodeAt(1)}}}}function T(af){if(af<32){return(af<16?"\\x0":"\\x")+af.toString(16)}var ag=String.fromCharCode(af);if(ag==="\\"||ag==="-"||ag==="["||ag==="]"){ag="\\"+ag}return ag}function X(am){var aq=am.substring(1,am.length-1).match(new RegExp("\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]","g"));var ak=[];var af=[];var ao=aq[0]==="^";for(var ar=ao?1:0,aj=aq.length;ar<aj;++ar){var ah=aq[ar];if(/\\[bdsw]/i.test(ah)){ak.push(ah)}else{var ag=ab(ah);var al;if(ar+2<aj&&"-"===aq[ar+1]){al=ab(aq[ar+2]);ar+=2}else{al=ag}af.push([ag,al]);if(!(al<65||ag>122)){if(!(al<65||ag>90)){af.push([Math.max(65,ag)|32,Math.min(al,90)|32])}if(!(al<97||ag>122)){af.push([Math.max(97,ag)&~32,Math.min(al,122)&~32])}}}}af.sort(function(av,au){return(av[0]-au[0])||(au[1]-av[1])});var ai=[];var ap=[NaN,NaN];for(var ar=0;ar<af.length;++ar){var at=af[ar];if(at[0]<=ap[1]+1){ap[1]=Math.max(ap[1],at[1])}else{ai.push(ap=at)}}var an=["["];if(ao){an.push("^")}an.push.apply(an,ak);for(var ar=0;ar<ai.length;++ar){var at=ai[ar];an.push(T(at[0]));if(at[1]>at[0]){if(at[1]+1>at[0]){an.push("-")}an.push(T(at[1]))}}an.push("]");return an.join("")}function W(al){var aj=al.source.match(new RegExp("(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)","g"));var ah=aj.length;var an=[];for(var ak=0,am=0;ak<ah;++ak){var ag=aj[ak];if(ag==="("){++am}else{if("\\"===ag.charAt(0)){var af=+ag.substring(1);if(af&&af<=am){an[af]=-1}}}}for(var ak=1;ak<an.length;++ak){if(-1===an[ak]){an[ak]=++ad}}for(var ak=0,am=0;ak<ah;++ak){var ag=aj[ak];if(ag==="("){++am;if(an[am]===undefined){aj[ak]="(?:"}}else{if("\\"===ag.charAt(0)){var af=+ag.substring(1);if(af&&af<=am){aj[ak]="\\"+an[am]}}}}for(var ak=0,am=0;ak<ah;++ak){if("^"===aj[ak]&&"^"!==aj[ak+1]){aj[ak]=""}}if(al.ignoreCase&&S){for(var ak=0;ak<ah;++ak){var ag=aj[ak];var ai=ag.charAt(0);if(ag.length>=2&&ai==="["){aj[ak]=X(ag)}else{if(ai!=="\\"){aj[ak]=ag.replace(/[a-zA-Z]/g,function(ao){var ap=ao.charCodeAt(0);return"["+String.fromCharCode(ap&~32,ap|32)+"]"})}}}}return aj.join("")}var aa=[];for(var V=0,U=Z.length;V<U;++V){var ae=Z[V];if(ae.global||ae.multiline){throw new Error(""+ae)}aa.push("(?:"+W(ae)+")")}return new RegExp(aa.join("|"),ac?"gi":"g")}function a(V){var U=/(?:^|\s)nocode(?:\s|$)/;var X=[];var T=0;var Z=[];var W=0;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=document.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Y=S&&"pre"===S.substring(0,3);function aa(ab){switch(ab.nodeType){case 1:if(U.test(ab.className)){return}for(var ae=ab.firstChild;ae;ae=ae.nextSibling){aa(ae)}var ad=ab.nodeName;if("BR"===ad||"LI"===ad){X[W]="\n";Z[W<<1]=T++;Z[(W++<<1)|1]=ab}break;case 3:case 4:var ac=ab.nodeValue;if(ac.length){if(!Y){ac=ac.replace(/[ \t\r\n]+/g," ")}else{ac=ac.replace(/\r\n?/g,"\n")}X[W]=ac;Z[W<<1]=T;T+=ac.length;Z[(W++<<1)|1]=ab}break}}aa(V);return{sourceCode:X.join("").replace(/\n$/,""),spans:Z}}function B(S,U,W,T){if(!U){return}var V={sourceCode:U,basePos:S};W(V);T.push.apply(T,V.decorations)}var v=/\S/;function o(S){var V=undefined;for(var U=S.firstChild;U;U=U.nextSibling){var T=U.nodeType;V=(T===1)?(V?S:U):(T===3)?(v.test(U.nodeValue)?S:V):V}return V===S?undefined:V}function g(U,T){var S={};var V;(function(){var ad=U.concat(T);var ah=[];var ag={};for(var ab=0,Z=ad.length;ab<Z;++ab){var Y=ad[ab];var ac=Y[3];if(ac){for(var ae=ac.length;--ae>=0;){S[ac.charAt(ae)]=Y}}var af=Y[1];var aa=""+af;if(!ag.hasOwnProperty(aa)){ah.push(af);ag[aa]=null}}ah.push(/[\0-\uffff]/);V=k(ah)})();var X=T.length;var W=function(ah){var Z=ah.sourceCode,Y=ah.basePos;var ad=[Y,F];var af=0;var an=Z.match(V)||[];var aj={};for(var ae=0,aq=an.length;ae<aq;++ae){var ag=an[ae];var ap=aj[ag];var ai=void 0;var am;if(typeof ap==="string"){am=false}else{var aa=S[ag.charAt(0)];if(aa){ai=ag.match(aa[1]);ap=aa[0]}else{for(var ao=0;ao<X;++ao){aa=T[ao];ai=ag.match(aa[1]);if(ai){ap=aa[0];break}}if(!ai){ap=F}}am=ap.length>=5&&"lang-"===ap.substring(0,5);if(am&&!(ai&&typeof ai[1]==="string")){am=false;ap=J}if(!am){aj[ag]=ap}}var ab=af;af+=ag.length;if(!am){ad.push(Y+ab,ap)}else{var al=ai[1];var ak=ag.indexOf(al);var ac=ak+al.length;if(ai[2]){ac=ag.length-ai[2].length;ak=ac-al.length}var ar=ap.substring(5);B(Y+ab,ag.substring(0,ak),W,ad);B(Y+ab+ak,al,q(ar,al),ad);B(Y+ab+ac,ag.substring(ac),W,ad)}}ah.decorations=ad};return W}function i(T){var W=[],S=[];if(T.tripleQuotedStrings){W.push([C,/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,null,"'\""])}else{if(T.multiLineStrings){W.push([C,/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,null,"'\"`"])}else{W.push([C,/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,null,"\"'"])}}if(T.verbatimStrings){S.push([C,/^@\"(?:[^\"]|\"\")*(?:\"|$)/,null])}var Y=T.hashComments;if(Y){if(T.cStyleComments){if(Y>1){W.push([j,/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,null,"#"])}else{W.push([j,/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,null,"#"])}S.push([C,/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,null])}else{W.push([j,/^#[^\r\n]*/,null,"#"])}}if(T.cStyleComments){S.push([j,/^\/\/[^\r\n]*/,null]);S.push([j,/^\/\*[\s\S]*?(?:\*\/|$)/,null])}if(T.regexLiterals){var X=("/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/");S.push(["lang-regex",new RegExp("^"+M+"("+X+")")])}var V=T.types;if(V){S.push([O,V])}var U=(""+T.keywords).replace(/^ | $/g,"");if(U.length){S.push([z,new RegExp("^(?:"+U.replace(/[\s,]+/g,"|")+")\\b"),null])}W.push([F,/^\s+/,null," \r\n\t\xA0"]);S.push([G,/^@[a-z_$][a-z_$@0-9]*/i,null],[O,/^(?:[@_]?[A-Z]+[a-z][A-Za-z_$@0-9]*|\w+_t\b)/,null],[F,/^[a-z_$][a-z_$@0-9]*/i,null],[G,new RegExp("^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*","i"),null,"0123456789"],[F,/^\\[\s\S]?/,null],[L,/^.[^\s\w\.$@\'\"\`\/\#\\]*/,null]);return g(W,S)}var K=i({keywords:A,hashComments:true,cStyleComments:true,multiLineStrings:true,regexLiterals:true});function Q(V,ag){var U=/(?:^|\s)nocode(?:\s|$)/;var ab=/\r\n?|\n/;var ac=V.ownerDocument;var S;if(V.currentStyle){S=V.currentStyle.whiteSpace}else{if(window.getComputedStyle){S=ac.defaultView.getComputedStyle(V,null).getPropertyValue("white-space")}}var Z=S&&"pre"===S.substring(0,3);var af=ac.createElement("LI");while(V.firstChild){af.appendChild(V.firstChild)}var W=[af];function ae(al){switch(al.nodeType){case 1:if(U.test(al.className)){break}if("BR"===al.nodeName){ad(al);if(al.parentNode){al.parentNode.removeChild(al)}}else{for(var an=al.firstChild;an;an=an.nextSibling){ae(an)}}break;case 3:case 4:if(Z){var am=al.nodeValue;var aj=am.match(ab);if(aj){var ai=am.substring(0,aj.index);al.nodeValue=ai;var ah=am.substring(aj.index+aj[0].length);if(ah){var ak=al.parentNode;ak.insertBefore(ac.createTextNode(ah),al.nextSibling)}ad(al);if(!ai){al.parentNode.removeChild(al)}}}break}}function ad(ak){while(!ak.nextSibling){ak=ak.parentNode;if(!ak){return}}function ai(al,ar){var aq=ar?al.cloneNode(false):al;var ao=al.parentNode;if(ao){var ap=ai(ao,1);var an=al.nextSibling;ap.appendChild(aq);for(var am=an;am;am=an){an=am.nextSibling;ap.appendChild(am)}}return aq}var ah=ai(ak.nextSibling,0);for(var aj;(aj=ah.parentNode)&&aj.nodeType===1;){ah=aj}W.push(ah)}for(var Y=0;Y<W.length;++Y){ae(W[Y])}if(ag===(ag|0)){W[0].setAttribute("value",ag)}var aa=ac.createElement("OL");aa.className="linenums";var X=Math.max(0,((ag-1))|0)||0;for(var Y=0,T=W.length;Y<T;++Y){af=W[Y];af.className="L"+((Y+X)%10);if(!af.firstChild){af.appendChild(ac.createTextNode("\xA0"))}aa.appendChild(af)}V.appendChild(aa)}function D(ac){var aj=/\bMSIE\b/.test(navigator.userAgent);var am=/\n/g;var al=ac.sourceCode;var an=al.length;var V=0;var aa=ac.spans;var T=aa.length;var ah=0;var X=ac.decorations;var Y=X.length;var Z=0;X[Y]=an;var ar,aq;for(aq=ar=0;aq<Y;){if(X[aq]!==X[aq+2]){X[ar++]=X[aq++];X[ar++]=X[aq++]}else{aq+=2}}Y=ar;for(aq=ar=0;aq<Y;){var at=X[aq];var ab=X[aq+1];var W=aq+2;while(W+2<=Y&&X[W+1]===ab){W+=2}X[ar++]=at;X[ar++]=ab;aq=W}Y=X.length=ar;var ae=null;while(ah<T){var af=aa[ah];var S=aa[ah+2]||an;var ag=X[Z];var ap=X[Z+2]||an;var W=Math.min(S,ap);var ak=aa[ah+1];var U;if(ak.nodeType!==1&&(U=al.substring(V,W))){if(aj){U=U.replace(am,"\r")}ak.nodeValue=U;var ai=ak.ownerDocument;var ao=ai.createElement("SPAN");ao.className=X[Z+1];var ad=ak.parentNode;ad.replaceChild(ao,ak);ao.appendChild(ak);if(V<S){aa[ah+1]=ak=ai.createTextNode(al.substring(W,S));ad.insertBefore(ak,ao.nextSibling)}}V=W;if(V>=S){ah+=2}if(V>=ap){Z+=2}}}var t={};function c(U,V){for(var S=V.length;--S>=0;){var T=V[S];if(!t.hasOwnProperty(T)){t[T]=U}else{if(window.console){console.warn("cannot override language handler %s",T)}}}}function q(T,S){if(!(T&&t.hasOwnProperty(T))){T=/^\s*</.test(S)?"default-markup":"default-code"}return t[T]}c(K,["default-code"]);c(g([],[[F,/^[^<?]+/],[E,/^<!\w[^>]*(?:>|$)/],[j,/^<\!--[\s\S]*?(?:-\->|$)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],[L,/^(?:<[%?]|[%?]>)/],["lang-",/^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);c(g([[F,/^[\s]+/,null," \t\r\n"],[n,/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,null,"\"'"]],[[m,/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],[P,/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],[L,/^[=<>\/]+/],["lang-js",/^on\w+\s*=\s*\"([^\"]+)\"/i],["lang-js",/^on\w+\s*=\s*\'([^\']+)\'/i],["lang-js",/^on\w+\s*=\s*([^\"\'>\s]+)/i],["lang-css",/^style\s*=\s*\"([^\"]+)\"/i],["lang-css",/^style\s*=\s*\'([^\']+)\'/i],["lang-css",/^style\s*=\s*([^\"\'>\s]+)/i]]),["in.tag"]);c(g([],[[n,/^[\s\S]+/]]),["uq.val"]);c(i({keywords:l,hashComments:true,cStyleComments:true,types:e}),["c","cc","cpp","cxx","cyc","m"]);c(i({keywords:"null,true,false"}),["json"]);c(i({keywords:R,hashComments:true,cStyleComments:true,verbatimStrings:true,types:e}),["cs"]);c(i({keywords:x,cStyleComments:true}),["java"]);c(i({keywords:H,hashComments:true,multiLineStrings:true}),["bsh","csh","sh"]);c(i({keywords:I,hashComments:true,multiLineStrings:true,tripleQuotedStrings:true}),["cv","py"]);c(i({keywords:s,hashComments:true,multiLineStrings:true,regexLiterals:true}),["perl","pl","pm"]);c(i({keywords:f,hashComments:true,multiLineStrings:true,regexLiterals:true}),["rb"]);c(i({keywords:w,cStyleComments:true,regexLiterals:true}),["js"]);c(i({keywords:r,hashComments:3,cStyleComments:true,multilineStrings:true,tripleQuotedStrings:true,regexLiterals:true}),["coffee"]);c(g([],[[C,/^[\s\S]+/]]),["regex"]);function d(V){var U=V.langExtension;try{var S=a(V.sourceNode);var T=S.sourceCode;V.sourceCode=T;V.spans=S.spans;V.basePos=0;q(U,T)(V);D(V)}catch(W){if("console" in window){console.log(W&&W.stack?W.stack:W)}}}function y(W,V,U){var S=document.createElement("PRE");S.innerHTML=W;if(U){Q(S,U)}var T={langExtension:V,numberLines:U,sourceNode:S};d(T);return S.innerHTML}function b(ad){function Y(af){return document.getElementsByTagName(af)}var ac=[Y("pre"),Y("code"),Y("xmp")];var T=[];for(var aa=0;aa<ac.length;++aa){for(var Z=0,V=ac[aa].length;Z<V;++Z){T.push(ac[aa][Z])}}ac=null;var W=Date;if(!W.now){W={now:function(){return +(new Date)}}}var X=0;var S;var ab=/\blang(?:uage)?-([\w.]+)(?!\S)/;var ae=/\bprettyprint\b/;function U(){var ag=(window.PR_SHOULD_USE_CONTINUATION?W.now()+250:Infinity);for(;X<T.length&&W.now()<ag;X++){var aj=T[X];var ai=aj.className;if(ai.indexOf("prettyprint")>=0){var ah=ai.match(ab);var am;if(!ah&&(am=o(aj))&&"CODE"===am.tagName){ah=am.className.match(ab)}if(ah){ah=ah[1]}var al=false;for(var ak=aj.parentNode;ak;ak=ak.parentNode){if((ak.tagName==="pre"||ak.tagName==="code"||ak.tagName==="xmp")&&ak.className&&ak.className.indexOf("prettyprint")>=0){al=true;break}}if(!al){var af=aj.className.match(/\blinenums\b(?::(\d+))?/);af=af?af[1]&&af[1].length?+af[1]:true:false;if(af){Q(aj,af)}S={langExtension:ah,sourceNode:aj,numberLines:af};d(S)}}}if(X<T.length){setTimeout(U,250)}else{if(ad){ad()}}}U()}window.prettyPrintOne=y;window.prettyPrint=b;window.PR={createSimpleLexer:g,registerLangHandler:c,sourceDecorator:i,PR_ATTRIB_NAME:P,PR_ATTRIB_VALUE:n,PR_COMMENT:j,PR_DECLARATION:E,PR_KEYWORD:z,PR_LITERAL:G,PR_NOCODE:N,PR_PLAIN:F,PR_PUNCTUATION:L,PR_SOURCE:J,PR_STRING:C,PR_TAG:m,PR_TYPE:O}})();PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_DECLARATION,/^<!\w[^>]*(?:>|$)/],[PR.PR_COMMENT,/^<\!--[\s\S]*?(?:-\->|$)/],[PR.PR_PUNCTUATION,/^(?:<[%?]|[%?]>)/],["lang-",/^<\?([\s\S]+?)(?:\?>|$)/],["lang-",/^<%([\s\S]+?)(?:%>|$)/],["lang-",/^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],["lang-handlebars",/^<script\b[^>]*type\s*=\s*['"]?text\/x-handlebars-template['"]?\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-js",/^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i],[PR.PR_DECLARATION,/^{{[#^>/]?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{&?\s*[\w.][^}]*}}/],[PR.PR_DECLARATION,/^{{{>?\s*[\w.][^}]*}}}/],[PR.PR_COMMENT,/^{{![^}]*}}/]]),["handlebars","hbs"]);PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null," \t\r\n\f"]],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],["lang-css-str",/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:<!--|-->)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),["css-str"]);
Binary file
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Calculates complexity breakdown for a specific function
3
+ * @param {number} functionLine - Line number of the function
4
+ * @param {Array} decisionPoints - All decision points
5
+ * @param {number} baseComplexity - Base complexity (should be 1)
6
+ * @returns {Object} Breakdown, total, and decision points
7
+ */
8
+ export function calculateComplexityBreakdown(
9
+ functionLine,
10
+ decisionPoints,
11
+ baseComplexity
12
+ ) {
13
+ const functionDecisionPoints = decisionPoints.filter(
14
+ (dp) => dp.functionLine === functionLine
15
+ );
16
+
17
+ const breakdown = {
18
+ base: baseComplexity || 1,
19
+ 'if': 0,
20
+ 'else if': 0,
21
+ 'for': 0,
22
+ 'for...of': 0,
23
+ 'for...in': 0,
24
+ 'while': 0,
25
+ 'do...while': 0,
26
+ 'switch': 0,
27
+ 'case': 0,
28
+ 'catch': 0,
29
+ 'ternary': 0,
30
+ '&&': 0,
31
+ '||': 0,
32
+ '??': 0,
33
+ '?.': 0,
34
+ 'default parameter': 0,
35
+ };
36
+
37
+ functionDecisionPoints.forEach((dp) => {
38
+ if (Object.hasOwn(breakdown, dp.type)) {
39
+ breakdown[dp.type] += 1;
40
+ }
41
+ });
42
+
43
+ const calculatedTotal = Object.values(breakdown).reduce(
44
+ (sum, count) => sum + count,
45
+ 0
46
+ );
47
+
48
+ return {
49
+ breakdown,
50
+ calculatedTotal,
51
+ decisionPoints: functionDecisionPoints,
52
+ };
53
+ }
@@ -0,0 +1,127 @@
1
+ /**
2
+ * AST traversal and parsing utilities for decision-points.
3
+ */
4
+
5
+ import { parse } from '@typescript-eslint/typescript-estree';
6
+
7
+ /**
8
+ * Parses source code into an AST
9
+ * @param {string} sourceCode - Source code to parse
10
+ * @param {string} filePath - Path to the file (for parser context)
11
+ * @returns {Object} Parsed AST
12
+ */
13
+ export function parseAST(sourceCode, filePath) {
14
+ try {
15
+ const isTSX = filePath.endsWith('.tsx') || filePath.endsWith('.jsx');
16
+ return parse(sourceCode, {
17
+ sourceType: 'module',
18
+ ecmaVersion: 2020,
19
+ jsx: isTSX,
20
+ filePath: filePath,
21
+ comment: true,
22
+ loc: true,
23
+ range: true,
24
+ });
25
+ } catch (error) {
26
+ console.error(`Error parsing AST for ${filePath}:`, error.message);
27
+ return null;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Checks if a key should be skipped during AST traversal
33
+ * @param {string} key - Key to check
34
+ * @returns {boolean} True if key should be skipped
35
+ */
36
+ export function shouldSkipKey(key) {
37
+ return key === 'parent' || key === 'range' || key === 'loc' ||
38
+ key === 'leadingComments' || key === 'trailingComments';
39
+ }
40
+
41
+ /**
42
+ * Processes an array of child nodes
43
+ * @param {Array} array - Array of child nodes
44
+ * @param {string} nodeType - Type of nodes to collect
45
+ * @param {Array} results - Array to collect results
46
+ * @param {Function} collectNodesByType - Collector to call
47
+ */
48
+ export function processArrayChildren(array, nodeType, results, collectNodesByType) {
49
+ array.forEach(item => {
50
+ if (item && typeof item === 'object' && item.type) {
51
+ collectNodesByType(item, nodeType, results);
52
+ }
53
+ });
54
+ }
55
+
56
+ /**
57
+ * Processes a single child node
58
+ * @param {Object} child - Child node
59
+ * @param {string} nodeType - Type of nodes to collect
60
+ * @param {Array} results - Array to collect results
61
+ * @param {Function} collectNodesByType - Collector to call
62
+ */
63
+ export function processChildNode(child, nodeType, results, collectNodesByType) {
64
+ if (child && typeof child === 'object' && child.type) {
65
+ collectNodesByType(child, nodeType, results);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Traverses AST and collects all nodes of a specific type
71
+ * @param {Object} node - AST node to traverse
72
+ * @param {string} nodeType - Type of nodes to collect
73
+ * @param {Array} results - Array to collect results
74
+ */
75
+ export function collectNodesByType(node, nodeType, results) {
76
+ if (!node || typeof node !== 'object') return;
77
+
78
+ if (node.type === nodeType) {
79
+ results.push(node);
80
+ }
81
+
82
+ for (const key in node) {
83
+ if (shouldSkipKey(key)) continue;
84
+ const child = node[key];
85
+ if (Array.isArray(child)) {
86
+ processArrayChildren(child, nodeType, results, collectNodesByType);
87
+ } else {
88
+ processChildNode(child, nodeType, results, collectNodesByType);
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * Finds all function nodes in the AST
95
+ * @param {Object} ast - ESLint AST
96
+ * @returns {Array} Array of function nodes
97
+ */
98
+ export function findAllFunctions(ast) {
99
+ const functions = [];
100
+ const functionTypes = [
101
+ 'FunctionDeclaration',
102
+ 'FunctionExpression',
103
+ 'ArrowFunctionExpression',
104
+ 'MethodDefinition',
105
+ ];
106
+
107
+ functionTypes.forEach(type => {
108
+ collectNodesByType(ast, type, functions);
109
+ });
110
+
111
+ return functions;
112
+ }
113
+
114
+ /**
115
+ * Gets the line number for an AST node
116
+ * @param {Object} node - AST node
117
+ * @returns {number} Line number (1-based)
118
+ */
119
+ export function getNodeLine(node) {
120
+ if (node.loc && node.loc.start) {
121
+ return node.loc.start.line;
122
+ }
123
+ if (node.range && node.range[0] !== undefined) {
124
+ return 1;
125
+ }
126
+ return 1;
127
+ }
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Decision point type mapping (control flow, expressions, default params).
3
+ */
4
+
5
+ import { isInFunctionParameters } from './in-params.js';
6
+
7
+ /** Control flow node types that map directly to a decision type (no variant). */
8
+ const CONTROL_FLOW_TYPE_MAP = {
9
+ IfStatement: 'if',
10
+ ForStatement: 'for',
11
+ ForInStatement: 'for',
12
+ ForOfStatement: 'for',
13
+ WhileStatement: 'while',
14
+ DoWhileStatement: 'while',
15
+ SwitchStatement: 'switch',
16
+ CatchClause: 'catch',
17
+ };
18
+
19
+ /**
20
+ * Gets decision point type for control flow statements
21
+ * @param {string} nodeType - Node type
22
+ * @param {string} variant - 'classic' or 'modified'
23
+ * @returns {string|null} Decision point type or null
24
+ */
25
+ export function getControlFlowDecisionType(nodeType, variant) {
26
+ if (nodeType === 'SwitchCase') {
27
+ return variant === 'modified' ? null : 'case';
28
+ }
29
+ return CONTROL_FLOW_TYPE_MAP[nodeType] ?? null;
30
+ }
31
+
32
+ /**
33
+ * Gets decision point type for logical expressions
34
+ * @param {Object} node - AST node
35
+ * @returns {string|null} Decision point type or null
36
+ */
37
+ export function getLogicalExpressionType(node) {
38
+ if (node.operator === '&&') return '&&';
39
+ if (node.operator === '||') return '||';
40
+ if (node.operator === '??') return '??';
41
+ return null;
42
+ }
43
+
44
+ /**
45
+ * Gets decision point type for expressions
46
+ * @param {Object} node - AST node
47
+ * @param {string} nodeType - Node type
48
+ * @returns {string|null} Decision point type or null
49
+ */
50
+ export function getExpressionDecisionType(node, nodeType) {
51
+ if (nodeType === 'ConditionalExpression') {
52
+ return 'ternary';
53
+ }
54
+ if (nodeType === 'LogicalExpression') {
55
+ return getLogicalExpressionType(node);
56
+ }
57
+ if (nodeType === 'MemberExpression' && node.optional) {
58
+ return '?.';
59
+ }
60
+ if (nodeType === 'BinaryExpression' && node.operator === '??') {
61
+ return '??';
62
+ }
63
+ return null;
64
+ }
65
+
66
+ /**
67
+ * Checks if a node represents a decision point
68
+ * @param {Object} node - AST node
69
+ * @param {Map} parentMap - Parent map for checking context
70
+ * @param {Object} ast - Root AST node (for checking function context)
71
+ * @param {string} variant - 'classic' or 'modified'
72
+ * @returns {string|null} Decision point type or null
73
+ */
74
+ export function getDecisionPointType(node, parentMap, ast, variant) {
75
+ if (!node || !node.type) return null;
76
+
77
+ const nodeType = node.type;
78
+
79
+ const controlFlowType = getControlFlowDecisionType(nodeType, variant);
80
+ if (controlFlowType) return controlFlowType;
81
+
82
+ const expressionType = getExpressionDecisionType(node, nodeType);
83
+ if (expressionType) return expressionType;
84
+
85
+ if (nodeType === 'AssignmentPattern' && parentMap) {
86
+ if (isInFunctionParameters(node, parentMap, ast)) {
87
+ return 'default parameter';
88
+ }
89
+ }
90
+
91
+ return null;
92
+ }