diff --git a/.gitignore b/.gitignore index 0b9f05da..97ecdaeb 100644 --- a/.gitignore +++ b/.gitignore @@ -13,4 +13,5 @@ examples/csharp/EndpointExample/bin auditResponse.json .extracted/ napi-output/ -coverage/ \ No newline at end of file +coverage/ +.vite/ diff --git a/deno.lock b/deno.lock index 3f27d527..785764dc 100644 --- a/deno.lock +++ b/deno.lock @@ -1,5 +1,5 @@ { - "version": "5", + "version": "4", "specifiers": { "jsr:@oak/commons@1": "1.0.1", "jsr:@oak/oak@^17.1.4": "17.1.4", @@ -20,9 +20,9 @@ "jsr:@std/path@1": "1.0.9", "jsr:@std/path@^1.0.9": "1.0.9", "jsr:@std/testing@^1.0.12": "1.0.12", - "npm:@deno/vite-plugin@^1.0.4": "1.0.4_vite@6.3.5__picomatch@4.0.2", - "npm:@inquirer/prompts@*": "7.5.1", - "npm:@inquirer/prompts@^7.5.1": "7.5.1", + "npm:@deno/vite-plugin@^1.0.4": "1.0.4_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0", + "npm:@inquirer/prompts@*": "7.5.1_@types+node@22.12.0", + "npm:@inquirer/prompts@^7.5.1": "7.5.1_@types+node@22.12.0", "npm:@radix-ui/react-dialog@^1.1.13": "1.1.13_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0", "npm:@radix-ui/react-dropdown-menu@^2.1.14": "2.1.14_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0", "npm:@radix-ui/react-label@^2.1.6": "2.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0", @@ -33,13 +33,14 @@ "npm:@radix-ui/react-toast@^1.2.13": "1.2.13_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0", "npm:@radix-ui/react-tooltip@^1.2.6": "1.2.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0", "npm:@radix-ui/themes@^3.2.1": "3.2.1_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0", - "npm:@tailwindcss/vite@^4.1.5": "4.1.6_vite@6.3.5__picomatch@4.0.2", + "npm:@tailwindcss/vite@^4.1.5": "4.1.6_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0", "npm:@types/cytoscape-fcose@^2.2.4": "2.2.4", + "npm:@types/node@*": "22.12.0", "npm:@types/react-dom@^19.1.3": "19.1.5_@types+react@19.1.4", "npm:@types/react-router@^5.1.20": "5.1.20", "npm:@types/react@^19.1.3": "19.1.4", "npm:@types/yargs@^17.0.33": "17.0.33", - "npm:@vitejs/plugin-react@^4.4.1": "4.4.1_vite@6.3.5__picomatch@4.0.2_@babel+core@7.27.1", + "npm:@vitejs/plugin-react@^4.4.1": "4.4.1_vite@6.3.5__picomatch@4.0.2_@babel+core@7.27.1_@types+node@22.12.0", "npm:class-variance-authority@~0.7.1": "0.7.1", "npm:clsx@^2.1.1": "2.1.1", "npm:cytoscape-fcose@^2.2.0": "2.2.0_cytoscape@3.31.1", @@ -56,13 +57,14 @@ "npm:tailwindcss@^4.1.5": "4.1.6", "npm:tree-sitter-c-sharp@*": "0.23.1_tree-sitter@0.22.4", "npm:tree-sitter-c-sharp@~0.23.1": "0.23.1_tree-sitter@0.22.4", + "npm:tree-sitter-c@*": "0.23.5_tree-sitter@0.22.4", "npm:tree-sitter-python@*": "0.23.6_tree-sitter@0.22.4", "npm:tree-sitter-python@~0.23.6": "0.23.6_tree-sitter@0.22.4", "npm:tree-sitter@*": "0.22.4", "npm:tree-sitter@~0.22.4": "0.22.4", "npm:tw-animate-css@^1.2.9": "1.2.9", - "npm:vite@*": "6.3.5_picomatch@4.0.2", - "npm:vite@^6.3.5": "6.3.5_picomatch@4.0.2", + "npm:vite@*": "6.3.5_picomatch@4.0.2_@types+node@22.12.0", + "npm:vite@^6.3.5": "6.3.5_picomatch@4.0.2_@types+node@22.12.0", "npm:yargs@*": "17.7.2", "npm:yargs@^17.7.2": "17.7.2", "npm:zod@*": "3.24.4", @@ -251,8 +253,7 @@ "integrity": "sha512-QYLs8299NA7WM/bZAdp+CviYYkVoYXlDW2rzliy3chxd1PQjej7JORuMJDJXJUb9g0TT+B99EwaVLKmX+sPXWw==", "dependencies": [ "@babel/types" - ], - "bin": true + ] }, "@babel/plugin-transform-react-jsx-self@7.27.1_@babel+core@7.27.1": { "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", @@ -298,7 +299,13 @@ "@deno/vite-plugin@1.0.4_vite@6.3.5__picomatch@4.0.2": { "integrity": "sha512-xg8YT8Wn2sGXSnJgiGTpBGX1Dov0c6fd1rAp8VsfrCUtyBRRWzwVMAnd3fQ4yq8h7LSVvJUxEFN4U421k/DQLA==", "dependencies": [ - "vite" + "vite@6.3.5_picomatch@4.0.2" + ] + }, + "@deno/vite-plugin@1.0.4_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0": { + "integrity": "sha512-xg8YT8Wn2sGXSnJgiGTpBGX1Dov0c6fd1rAp8VsfrCUtyBRRWzwVMAnd3fQ4yq8h7LSVvJUxEFN4U421k/DQLA==", + "dependencies": [ + "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0" ] }, "@emnapi/core@1.4.3": { @@ -321,129 +328,79 @@ ] }, "@esbuild/aix-ppc64@0.25.4": { - "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==", - "os": ["aix"], - "cpu": ["ppc64"] + "integrity": "sha512-1VCICWypeQKhVbE9oW/sJaAmjLxhVqacdkvPLEjwlttjfwENRSClS8EjBz0KzRyFSCPDIkuXW34Je/vk7zdB7Q==" }, "@esbuild/android-arm64@0.25.4": { - "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==", - "os": ["android"], - "cpu": ["arm64"] + "integrity": "sha512-bBy69pgfhMGtCnwpC/x5QhfxAz/cBgQ9enbtwjf6V9lnPI/hMyT9iWpR1arm0l3kttTr4L0KSLpKmLp/ilKS9A==" }, "@esbuild/android-arm@0.25.4": { - "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==", - "os": ["android"], - "cpu": ["arm"] + "integrity": "sha512-QNdQEps7DfFwE3hXiU4BZeOV68HHzYwGd0Nthhd3uCkkEKK7/R6MTgM0P7H7FAs5pU/DIWsviMmEGxEoxIZ+ZQ==" }, "@esbuild/android-x64@0.25.4": { - "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==", - "os": ["android"], - "cpu": ["x64"] + "integrity": "sha512-TVhdVtQIFuVpIIR282btcGC2oGQoSfZfmBdTip2anCaVYcqWlZXGcdcKIUklfX2wj0JklNYgz39OBqh2cqXvcQ==" }, "@esbuild/darwin-arm64@0.25.4": { - "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==", - "os": ["darwin"], - "cpu": ["arm64"] + "integrity": "sha512-Y1giCfM4nlHDWEfSckMzeWNdQS31BQGs9/rouw6Ub91tkK79aIMTH3q9xHvzH8d0wDru5Ci0kWB8b3up/nl16g==" }, "@esbuild/darwin-x64@0.25.4": { - "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==", - "os": ["darwin"], - "cpu": ["x64"] + "integrity": "sha512-CJsry8ZGM5VFVeyUYB3cdKpd/H69PYez4eJh1W/t38vzutdjEjtP7hB6eLKBoOdxcAlCtEYHzQ/PJ/oU9I4u0A==" }, "@esbuild/freebsd-arm64@0.25.4": { - "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==", - "os": ["freebsd"], - "cpu": ["arm64"] + "integrity": "sha512-yYq+39NlTRzU2XmoPW4l5Ifpl9fqSk0nAJYM/V/WUGPEFfek1epLHJIkTQM6bBs1swApjO5nWgvr843g6TjxuQ==" }, "@esbuild/freebsd-x64@0.25.4": { - "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==", - "os": ["freebsd"], - "cpu": ["x64"] + "integrity": "sha512-0FgvOJ6UUMflsHSPLzdfDnnBBVoCDtBTVyn/MrWloUNvq/5SFmh13l3dvgRPkDihRxb77Y17MbqbCAa2strMQQ==" }, "@esbuild/linux-arm64@0.25.4": { - "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==", - "os": ["linux"], - "cpu": ["arm64"] + "integrity": "sha512-+89UsQTfXdmjIvZS6nUnOOLoXnkUTB9hR5QAeLrQdzOSWZvNSAXAtcRDHWtqAUtAmv7ZM1WPOOeSxDzzzMogiQ==" }, "@esbuild/linux-arm@0.25.4": { - "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==", - "os": ["linux"], - "cpu": ["arm"] + "integrity": "sha512-kro4c0P85GMfFYqW4TWOpvmF8rFShbWGnrLqlzp4X1TNWjRY3JMYUfDCtOxPKOIY8B0WC8HN51hGP4I4hz4AaQ==" }, "@esbuild/linux-ia32@0.25.4": { - "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==", - "os": ["linux"], - "cpu": ["ia32"] + "integrity": "sha512-yTEjoapy8UP3rv8dB0ip3AfMpRbyhSN3+hY8mo/i4QXFeDxmiYbEKp3ZRjBKcOP862Ua4b1PDfwlvbuwY7hIGQ==" }, "@esbuild/linux-loong64@0.25.4": { - "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==", - "os": ["linux"], - "cpu": ["loong64"] + "integrity": "sha512-NeqqYkrcGzFwi6CGRGNMOjWGGSYOpqwCjS9fvaUlX5s3zwOtn1qwg1s2iE2svBe4Q/YOG1q6875lcAoQK/F4VA==" }, "@esbuild/linux-mips64el@0.25.4": { - "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==", - "os": ["linux"], - "cpu": ["mips64el"] + "integrity": "sha512-IcvTlF9dtLrfL/M8WgNI/qJYBENP3ekgsHbYUIzEzq5XJzzVEV/fXY9WFPfEEXmu3ck2qJP8LG/p3Q8f7Zc2Xg==" }, "@esbuild/linux-ppc64@0.25.4": { - "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==", - "os": ["linux"], - "cpu": ["ppc64"] + "integrity": "sha512-HOy0aLTJTVtoTeGZh4HSXaO6M95qu4k5lJcH4gxv56iaycfz1S8GO/5Jh6X4Y1YiI0h7cRyLi+HixMR+88swag==" }, "@esbuild/linux-riscv64@0.25.4": { - "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==", - "os": ["linux"], - "cpu": ["riscv64"] + "integrity": "sha512-i8JUDAufpz9jOzo4yIShCTcXzS07vEgWzyX3NH2G7LEFVgrLEhjwL3ajFE4fZI3I4ZgiM7JH3GQ7ReObROvSUA==" }, "@esbuild/linux-s390x@0.25.4": { - "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==", - "os": ["linux"], - "cpu": ["s390x"] + "integrity": "sha512-jFnu+6UbLlzIjPQpWCNh5QtrcNfMLjgIavnwPQAfoGx4q17ocOU9MsQ2QVvFxwQoWpZT8DvTLooTvmOQXkO51g==" }, "@esbuild/linux-x64@0.25.4": { - "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==", - "os": ["linux"], - "cpu": ["x64"] + "integrity": "sha512-6e0cvXwzOnVWJHq+mskP8DNSrKBr1bULBvnFLpc1KY+d+irZSgZ02TGse5FsafKS5jg2e4pbvK6TPXaF/A6+CA==" }, "@esbuild/netbsd-arm64@0.25.4": { - "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==", - "os": ["netbsd"], - "cpu": ["arm64"] + "integrity": "sha512-vUnkBYxZW4hL/ie91hSqaSNjulOnYXE1VSLusnvHg2u3jewJBz3YzB9+oCw8DABeVqZGg94t9tyZFoHma8gWZQ==" }, "@esbuild/netbsd-x64@0.25.4": { - "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==", - "os": ["netbsd"], - "cpu": ["x64"] + "integrity": "sha512-XAg8pIQn5CzhOB8odIcAm42QsOfa98SBeKUdo4xa8OvX8LbMZqEtgeWE9P/Wxt7MlG2QqvjGths+nq48TrUiKw==" }, "@esbuild/openbsd-arm64@0.25.4": { - "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==", - "os": ["openbsd"], - "cpu": ["arm64"] + "integrity": "sha512-Ct2WcFEANlFDtp1nVAXSNBPDxyU+j7+tId//iHXU2f/lN5AmO4zLyhDcpR5Cz1r08mVxzt3Jpyt4PmXQ1O6+7A==" }, "@esbuild/openbsd-x64@0.25.4": { - "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==", - "os": ["openbsd"], - "cpu": ["x64"] + "integrity": "sha512-xAGGhyOQ9Otm1Xu8NT1ifGLnA6M3sJxZ6ixylb+vIUVzvvd6GOALpwQrYrtlPouMqd/vSbgehz6HaVk4+7Afhw==" }, "@esbuild/sunos-x64@0.25.4": { - "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==", - "os": ["sunos"], - "cpu": ["x64"] + "integrity": "sha512-Mw+tzy4pp6wZEK0+Lwr76pWLjrtjmJyUB23tHKqEDP74R3q95luY/bXqXZeYl4NYlvwOqoRKlInQialgCKy67Q==" }, "@esbuild/win32-arm64@0.25.4": { - "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==", - "os": ["win32"], - "cpu": ["arm64"] + "integrity": "sha512-AVUP428VQTSddguz9dO9ngb+E5aScyg7nOeJDrF1HPYu555gmza3bDGMPhmVXL8svDSoqPCsCPjb265yG/kLKQ==" }, "@esbuild/win32-ia32@0.25.4": { - "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==", - "os": ["win32"], - "cpu": ["ia32"] + "integrity": "sha512-i1sW+1i+oWvQzSgfRcxxG2k4I9n3O9NRqy8U+uugaT2Dy7kLO9Y7wI72haOahxceMX8hZAzgGou1FhndRldxRg==" }, "@esbuild/win32-x64@0.25.4": { - "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==", - "os": ["win32"], - "cpu": ["x64"] + "integrity": "sha512-nOT2vZNw6hJ+z43oP1SPea/G/6AbN6X+bGNhNuq8NtRHy4wsMhw765IKLNmnjek7GvjWBYQ8Q5VBoYTFg9y1UQ==" }, "@floating-ui/core@1.7.0": { "integrity": "sha512-FRdBLykrPPA6P76GGGqlex/e7fbe0F1ykgxHYNXQsH/iTEtjMj/f9bpY5oQqbjt5VgZvgz/uKXbGuROijh3VLA==", @@ -472,9 +429,20 @@ "@inquirer/checkbox@4.1.6": { "integrity": "sha512-62u896rWCtKKE43soodq5e/QcRsA22I+7/4Ov7LESWnKRO6BVo2A1DFLDmXL9e28TB0CfHc3YtkbPm7iwajqkg==", "dependencies": [ - "@inquirer/core", + "@inquirer/core@10.1.11", "@inquirer/figures", - "@inquirer/type", + "@inquirer/type@3.0.6", + "ansi-escapes", + "yoctocolors-cjs" + ] + }, + "@inquirer/checkbox@4.1.6_@types+node@22.12.0": { + "integrity": "sha512-62u896rWCtKKE43soodq5e/QcRsA22I+7/4Ov7LESWnKRO6BVo2A1DFLDmXL9e28TB0CfHc3YtkbPm7iwajqkg==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/figures", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node", "ansi-escapes", "yoctocolors-cjs" ] @@ -482,15 +450,37 @@ "@inquirer/confirm@5.1.10": { "integrity": "sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g==", "dependencies": [ - "@inquirer/core", - "@inquirer/type" + "@inquirer/core@10.1.11", + "@inquirer/type@3.0.6" + ] + }, + "@inquirer/confirm@5.1.10_@types+node@22.12.0": { + "integrity": "sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node" ] }, "@inquirer/core@10.1.11": { "integrity": "sha512-BXwI/MCqdtAhzNQlBEFE7CEflhPkl/BqvAuV/aK6lW3DClIfYVDWPP/kXuXHtBWC7/EEbNqd/1BGq2BGBBnuxw==", "dependencies": [ "@inquirer/figures", - "@inquirer/type", + "@inquirer/type@3.0.6", + "ansi-escapes", + "cli-width", + "mute-stream", + "signal-exit", + "wrap-ansi@6.2.0", + "yoctocolors-cjs" + ] + }, + "@inquirer/core@10.1.11_@types+node@22.12.0": { + "integrity": "sha512-BXwI/MCqdtAhzNQlBEFE7CEflhPkl/BqvAuV/aK6lW3DClIfYVDWPP/kXuXHtBWC7/EEbNqd/1BGq2BGBBnuxw==", + "dependencies": [ + "@inquirer/figures", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node", "ansi-escapes", "cli-width", "mute-stream", @@ -502,16 +492,34 @@ "@inquirer/editor@4.2.11": { "integrity": "sha512-YoZr0lBnnLFPpfPSNsQ8IZyKxU47zPyVi9NLjCWtna52//M/xuL0PGPAxHxxYhdOhnvY2oBafoM+BI5w/JK7jw==", "dependencies": [ - "@inquirer/core", - "@inquirer/type", + "@inquirer/core@10.1.11", + "@inquirer/type@3.0.6", + "external-editor" + ] + }, + "@inquirer/editor@4.2.11_@types+node@22.12.0": { + "integrity": "sha512-YoZr0lBnnLFPpfPSNsQ8IZyKxU47zPyVi9NLjCWtna52//M/xuL0PGPAxHxxYhdOhnvY2oBafoM+BI5w/JK7jw==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node", "external-editor" ] }, "@inquirer/expand@4.0.13": { "integrity": "sha512-HgYNWuZLHX6q5y4hqKhwyytqAghmx35xikOGY3TcgNiElqXGPas24+UzNPOwGUZa5Dn32y25xJqVeUcGlTv+QQ==", "dependencies": [ - "@inquirer/core", - "@inquirer/type", + "@inquirer/core@10.1.11", + "@inquirer/type@3.0.6", + "yoctocolors-cjs" + ] + }, + "@inquirer/expand@4.0.13_@types+node@22.12.0": { + "integrity": "sha512-HgYNWuZLHX6q5y4hqKhwyytqAghmx35xikOGY3TcgNiElqXGPas24+UzNPOwGUZa5Dn32y25xJqVeUcGlTv+QQ==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node", "yoctocolors-cjs" ] }, @@ -521,63 +529,134 @@ "@inquirer/input@4.1.10": { "integrity": "sha512-kV3BVne3wJ+j6reYQUZi/UN9NZGZLxgc/tfyjeK3mrx1QI7RXPxGp21IUTv+iVHcbP4ytZALF8vCHoxyNSC6qg==", "dependencies": [ - "@inquirer/core", - "@inquirer/type" + "@inquirer/core@10.1.11", + "@inquirer/type@3.0.6" + ] + }, + "@inquirer/input@4.1.10_@types+node@22.12.0": { + "integrity": "sha512-kV3BVne3wJ+j6reYQUZi/UN9NZGZLxgc/tfyjeK3mrx1QI7RXPxGp21IUTv+iVHcbP4ytZALF8vCHoxyNSC6qg==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node" ] }, "@inquirer/number@3.0.13": { "integrity": "sha512-IrLezcg/GWKS8zpKDvnJ/YTflNJdG0qSFlUM/zNFsdi4UKW/CO+gaJpbMgQ20Q58vNKDJbEzC6IebdkprwL6ew==", "dependencies": [ - "@inquirer/core", - "@inquirer/type" + "@inquirer/core@10.1.11", + "@inquirer/type@3.0.6" + ] + }, + "@inquirer/number@3.0.13_@types+node@22.12.0": { + "integrity": "sha512-IrLezcg/GWKS8zpKDvnJ/YTflNJdG0qSFlUM/zNFsdi4UKW/CO+gaJpbMgQ20Q58vNKDJbEzC6IebdkprwL6ew==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node" ] }, "@inquirer/password@4.0.13": { "integrity": "sha512-NN0S/SmdhakqOTJhDwOpeBEEr8VdcYsjmZHDb0rblSh2FcbXQOr+2IApP7JG4WE3sxIdKytDn4ed3XYwtHxmJQ==", "dependencies": [ - "@inquirer/core", - "@inquirer/type", + "@inquirer/core@10.1.11", + "@inquirer/type@3.0.6", + "ansi-escapes" + ] + }, + "@inquirer/password@4.0.13_@types+node@22.12.0": { + "integrity": "sha512-NN0S/SmdhakqOTJhDwOpeBEEr8VdcYsjmZHDb0rblSh2FcbXQOr+2IApP7JG4WE3sxIdKytDn4ed3XYwtHxmJQ==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node", "ansi-escapes" ] }, "@inquirer/prompts@7.5.1": { "integrity": "sha512-5AOrZPf2/GxZ+SDRZ5WFplCA2TAQgK3OYrXCYmJL5NaTu4ECcoWFlfUZuw7Es++6Njv7iu/8vpYJhuzxUH76Vg==", "dependencies": [ - "@inquirer/checkbox", - "@inquirer/confirm", - "@inquirer/editor", - "@inquirer/expand", - "@inquirer/input", - "@inquirer/number", - "@inquirer/password", - "@inquirer/rawlist", - "@inquirer/search", - "@inquirer/select" + "@inquirer/checkbox@4.1.6", + "@inquirer/confirm@5.1.10", + "@inquirer/editor@4.2.11", + "@inquirer/expand@4.0.13", + "@inquirer/input@4.1.10", + "@inquirer/number@3.0.13", + "@inquirer/password@4.0.13", + "@inquirer/rawlist@4.1.1", + "@inquirer/search@3.0.13", + "@inquirer/select@4.2.1" + ] + }, + "@inquirer/prompts@7.5.1_@types+node@22.12.0": { + "integrity": "sha512-5AOrZPf2/GxZ+SDRZ5WFplCA2TAQgK3OYrXCYmJL5NaTu4ECcoWFlfUZuw7Es++6Njv7iu/8vpYJhuzxUH76Vg==", + "dependencies": [ + "@inquirer/checkbox@4.1.6_@types+node@22.12.0", + "@inquirer/confirm@5.1.10_@types+node@22.12.0", + "@inquirer/editor@4.2.11_@types+node@22.12.0", + "@inquirer/expand@4.0.13_@types+node@22.12.0", + "@inquirer/input@4.1.10_@types+node@22.12.0", + "@inquirer/number@3.0.13_@types+node@22.12.0", + "@inquirer/password@4.0.13_@types+node@22.12.0", + "@inquirer/rawlist@4.1.1_@types+node@22.12.0", + "@inquirer/search@3.0.13_@types+node@22.12.0", + "@inquirer/select@4.2.1_@types+node@22.12.0", + "@types/node" ] }, "@inquirer/rawlist@4.1.1": { "integrity": "sha512-VBUC0jPN2oaOq8+krwpo/mf3n/UryDUkKog3zi+oIi8/e5hykvdntgHUB9nhDM78RubiyR1ldIOfm5ue+2DeaQ==", "dependencies": [ - "@inquirer/core", - "@inquirer/type", + "@inquirer/core@10.1.11", + "@inquirer/type@3.0.6", + "yoctocolors-cjs" + ] + }, + "@inquirer/rawlist@4.1.1_@types+node@22.12.0": { + "integrity": "sha512-VBUC0jPN2oaOq8+krwpo/mf3n/UryDUkKog3zi+oIi8/e5hykvdntgHUB9nhDM78RubiyR1ldIOfm5ue+2DeaQ==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node", "yoctocolors-cjs" ] }, "@inquirer/search@3.0.13": { "integrity": "sha512-9g89d2c5Izok/Gw/U7KPC3f9kfe5rA1AJ24xxNZG0st+vWekSk7tB9oE+dJv5JXd0ZSijomvW0KPMoBd8qbN4g==", "dependencies": [ - "@inquirer/core", + "@inquirer/core@10.1.11", "@inquirer/figures", - "@inquirer/type", + "@inquirer/type@3.0.6", + "yoctocolors-cjs" + ] + }, + "@inquirer/search@3.0.13_@types+node@22.12.0": { + "integrity": "sha512-9g89d2c5Izok/Gw/U7KPC3f9kfe5rA1AJ24xxNZG0st+vWekSk7tB9oE+dJv5JXd0ZSijomvW0KPMoBd8qbN4g==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", + "@inquirer/figures", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node", "yoctocolors-cjs" ] }, "@inquirer/select@4.2.1": { "integrity": "sha512-gt1Kd5XZm+/ddemcT3m23IP8aD8rC9drRckWoP/1f7OL46Yy2FGi8DSmNjEjQKtPl6SV96Kmjbl6p713KXJ/Jg==", "dependencies": [ - "@inquirer/core", + "@inquirer/core@10.1.11", + "@inquirer/figures", + "@inquirer/type@3.0.6", + "ansi-escapes", + "yoctocolors-cjs" + ] + }, + "@inquirer/select@4.2.1_@types+node@22.12.0": { + "integrity": "sha512-gt1Kd5XZm+/ddemcT3m23IP8aD8rC9drRckWoP/1f7OL46Yy2FGi8DSmNjEjQKtPl6SV96Kmjbl6p713KXJ/Jg==", + "dependencies": [ + "@inquirer/core@10.1.11_@types+node@22.12.0", "@inquirer/figures", - "@inquirer/type", + "@inquirer/type@3.0.6_@types+node@22.12.0", + "@types/node", "ansi-escapes", "yoctocolors-cjs" ] @@ -585,15 +664,21 @@ "@inquirer/type@3.0.6": { "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==" }, + "@inquirer/type@3.0.6_@types+node@22.12.0": { + "integrity": "sha512-/mKVCtVpyBu3IDarv0G+59KC4stsD5mDsGpYh+GKs1NZT88Jh52+cuoA1AtLk2Q0r/quNl+1cSUyLRHBFeD0XA==", + "dependencies": [ + "@types/node" + ] + }, "@isaacs/cliui@8.0.2": { "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", "dependencies": [ - "string-width@5.1.2", "string-width-cjs@npm:string-width@4.2.3", - "strip-ansi@7.1.0", + "string-width@5.1.2", "strip-ansi-cjs@npm:strip-ansi@6.0.1", - "wrap-ansi@8.1.0", - "wrap-ansi-cjs@npm:wrap-ansi@7.0.0" + "strip-ansi@7.1.0", + "wrap-ansi-cjs@npm:wrap-ansi@7.0.0", + "wrap-ansi@8.1.0" ] }, "@isaacs/fs-minipass@4.0.1": { @@ -651,10 +736,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-accordion@1.2.10_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -673,10 +754,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-alert-dialog@1.1.13_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -692,10 +769,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-arrow@1.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -706,10 +779,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-aspect-ratio@1.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -720,10 +789,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-avatar@1.1.9_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -738,10 +803,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-checkbox@1.3.1_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -759,10 +820,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-collapsible@1.1.10_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -780,10 +837,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-collection@1.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -797,10 +850,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-compose-refs@1.1.2_@types+react@19.1.4_react@19.1.0": { @@ -808,9 +857,6 @@ "dependencies": [ "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-context-menu@2.2.14_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -826,10 +872,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-context@1.1.2_@types+react@19.1.4_react@19.1.0": { @@ -837,9 +879,6 @@ "dependencies": [ "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-dialog@1.1.13_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -863,10 +902,6 @@ "react", "react-dom", "react-remove-scroll" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-direction@1.1.1_@types+react@19.1.4_react@19.1.0": { @@ -874,9 +909,6 @@ "dependencies": [ "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-dismissable-layer@1.1.9_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -891,10 +923,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-dropdown-menu@2.1.14_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -911,10 +939,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-focus-guards@1.1.2_@types+react@19.1.4_react@19.1.0": { @@ -922,9 +946,6 @@ "dependencies": [ "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-focus-scope@1.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -937,10 +958,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-form@0.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -956,10 +973,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-hover-card@1.1.13_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -978,10 +991,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-id@1.1.1_@types+react@19.1.4_react@19.1.0": { @@ -990,9 +999,6 @@ "@radix-ui/react-use-layout-effect", "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-label@2.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1003,10 +1009,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-menu@2.1.14_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1034,10 +1036,6 @@ "react", "react-dom", "react-remove-scroll" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-menubar@1.1.14_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1057,10 +1055,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-navigation-menu@1.2.12_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1084,10 +1078,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-one-time-password-field@0.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1109,10 +1099,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-password-toggle-field@0.1.1_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1130,10 +1116,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-popover@1.1.13_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1158,10 +1140,6 @@ "react", "react-dom", "react-remove-scroll" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-popper@1.2.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1181,10 +1159,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-portal@1.1.8_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1196,10 +1170,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-presence@1.1.4_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1211,10 +1181,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-primitive@2.1.2_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1225,10 +1191,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-progress@1.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1240,10 +1202,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-radio-group@1.3.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1263,10 +1221,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-roving-focus@1.1.9_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1285,10 +1239,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-scroll-area@1.2.8_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1307,10 +1257,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-select@2.2.4_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1341,10 +1287,6 @@ "react", "react-dom", "react-remove-scroll" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-separator@1.1.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1355,10 +1297,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-slider@1.3.4_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1379,10 +1317,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-slot@1.2.2_@types+react@19.1.4_react@19.1.0": { @@ -1391,9 +1325,6 @@ "@radix-ui/react-compose-refs", "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-switch@1.2.4_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1410,10 +1341,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-tabs@1.1.11_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1431,10 +1358,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-toast@1.2.13_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1456,10 +1379,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-toggle-group@1.1.9_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1476,10 +1395,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-toggle@1.1.8_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1492,10 +1407,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-toolbar@1.1.9_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1512,10 +1423,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-tooltip@1.2.6_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1537,10 +1444,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/react-use-callback-ref@1.1.1_@types+react@19.1.4_react@19.1.0": { @@ -1548,9 +1451,6 @@ "dependencies": [ "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-use-controllable-state@1.2.2_@types+react@19.1.4_react@19.1.0": { @@ -1560,9 +1460,6 @@ "@radix-ui/react-use-layout-effect", "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-use-effect-event@0.0.2_@types+react@19.1.4_react@19.1.0": { @@ -1571,9 +1468,6 @@ "@radix-ui/react-use-layout-effect", "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-use-escape-keydown@1.1.1_@types+react@19.1.4_react@19.1.0": { @@ -1582,9 +1476,6 @@ "@radix-ui/react-use-callback-ref", "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-use-is-hydrated@0.1.0_@types+react@19.1.4_react@19.1.0": { @@ -1593,9 +1484,6 @@ "@types/react", "react", "use-sync-external-store" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-use-layout-effect@1.1.1_@types+react@19.1.4_react@19.1.0": { @@ -1603,9 +1491,6 @@ "dependencies": [ "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-use-previous@1.1.1_@types+react@19.1.4_react@19.1.0": { @@ -1613,9 +1498,6 @@ "dependencies": [ "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-use-rect@1.1.1_@types+react@19.1.4_react@19.1.0": { @@ -1624,9 +1506,6 @@ "@radix-ui/rect", "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-use-size@1.1.1_@types+react@19.1.4_react@19.1.0": { @@ -1635,9 +1514,6 @@ "@radix-ui/react-use-layout-effect", "@types/react", "react" - ], - "optionalPeers": [ - "@types/react" ] }, "@radix-ui/react-visually-hidden@1.2.2_@types+react@19.1.4_@types+react-dom@19.1.5__@types+react@19.1.4_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -1648,10 +1524,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@radix-ui/rect@1.1.1": { @@ -1668,111 +1540,67 @@ "react", "react-dom", "react-remove-scroll-bar" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "@rollup/rollup-android-arm-eabi@4.40.2": { - "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", - "os": ["android"], - "cpu": ["arm"] + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==" }, "@rollup/rollup-android-arm64@4.40.2": { - "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", - "os": ["android"], - "cpu": ["arm64"] + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==" }, "@rollup/rollup-darwin-arm64@4.40.2": { - "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", - "os": ["darwin"], - "cpu": ["arm64"] + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==" }, "@rollup/rollup-darwin-x64@4.40.2": { - "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", - "os": ["darwin"], - "cpu": ["x64"] + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==" }, "@rollup/rollup-freebsd-arm64@4.40.2": { - "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", - "os": ["freebsd"], - "cpu": ["arm64"] + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==" }, "@rollup/rollup-freebsd-x64@4.40.2": { - "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", - "os": ["freebsd"], - "cpu": ["x64"] + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==" }, "@rollup/rollup-linux-arm-gnueabihf@4.40.2": { - "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", - "os": ["linux"], - "cpu": ["arm"] + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==" }, "@rollup/rollup-linux-arm-musleabihf@4.40.2": { - "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", - "os": ["linux"], - "cpu": ["arm"] + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==" }, "@rollup/rollup-linux-arm64-gnu@4.40.2": { - "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", - "os": ["linux"], - "cpu": ["arm64"] + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==" }, "@rollup/rollup-linux-arm64-musl@4.40.2": { - "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", - "os": ["linux"], - "cpu": ["arm64"] + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==" }, "@rollup/rollup-linux-loongarch64-gnu@4.40.2": { - "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", - "os": ["linux"], - "cpu": ["loong64"] + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==" }, "@rollup/rollup-linux-powerpc64le-gnu@4.40.2": { - "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", - "os": ["linux"], - "cpu": ["ppc64"] + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==" }, "@rollup/rollup-linux-riscv64-gnu@4.40.2": { - "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", - "os": ["linux"], - "cpu": ["riscv64"] + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==" }, "@rollup/rollup-linux-riscv64-musl@4.40.2": { - "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", - "os": ["linux"], - "cpu": ["riscv64"] + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==" }, "@rollup/rollup-linux-s390x-gnu@4.40.2": { - "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", - "os": ["linux"], - "cpu": ["s390x"] + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==" }, "@rollup/rollup-linux-x64-gnu@4.40.2": { - "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", - "os": ["linux"], - "cpu": ["x64"] + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==" }, "@rollup/rollup-linux-x64-musl@4.40.2": { - "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", - "os": ["linux"], - "cpu": ["x64"] + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==" }, "@rollup/rollup-win32-arm64-msvc@4.40.2": { - "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", - "os": ["win32"], - "cpu": ["arm64"] + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==" }, "@rollup/rollup-win32-ia32-msvc@4.40.2": { - "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", - "os": ["win32"], - "cpu": ["ia32"] + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==" }, "@rollup/rollup-win32-x64-msvc@4.40.2": { - "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", - "os": ["win32"], - "cpu": ["x64"] + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==" }, "@tailwindcss/node@4.1.6": { "integrity": "sha512-ed6zQbgmKsjsVvodAS1q1Ld2BolEuxJOSyyNc+vhkjdmfNUDCmQnlXBfQkHrlzNmslxHsQU/bFmzcEbv4xXsLg==", @@ -1787,49 +1615,31 @@ ] }, "@tailwindcss/oxide-android-arm64@4.1.6": { - "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==", - "os": ["android"], - "cpu": ["arm64"] + "integrity": "sha512-VHwwPiwXtdIvOvqT/0/FLH/pizTVu78FOnI9jQo64kSAikFSZT7K4pjyzoDpSMaveJTGyAKvDjuhxJxKfmvjiQ==" }, "@tailwindcss/oxide-darwin-arm64@4.1.6": { - "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==", - "os": ["darwin"], - "cpu": ["arm64"] + "integrity": "sha512-weINOCcqv1HVBIGptNrk7c6lWgSFFiQMcCpKM4tnVi5x8OY2v1FrV76jwLukfT6pL1hyajc06tyVmZFYXoxvhQ==" }, "@tailwindcss/oxide-darwin-x64@4.1.6": { - "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==", - "os": ["darwin"], - "cpu": ["x64"] + "integrity": "sha512-3FzekhHG0ww1zQjQ1lPoq0wPrAIVXAbUkWdWM8u5BnYFZgb9ja5ejBqyTgjpo5mfy0hFOoMnMuVDI+7CXhXZaQ==" }, "@tailwindcss/oxide-freebsd-x64@4.1.6": { - "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==", - "os": ["freebsd"], - "cpu": ["x64"] + "integrity": "sha512-4m5F5lpkBZhVQJq53oe5XgJ+aFYWdrgkMwViHjRsES3KEu2m1udR21B1I77RUqie0ZYNscFzY1v9aDssMBZ/1w==" }, "@tailwindcss/oxide-linux-arm-gnueabihf@4.1.6": { - "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==", - "os": ["linux"], - "cpu": ["arm"] + "integrity": "sha512-qU0rHnA9P/ZoaDKouU1oGPxPWzDKtIfX7eOGi5jOWJKdxieUJdVV+CxWZOpDWlYTd4N3sFQvcnVLJWJ1cLP5TA==" }, "@tailwindcss/oxide-linux-arm64-gnu@4.1.6": { - "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==", - "os": ["linux"], - "cpu": ["arm64"] + "integrity": "sha512-jXy3TSTrbfgyd3UxPQeXC3wm8DAgmigzar99Km9Sf6L2OFfn/k+u3VqmpgHQw5QNfCpPe43em6Q7V76Wx7ogIQ==" }, "@tailwindcss/oxide-linux-arm64-musl@4.1.6": { - "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==", - "os": ["linux"], - "cpu": ["arm64"] + "integrity": "sha512-8kjivE5xW0qAQ9HX9reVFmZj3t+VmljDLVRJpVBEoTR+3bKMnvC7iLcoSGNIUJGOZy1mLVq7x/gerVg0T+IsYw==" }, "@tailwindcss/oxide-linux-x64-gnu@4.1.6": { - "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==", - "os": ["linux"], - "cpu": ["x64"] + "integrity": "sha512-A4spQhwnWVpjWDLXnOW9PSinO2PTKJQNRmL/aIl2U/O+RARls8doDfs6R41+DAXK0ccacvRyDpR46aVQJJCoCg==" }, "@tailwindcss/oxide-linux-x64-musl@4.1.6": { - "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==", - "os": ["linux"], - "cpu": ["x64"] + "integrity": "sha512-YRee+6ZqdzgiQAHVSLfl3RYmqeeaWVCk796MhXhLQu2kJu2COHBkqlqsqKYx3p8Hmk5pGCQd2jTAoMWWFeyG2A==" }, "@tailwindcss/oxide-wasm32-wasi@4.1.6": { "integrity": "sha512-qAp4ooTYrBQ5pk5jgg54/U1rCJ/9FLYOkkQ/nTE+bVMseMfB6O7J8zb19YTpWuu4UdfRf5zzOrNKfl6T64MNrQ==", @@ -1840,26 +1650,17 @@ "@napi-rs/wasm-runtime", "@tybys/wasm-util", "tslib" - ], - "cpu": ["wasm32"] + ] }, "@tailwindcss/oxide-win32-arm64-msvc@4.1.6": { - "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==", - "os": ["win32"], - "cpu": ["arm64"] + "integrity": "sha512-nqpDWk0Xr8ELO/nfRUDjk1pc9wDJ3ObeDdNMHLaymc4PJBWj11gdPCWZFKSK2AVKjJQC7J2EfmSmf47GN7OuLg==" }, "@tailwindcss/oxide-win32-x64-msvc@4.1.6": { - "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==", - "os": ["win32"], - "cpu": ["x64"] + "integrity": "sha512-5k9xF33xkfKpo9wCvYcegQ21VwIBU1/qEbYlVukfEIyQbEA47uK8AAwS7NVjNE3vHzcmxMYwd0l6L4pPjjm1rQ==" }, "@tailwindcss/oxide@4.1.6": { "integrity": "sha512-0bpEBQiGx+227fW4G0fLQ8vuvyy5rsB1YIYNapTq3aRsJ9taF3f5cCaovDjN5pUGKKzcpMrZst/mhNaKAPOHOA==", "dependencies": [ - "detect-libc", - "tar" - ], - "optionalDependencies": [ "@tailwindcss/oxide-android-arm64", "@tailwindcss/oxide-darwin-arm64", "@tailwindcss/oxide-darwin-x64", @@ -1871,9 +1672,10 @@ "@tailwindcss/oxide-linux-x64-musl", "@tailwindcss/oxide-wasm32-wasi", "@tailwindcss/oxide-win32-arm64-msvc", - "@tailwindcss/oxide-win32-x64-msvc" - ], - "scripts": true + "@tailwindcss/oxide-win32-x64-msvc", + "detect-libc", + "tar" + ] }, "@tailwindcss/vite@4.1.6_vite@6.3.5__picomatch@4.0.2": { "integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==", @@ -1881,7 +1683,16 @@ "@tailwindcss/node", "@tailwindcss/oxide", "tailwindcss", - "vite" + "vite@6.3.5_picomatch@4.0.2" + ] + }, + "@tailwindcss/vite@4.1.6_vite@6.3.5__picomatch@4.0.2_@types+node@22.12.0": { + "integrity": "sha512-zjtqjDeY1w3g2beYQtrMAf51n5G7o+UwmyOjtsDMP7t6XyoRMOidcoKP32ps7AkNOHIXEOK0bhIC05dj8oJp4w==", + "dependencies": [ + "@tailwindcss/node", + "@tailwindcss/oxide", + "tailwindcss", + "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0" ] }, "@tybys/wasm-util@0.9.0": { @@ -1934,6 +1745,12 @@ "@types/history@4.7.11": { "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==" }, + "@types/node@22.12.0": { + "integrity": "sha512-Fll2FZ1riMjNmlmJOdAyY5pUbkftXslB5DgEzlIuNaiWhXd00FhWxVC/r4yV/4wBb9JfImTu+jiSvXTkJ7F/gA==", + "dependencies": [ + "undici-types" + ] + }, "@types/react-dom@19.1.5_@types+react@19.1.4": { "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", "dependencies": [ @@ -1970,7 +1787,18 @@ "@babel/plugin-transform-react-jsx-source", "@types/babel__core", "react-refresh", - "vite" + "vite@6.3.5_picomatch@4.0.2" + ] + }, + "@vitejs/plugin-react@4.4.1_vite@6.3.5__picomatch@4.0.2_@babel+core@7.27.1_@types+node@22.12.0": { + "integrity": "sha512-IpEm5ZmeXAP/osiBXVVP5KjFMzbWOonMs0NaQQl+xYnUAcq4oHUBsF2+p4MgKWG4YMmFYJU8A6sxRPuowllm6w==", + "dependencies": [ + "@babel/core", + "@babel/plugin-transform-react-jsx-self", + "@babel/plugin-transform-react-jsx-source", + "@types/babel__core", + "react-refresh", + "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0" ] }, "ansi-escapes@4.3.2": { @@ -2016,8 +1844,7 @@ "electron-to-chromium", "node-releases", "update-browserslist-db" - ], - "bin": true + ] }, "caniuse-lite@1.0.30001718": { "integrity": "sha512-AflseV1ahcSunK53NfEs9gFWgOEmzr0f+kaMFA4xiLZlr9Hzt7HxcSpIFcnNCUkz6R6dWKa54rUz3HUmI3nVcw==" @@ -2126,7 +1953,7 @@ }, "esbuild@0.25.4": { "integrity": "sha512-8pgjLUcUjcgDg+2Q4NYXnPbo/vncAY4UmyaCm0jZevERqCHZIaWwdJHkf8XQtu4AxSKCdvrUbT0XUr1IdZzI8Q==", - "optionalDependencies": [ + "dependencies": [ "@esbuild/aix-ppc64", "@esbuild/android-arm", "@esbuild/android-arm64", @@ -2152,9 +1979,7 @@ "@esbuild/win32-arm64", "@esbuild/win32-ia32", "@esbuild/win32-x64" - ], - "scripts": true, - "bin": true + ] }, "escalade@3.2.0": { "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" @@ -2171,9 +1996,6 @@ "integrity": "sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg==", "dependencies": [ "picomatch" - ], - "optionalPeers": [ - "picomatch" ] }, "foreground-child@3.3.1": { @@ -2184,9 +2006,7 @@ ] }, "fsevents@2.3.3": { - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "os": ["darwin"], - "scripts": true + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==" }, "gensync@1.0.0-beta.2": { "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" @@ -2206,8 +2026,7 @@ "minipass", "package-json-from-dist", "path-scurry" - ], - "bin": true + ] }, "globals@11.12.0": { "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" @@ -2234,79 +2053,54 @@ ] }, "jiti@2.4.2": { - "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==", - "bin": true + "integrity": "sha512-rg9zJN+G4n2nfJl5MW3BMygZX56zKPNVEYYqq7adpmMh4Jn2QNEwhvQlFy6jPVdcod7txZtKHWnyZiA3a0zP7A==" }, "js-tokens@4.0.0": { "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" }, "jsesc@3.1.0": { - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "bin": true + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" }, "json5@2.2.3": { - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "bin": true + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, "layout-base@2.0.1": { "integrity": "sha512-dp3s92+uNI1hWIpPGH3jK2kxE2lMjdXdr+DH8ynZHpd6PUlH6x6cbuXnoMmiNumznqaNO31xu9e79F0uuZ0JFg==" }, "lightningcss-darwin-arm64@1.29.2": { - "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==", - "os": ["darwin"], - "cpu": ["arm64"] + "integrity": "sha512-cK/eMabSViKn/PG8U/a7aCorpeKLMlK0bQeNHmdb7qUnBkNPnL+oV5DjJUo0kqWsJUapZsM4jCfYItbqBDvlcA==" }, "lightningcss-darwin-x64@1.29.2": { - "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==", - "os": ["darwin"], - "cpu": ["x64"] + "integrity": "sha512-j5qYxamyQw4kDXX5hnnCKMf3mLlHvG44f24Qyi2965/Ycz829MYqjrVg2H8BidybHBp9kom4D7DR5VqCKDXS0w==" }, "lightningcss-freebsd-x64@1.29.2": { - "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==", - "os": ["freebsd"], - "cpu": ["x64"] + "integrity": "sha512-wDk7M2tM78Ii8ek9YjnY8MjV5f5JN2qNVO+/0BAGZRvXKtQrBC4/cn4ssQIpKIPP44YXw6gFdpUF+Ps+RGsCwg==" }, "lightningcss-linux-arm-gnueabihf@1.29.2": { - "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==", - "os": ["linux"], - "cpu": ["arm"] + "integrity": "sha512-IRUrOrAF2Z+KExdExe3Rz7NSTuuJ2HvCGlMKoquK5pjvo2JY4Rybr+NrKnq0U0hZnx5AnGsuFHjGnNT14w26sg==" }, "lightningcss-linux-arm64-gnu@1.29.2": { - "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==", - "os": ["linux"], - "cpu": ["arm64"] + "integrity": "sha512-KKCpOlmhdjvUTX/mBuaKemp0oeDIBBLFiU5Fnqxh1/DZ4JPZi4evEH7TKoSBFOSOV3J7iEmmBaw/8dpiUvRKlQ==" }, "lightningcss-linux-arm64-musl@1.29.2": { - "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==", - "os": ["linux"], - "cpu": ["arm64"] + "integrity": "sha512-Q64eM1bPlOOUgxFmoPUefqzY1yV3ctFPE6d/Vt7WzLW4rKTv7MyYNky+FWxRpLkNASTnKQUaiMJ87zNODIrrKQ==" }, "lightningcss-linux-x64-gnu@1.29.2": { - "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==", - "os": ["linux"], - "cpu": ["x64"] + "integrity": "sha512-0v6idDCPG6epLXtBH/RPkHvYx74CVziHo6TMYga8O2EiQApnUPZsbR9nFNrg2cgBzk1AYqEd95TlrsL7nYABQg==" }, "lightningcss-linux-x64-musl@1.29.2": { - "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==", - "os": ["linux"], - "cpu": ["x64"] + "integrity": "sha512-rMpz2yawkgGT8RULc5S4WiZopVMOFWjiItBT7aSfDX4NQav6M44rhn5hjtkKzB+wMTRlLLqxkeYEtQ3dd9696w==" }, "lightningcss-win32-arm64-msvc@1.29.2": { - "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==", - "os": ["win32"], - "cpu": ["arm64"] + "integrity": "sha512-nL7zRW6evGQqYVu/bKGK+zShyz8OVzsCotFgc7judbt6wnB2KbiKKJwBE4SGoDBQ1O94RjW4asrCjQL4i8Fhbw==" }, "lightningcss-win32-x64-msvc@1.29.2": { - "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==", - "os": ["win32"], - "cpu": ["x64"] + "integrity": "sha512-EdIUW3B2vLuHmv7urfzMI/h2fmlnOQBk1xlsDxkN1tCWKjNFjfLhGxYk8C8mzpSfr+A6jFFIi8fU6LbQGsRWjA==" }, "lightningcss@1.29.2": { "integrity": "sha512-6b6gd/RUXKaw5keVdSEtqFVdzWnU5jMxTUjA2bVcMNPLwSQ08Sv/UodBVtETLCn7k4S1Ibxwh7k68IwLZPgKaA==", "dependencies": [ - "detect-libc" - ], - "optionalDependencies": [ + "detect-libc", "lightningcss-darwin-arm64", "lightningcss-darwin-x64", "lightningcss-freebsd-x64", @@ -2356,8 +2150,7 @@ ] }, "mkdirp@3.0.1": { - "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", - "bin": true + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==" }, "ms@2.1.3": { "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" @@ -2366,15 +2159,13 @@ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==" }, "nanoid@3.3.11": { - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "bin": true + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==" }, "node-addon-api@8.3.1": { "integrity": "sha512-lytcDEdxKjGJPTLEfW4mYMigRezMlyJY8W4wxJK8zE533Jlb8L8dRuObJFWg2P+AuOIxoCgKF+2Oq4d4Zd0OUA==" }, "node-gyp-build@4.8.4": { - "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==", - "bin": true + "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==" }, "node-releases@2.0.19": { "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" @@ -2474,10 +2265,6 @@ "@types/react-dom", "react", "react-dom" - ], - "optionalPeers": [ - "@types/react", - "@types/react-dom" ] }, "react-dom@19.1.0_react@19.1.0": { @@ -2497,9 +2284,6 @@ "react", "react-style-singleton", "tslib" - ], - "optionalPeers": [ - "@types/react" ] }, "react-remove-scroll@2.6.3_@types+react@19.1.4_react@19.1.0": { @@ -2512,9 +2296,6 @@ "tslib", "use-callback-ref", "use-sidecar" - ], - "optionalPeers": [ - "@types/react" ] }, "react-resizable-panels@3.0.2_react@19.1.0_react-dom@19.1.0__react@19.1.0": { @@ -2531,9 +2312,6 @@ "react", "react-dom", "set-cookie-parser" - ], - "optionalPeers": [ - "react-dom" ] }, "react-style-singleton@2.2.3_@types+react@19.1.4_react@19.1.0": { @@ -2543,9 +2321,6 @@ "get-nonce", "react", "tslib" - ], - "optionalPeers": [ - "@types/react" ] }, "react@19.1.0": { @@ -2557,9 +2332,6 @@ "rollup@4.40.2": { "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", "dependencies": [ - "@types/estree" - ], - "optionalDependencies": [ "@rollup/rollup-android-arm-eabi", "@rollup/rollup-android-arm64", "@rollup/rollup-darwin-arm64", @@ -2580,9 +2352,9 @@ "@rollup/rollup-win32-arm64-msvc", "@rollup/rollup-win32-ia32-msvc", "@rollup/rollup-win32-x64-msvc", + "@types/estree", "fsevents" - ], - "bin": true + ] }, "safer-buffer@2.1.2": { "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" @@ -2591,8 +2363,7 @@ "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==" }, "semver@6.3.1": { - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "bin": true + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" }, "set-cookie-parser@2.7.1": { "integrity": "sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==" @@ -2679,11 +2450,15 @@ "node-addon-api", "node-gyp-build", "tree-sitter" - ], - "optionalPeers": [ + ] + }, + "tree-sitter-c@0.23.5_tree-sitter@0.22.4": { + "integrity": "sha512-riDWhqVIt8J14R7G0YMKlUy8E7eYR0Vp6DSGL90nX5CTAXkORCyp4WaOgNtfo8dEsHyZF5e/4E9Z9kWj+qLnTQ==", + "dependencies": [ + "node-addon-api", + "node-gyp-build", "tree-sitter" - ], - "scripts": true + ] }, "tree-sitter-python@0.23.6_tree-sitter@0.22.4": { "integrity": "sha512-yIM9z0oxKIxT7bAtPOhgoVl6gTXlmlIhue7liFT4oBPF/lha7Ha4dQBS82Av6hMMRZoVnFJI8M6mL+SwWoLD3A==", @@ -2691,19 +2466,14 @@ "node-addon-api", "node-gyp-build", "tree-sitter" - ], - "optionalPeers": [ - "tree-sitter" - ], - "scripts": true + ] }, "tree-sitter@0.22.4": { "integrity": "sha512-usbHZP9/oxNsUY65MQUsduGRqDHQOou1cagUSwjhoSYAmSahjQDAVsh9s+SlZkn8X8+O1FULRGwHu7AFP3kjzg==", "dependencies": [ "node-addon-api", "node-gyp-build" - ], - "scripts": true + ] }, "tslib@2.8.1": { "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" @@ -2714,14 +2484,16 @@ "type-fest@0.21.3": { "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==" }, + "undici-types@6.20.0": { + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, "update-browserslist-db@1.1.3_browserslist@4.24.5": { "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "dependencies": [ "browserslist", "escalade", "picocolors" - ], - "bin": true + ] }, "use-callback-ref@1.3.3_@types+react@19.1.4_react@19.1.0": { "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", @@ -2729,9 +2501,6 @@ "@types/react", "react", "tslib" - ], - "optionalPeers": [ - "@types/react" ] }, "use-sidecar@1.1.3_@types+react@19.1.4_react@19.1.0": { @@ -2741,9 +2510,6 @@ "detect-node-es", "react", "tslib" - ], - "optionalPeers": [ - "@types/react" ] }, "use-sync-external-store@1.5.0_react@19.1.0": { @@ -2757,22 +2523,31 @@ "dependencies": [ "esbuild", "fdir", + "fsevents", "picomatch", "postcss", "rollup", "tinyglobby" - ], - "optionalDependencies": [ - "fsevents" - ], - "bin": true + ] + }, + "vite@6.3.5_picomatch@4.0.2_@types+node@22.12.0": { + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", + "dependencies": [ + "@types/node", + "esbuild", + "fdir", + "fsevents", + "picomatch", + "postcss", + "rollup", + "tinyglobby" + ] }, "which@2.0.2": { "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dependencies": [ "isexe" - ], - "bin": true + ] }, "wrap-ansi@6.2.0": { "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", diff --git a/packages/cli/src/cli/handlers/init/prompts.ts b/packages/cli/src/cli/handlers/init/prompts.ts index 9b6c73ae..f1600e75 100644 --- a/packages/cli/src/cli/handlers/init/prompts.ts +++ b/packages/cli/src/cli/handlers/init/prompts.ts @@ -7,6 +7,7 @@ import pythonStdlibList from "../../../scripts/generate_python_stdlib_list/outpu import { confirm, input, number, search, select } from "@inquirer/prompts"; import { globSync } from "npm:glob"; import { + cLanguage, csharpLanguage, pythonLanguage, } from "../../../helpers/treeSitter/parsers.ts"; @@ -146,7 +147,7 @@ async function collectIncludePatterns( Include patterns define which files NanoAPI will process and analyze. Examples: -- '**/*.py' for all Python files +- '**/*.py' for all Python files - 'src/**' for all files in src directory - '*.py' for all Python files in the root directory `, @@ -500,6 +501,7 @@ export async function generateConfig( choices: [ { name: "Python", value: pythonLanguage }, { name: "C#", value: csharpLanguage }, + { name: "C", value: cLanguage }, ], }); diff --git a/packages/cli/src/cli/helpers/checkVersion.ts b/packages/cli/src/cli/helpers/checkVersion.ts index f686dd51..29e14fa8 100644 --- a/packages/cli/src/cli/helpers/checkVersion.ts +++ b/packages/cli/src/cli/helpers/checkVersion.ts @@ -33,8 +33,8 @@ export async function checkVersionMiddleware() { if (currentVersion !== latestVersion) { console.warn( ` -You are using version ${currentVersion}. -The latest version is ${latestVersion}. +You are using version ${currentVersion}. +The latest version is ${latestVersion}. Please update to the latest version to continue using napi. You can update the version by running the following command: diff --git a/packages/cli/src/config/localConfig.ts b/packages/cli/src/config/localConfig.ts index e9ed2d69..3370e246 100644 --- a/packages/cli/src/config/localConfig.ts +++ b/packages/cli/src/config/localConfig.ts @@ -4,6 +4,7 @@ import pythonStdlibList from "../scripts/generate_python_stdlib_list/output.json type: "json", }; import { + cLanguage, csharpLanguage, pythonLanguage, } from "../helpers/treeSitter/parsers.ts"; @@ -11,7 +12,7 @@ import { const pythonVersions = Object.keys(pythonStdlibList); export const localConfigSchema = z.object({ - language: z.enum([pythonLanguage, csharpLanguage]), + language: z.enum([pythonLanguage, csharpLanguage, cLanguage]), [pythonLanguage]: z .object({ version: z @@ -24,6 +25,11 @@ export const localConfigSchema = z.object({ .optional(), }) .optional(), // python specific config + [cLanguage]: z + .object({ + includedirs: z.array(z.string()).optional(), + }) + .optional(), // c specific config project: z.object({ include: z.array(z.string()), exclude: z.array(z.string()).optional(), diff --git a/packages/cli/src/helpers/fileSystem/index.ts b/packages/cli/src/helpers/fileSystem/index.ts index 35385461..e385fb58 100644 --- a/packages/cli/src/helpers/fileSystem/index.ts +++ b/packages/cli/src/helpers/fileSystem/index.ts @@ -1,11 +1,16 @@ import { globSync } from "npm:glob"; import { dirname, join } from "@std/path"; -import { csharpLanguage, pythonLanguage } from "../treeSitter/parsers.ts"; +import { + cLanguage, + csharpLanguage, + pythonLanguage, +} from "../treeSitter/parsers.ts"; export function getExtensionsForLanguage(language: string) { const supportedLanguages: Record = { [pythonLanguage]: ["py"], [csharpLanguage]: ["cs", "csproj"], + [cLanguage]: ["c", "h"], }; const supportedLanguage = supportedLanguages[language]; diff --git a/packages/cli/src/helpers/treeSitter/parsers.ts b/packages/cli/src/helpers/treeSitter/parsers.ts index 2422559f..b3cc64e8 100644 --- a/packages/cli/src/helpers/treeSitter/parsers.ts +++ b/packages/cli/src/helpers/treeSitter/parsers.ts @@ -1,6 +1,7 @@ import Parser, { type Language } from "npm:tree-sitter"; import Python from "npm:tree-sitter-python"; import CSharp from "npm:tree-sitter-c-sharp"; +import C from "npm:tree-sitter-c"; const pythonParser = new Parser(); pythonParser.setLanguage(Python as Language); @@ -10,4 +11,15 @@ const csharpParser = new Parser(); csharpParser.setLanguage(CSharp as Language); const csharpLanguage = CSharp.name as "c-sharp"; -export { csharpLanguage, csharpParser, pythonLanguage, pythonParser }; +const cParser = new Parser(); +cParser.setLanguage(C as Language); +const cLanguage = C.name as "c"; + +export { + cLanguage, + cParser, + csharpLanguage, + csharpParser, + pythonLanguage, + pythonParser, +}; diff --git a/packages/cli/src/languagePlugins/c/README.md b/packages/cli/src/languagePlugins/c/README.md new file mode 100644 index 00000000..6ce7f866 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/README.md @@ -0,0 +1,181 @@ +# NanoAPI C Plugin + +This plugin manages parsing and mapping of dependencies in C projects. + +**Warning :** This plugin relies on tree-sitter, which has an unreliable parser +for C. Not every C project is entirely compatible. Warnings may be issued where +tree-sitter finds errors. + +## Class diagram + +```mermaid +classDiagram + class CMetricsAnalyzer { + +analyzeNode(node: Parser.SyntaxNode): CComplexityMetrics + } + + class CExtractor { + -manifest: DependencyManifest + -registry: Map + -includeResolver: CIncludeResolver + +extractSymbols(symbolsMap: Map): Map + } + + class CSymbolRegistry { + -headerResolver: CHeaderResolver + -files: Map + +getRegistry(): Map + } + + class CHeaderResolver { + +resolveSymbols(file: File): ExportedSymbol[] + } + + class CIncludeResolver { + -symbolRegistry: Map + -files: Map + +getInclusions(): Map + } + + class CInvocationResolver { + -includeResolver: CIncludeResolver + +getInvocationsForSymbol(symbol: Symbol): Invocations + +getInvocationsForFile(filepath: string): Invocations + } + + class CDependencyFormatter { + -symbolRegistry: CSymbolRegistry + -includeResolver: CIncludeResolver + -invocationResolver: CInvocationResolver + +formatFile(filepath: string): CDepFile + } + + class Symbol { + <> + -name: string + -declaration: ExportedSymbol + } + + class FunctionSignature { + -definition: FunctionDefinition + -isMacro: boolean + } + + class FunctionDefinition { + -signature: FunctionSignature + -isMacro: boolean + } + + class DataType { + -typedefs: Map + } + + class Typedef { + -datatype: DataType + } + + class Variable { + -isMacro: boolean + } + + class CFile { + -file: File + -symbols: Map + -type: CFileType + } + + class ExportedSymbol { + -name: string + -type: SymbolType + -specifiers: StorageClassSpecifier[] + -qualifiers: TypeQualifier[] + -node: Parser.SyntaxNode + -identifierNode: Parser.SyntaxNode + -filepath: string + } + + class Inclusions { + -filepath: string + -symbols: Map + -internal: string[] + -standard: Map + } + + class Invocations { + -resolved: Map + -unresolved: Set + } + + class CDependency { + -id: string + -isExternal: boolean + -symbols: Record + } + + class CDepFile { + -id: string + -filePath: string + -rootNode: Parser.SyntaxNode + -lineCount: number + -characterCount: number + -dependencies: Record + -symbols: Record + } + + class CDepSymbol { + -id: string + -type: CDepSymbolType + -lineCount: number + -characterCount: number + -node: Parser.SyntaxNode + -dependents: Record + -dependencies: Record + } + + class CComplexityMetrics { + -cyclomaticComplexity: number + -codeLinesCount: number + -linesCount: number + -codeCharacterCount: number + -characterCount: number + } + + class CodeCounts { + -lines: number + -characters: number + } + + class CommentSpan { + -start: Point + -end: Point + } + + %% Relationships + Symbol <|-- FunctionSignature + Symbol <|-- FunctionDefinition + Symbol <|-- DataType + Symbol <|-- Typedef + Symbol <|-- Variable + CSymbolRegistry --> CFile + CSymbolRegistry --> CHeaderResolver + CIncludeResolver --> CSymbolRegistry + CInvocationResolver --> CIncludeResolver + CDependencyFormatter --> CSymbolRegistry + CDependencyFormatter --> CIncludeResolver + CDependencyFormatter --> CInvocationResolver + CExtractor --> CSymbolRegistry + CExtractor --> CIncludeResolver + CExtractor --> CFile + CFile --> Symbol + Typedef --> DataType + DataType --> Typedef + Invocations --> Symbol + Inclusions --> Symbol + CDepFile --> CDependency + CDepFile --> CDepSymbol + CDepSymbol --> CDependent + CDepSymbol --> CDependency + CMetricsAnalyzer --> CComplexityMetrics + CMetricsAnalyzer --> CodeCounts + CMetricsAnalyzer --> CommentSpan +``` diff --git a/packages/cli/src/languagePlugins/c/dependencyFormatting/index.test.ts b/packages/cli/src/languagePlugins/c/dependencyFormatting/index.test.ts new file mode 100644 index 00000000..1e489d6c --- /dev/null +++ b/packages/cli/src/languagePlugins/c/dependencyFormatting/index.test.ts @@ -0,0 +1,66 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { cFilesFolder, getCFilesMap } from "../testFiles/index.ts"; +import { CDependencyFormatter } from "./index.ts"; +import { join } from "@std/path"; + +describe("CDependencyFormatter", () => { + const cFilesMap = getCFilesMap(); + const depFormatter = new CDependencyFormatter(cFilesMap); + const burgersh = join(cFilesFolder, "burgers.h"); + const burgersc = join(cFilesFolder, "burgers.c"); + const personnelh = join(cFilesFolder, "personnel.h"); + const main = join(cFilesFolder, "main.c"); + + test("main.c", () => { + const fmain = depFormatter.formatFile(main); + expect(fmain).toBeDefined(); + expect(fmain.id).toBe(main); + expect(fmain.dependencies[burgersh]).toBeDefined(); + expect(fmain.dependencies[burgersc]).not.toBeDefined(); + expect(fmain.dependencies[personnelh]).toBeDefined(); + expect(fmain.dependencies[""]).toBeDefined(); + expect(fmain.dependencies[personnelh].isExternal).toBe(false); + expect(fmain.dependencies[burgersh].isExternal).toBe(false); + expect(fmain.dependencies[""].isExternal).toBe(true); + expect(fmain.dependencies[burgersh].symbols["Burger"]).toBeDefined(); + expect(fmain.dependencies[burgersh].symbols["create_burger"]).toBeDefined(); + expect(fmain.dependencies[personnelh].symbols["Employee"]).toBeDefined(); + expect( + fmain.dependencies[personnelh].symbols["create_employee"], + ).toBeDefined(); + expect( + fmain.dependencies[personnelh].symbols["print_employee_details"], + ).toBeDefined(); + expect(fmain.symbols["main"]).toBeDefined(); + expect(fmain.symbols["main"].type).toBe("function"); + expect(fmain.symbols["main"].lineCount > 1).toBe(true); + expect(fmain.symbols["main"].characterCount > 1).toBe(true); + expect(fmain.symbols["main"].dependents).toBeDefined(); + expect(fmain.symbols["main"].dependencies).toBeDefined(); + expect(fmain.symbols["main"].dependencies[burgersh]).toBeDefined(); + expect(fmain.symbols["main"].dependencies[burgersh].isExternal).toBe(false); + expect(fmain.symbols["main"].dependencies[burgersh].symbols["Burger"]).toBe( + "Burger", + ); + expect( + fmain.symbols["main"].dependencies[burgersh].symbols["create_burger"], + ).toBe("create_burger"); + expect(fmain.symbols["main"].dependencies[personnelh]).toBeDefined(); + expect(fmain.symbols["main"].dependencies[personnelh].isExternal).toBe( + false, + ); + expect( + fmain.symbols["main"].dependencies[personnelh].symbols["Employee"], + ).toBe("Employee"); + expect( + fmain.symbols["main"].dependencies[personnelh].symbols["create_employee"], + ).toBe("create_employee"); + expect( + fmain.symbols["main"].dependencies[personnelh].symbols[ + "print_employee_details" + ], + ).toBe("print_employee_details"); + expect(fmain.symbols["main"].dependencies[""]).not.toBeDefined(); + }); +}); diff --git a/packages/cli/src/languagePlugins/c/dependencyFormatting/index.ts b/packages/cli/src/languagePlugins/c/dependencyFormatting/index.ts new file mode 100644 index 00000000..8a0f2a31 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/dependencyFormatting/index.ts @@ -0,0 +1,155 @@ +import { + C_DEP_FUNCTION_TYPE, + type CDependency, + type CDepFile, + type CDepSymbol, + type CDepSymbolType, +} from "./types.ts"; +import { CSymbolRegistry } from "../symbolRegistry/index.ts"; +import { CIncludeResolver } from "../includeResolver/index.ts"; +import { CInvocationResolver } from "../invocationResolver/index.ts"; +import { + type CFile, + EnumMember, + type Symbol, +} from "../symbolRegistry/types.ts"; +import type { Invocations } from "../invocationResolver/types.ts"; +import type Parser from "npm:tree-sitter"; +import { C_VARIABLE_TYPE, type SymbolType } from "../headerResolver/types.ts"; + +export class CDependencyFormatter { + symbolRegistry: CSymbolRegistry; + includeResolver: CIncludeResolver; + invocationResolver: CInvocationResolver; + #registry: Map; + + constructor( + files: Map, + includeDirs: string[] = [], + ) { + this.symbolRegistry = new CSymbolRegistry(files); + this.#registry = this.symbolRegistry.getRegistry(); + this.includeResolver = new CIncludeResolver( + this.symbolRegistry, + includeDirs, + ); + this.invocationResolver = new CInvocationResolver(this.includeResolver); + } + + #formatSymbolType(st: SymbolType): CDepSymbolType { + if (["struct", "enum", "union", "typedef", "variable"].includes(st)) { + return st as CDepSymbolType; + } + if ( + ["function_signature", "function_definition", "macro_function"].includes( + st, + ) + ) { + return C_DEP_FUNCTION_TYPE; + } + if (st === "macro_constant") { + return C_VARIABLE_TYPE as CDepSymbolType; + } + throw new Error(`Unknown symbol type: ${st}`); + } + + /** + * Formats the dependencies of a file. + * @param fileDependencies - The dependencies of the file. + * @returns A formatted record of dependencies. + */ + #formatDependencies( + fileDependencies: Invocations, + ): Record { + const dependencies: Record = {}; + const resolved = fileDependencies.resolved; + for (const [symName, symbol] of resolved) { + const filepath = symbol.symbol.declaration.filepath; + const id = symName; + if (!dependencies[filepath]) { + dependencies[filepath] = { + id: filepath, + isExternal: false, + symbols: {}, + }; + } + dependencies[filepath].symbols[id] = id; + } + return dependencies; + } + + #formatStandardIncludes(stdincludes: string[]): Record { + const dependencies: Record = {}; + for (const id of stdincludes) { + if (!dependencies[id]) { + dependencies[id] = { + id: id, + isExternal: true, + symbols: {}, + }; + } + } + return dependencies; + } + + /** + * Formats the symbols of a file. + * @param fileSymbols - The symbols of the file. + * @returns A formatted record of symbols. + */ + #formatSymbols(fileSymbols: Map): Record { + const symbols: Record = {}; + for (const [symName, symbol] of fileSymbols) { + const id = symName; + const dependencies = this.invocationResolver.getInvocationsForSymbol( + symbol, + ); + if (!symbols[id] && !(symbol instanceof EnumMember)) { + symbols[id] = { + id: id, + type: this.#formatSymbolType(symbol.declaration.type), + lineCount: symbol.declaration.node.endPosition.row - + symbol.declaration.node.startPosition.row, + characterCount: symbol.declaration.node.endIndex - + symbol.declaration.node.startIndex, + node: symbol.declaration.node, + dependents: {}, + dependencies: this.#formatDependencies(dependencies), + }; + } + } + return symbols; + } + + formatFile(filepath: string): CDepFile { + const file = this.#registry.get(filepath); + if (!file) { + throw new Error(`File not found: ${filepath}`); + } + const fileSymbols = file.symbols; + const fileDependencies = this.invocationResolver.getInvocationsForFile( + filepath, + ); + const includes = this.includeResolver.getInclusions().get(filepath); + if (!includes) { + throw new Error(`File not found: ${filepath}`); + } + const stdincludes = Array.from(includes.standard.keys()); + const invokedDependencies = this.#formatDependencies(fileDependencies); + const stdDependencies = this.#formatStandardIncludes(stdincludes); + const allDependencies = { + ...invokedDependencies, + ...stdDependencies, + }; + const formattedFile: CDepFile = { + id: filepath, + filePath: file.file.path, + rootNode: file.file.rootNode, + lineCount: file.file.rootNode.endPosition.row, + characterCount: file.file.rootNode.endIndex, + dependencies: allDependencies, + symbols: this.#formatSymbols(fileSymbols), + }; + return formattedFile; + } +} diff --git a/packages/cli/src/languagePlugins/c/dependencyFormatting/types.ts b/packages/cli/src/languagePlugins/c/dependencyFormatting/types.ts new file mode 100644 index 00000000..39ee8524 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/dependencyFormatting/types.ts @@ -0,0 +1,60 @@ +import type { + C_ENUM_TYPE, + C_STRUCT_TYPE, + C_TYPEDEF_TYPE, + C_UNION_TYPE, + C_VARIABLE_TYPE, +} from "../headerResolver/types.ts"; +import type Parser from "npm:tree-sitter"; + +/** + * Represents a dependency in a C file + */ +export interface CDependency { + id: string; + isExternal: boolean; + symbols: Record; +} + +/** + * Represents a dependent in a C file + */ +export interface CDependent { + id: string; + symbols: Record; +} + +export const C_DEP_FUNCTION_TYPE = "function"; +export type CDepSymbolType = + | typeof C_ENUM_TYPE + | typeof C_UNION_TYPE + | typeof C_STRUCT_TYPE + | typeof C_TYPEDEF_TYPE + | typeof C_VARIABLE_TYPE + | typeof C_DEP_FUNCTION_TYPE; + +/** + * Represents a symbol in a C file + */ +export interface CDepSymbol { + id: string; + type: CDepSymbolType; + lineCount: number; + characterCount: number; + node: Parser.SyntaxNode; + dependents: Record; + dependencies: Record; +} + +/** + * Represents a C file with its dependencies and symbols. + */ +export interface CDepFile { + id: string; + filePath: string; + rootNode: Parser.SyntaxNode; + lineCount: number; + characterCount: number; + dependencies: Record; + symbols: Record; +} diff --git a/packages/cli/src/languagePlugins/c/extractor/index.test.ts b/packages/cli/src/languagePlugins/c/extractor/index.test.ts new file mode 100644 index 00000000..2e0928af --- /dev/null +++ b/packages/cli/src/languagePlugins/c/extractor/index.test.ts @@ -0,0 +1,120 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { + cFilesFolder, + dummyLocalConfig, + getCFilesContentMap, +} from "../testFiles/index.ts"; +import { CExtractor } from "./index.ts"; +import { join } from "@std/path"; +import { generateCDependencyManifest } from "../../../manifest/dependencyManifest/c/index.ts"; + +describe("CExtractor", () => { + const cContentMap = getCFilesContentMap(); + const manifest = generateCDependencyManifest(cContentMap, dummyLocalConfig); + const extractor = new CExtractor(cContentMap, manifest, dummyLocalConfig); + const burgers = join(cFilesFolder, "burgers.h"); + const burgersc = join(cFilesFolder, "burgers.c"); + const main = join(cFilesFolder, "main.c"); + const all = join(cFilesFolder, "all.h"); + const errorsh = join(cFilesFolder, "errors.h"); + test("extracts create_burger", () => { + const symbolsToExtract = new Map< + string, + { filePath: string; symbols: Set } + >(); + symbolsToExtract.set(burgers, { + filePath: burgers, + symbols: new Set(["create_burger"]), + }); + const extractedFiles = extractor.extractSymbols(symbolsToExtract); + expect(extractedFiles.size).toBe(2); + const newManifest = generateCDependencyManifest( + extractedFiles, + dummyLocalConfig, + ); + expect(newManifest[burgers]).toBeDefined(); + expect(newManifest[burgersc]).toBeDefined(); + // Expected symbols to be kept + expect(newManifest[burgers].symbols["create_burger"]).toBeDefined(); + expect(newManifest[burgersc].symbols["create_burger"]).toBeDefined(); + expect(newManifest[burgers].symbols["Condiment"]).toBeDefined(); + expect(newManifest[burgers].symbols["Burger"]).toBeDefined(); + expect(newManifest[burgers].symbols["Sauce"]).toBeDefined(); + expect(newManifest[burgers].symbols["burger_count"]).toBeDefined(); + expect(newManifest[burgers].symbols["BURGERS_H"]).toBeDefined(); + expect(newManifest[burgers].symbols["ClassicSauces"]).toBeDefined(); + // Expected symbols to be removed + expect(newManifest[burgers].symbols["MAX_BURGERS"]).not.toBeDefined(); + expect(newManifest[burgers].symbols["MAX"]).not.toBeDefined(); + expect(newManifest[burgers].symbols["Fries"]).not.toBeDefined(); + expect(newManifest[burgers].symbols["Drink_t"]).not.toBeDefined(); + expect(newManifest[burgers].symbols["Drink"]).not.toBeDefined(); + expect(newManifest[burgers].symbols["classicBurger"]).not.toBeDefined(); + expect(newManifest[burgers].symbols["destroy_burger"]).not.toBeDefined(); + expect(newManifest[burgers].symbols["get_burger_by_id"]).not.toBeDefined(); + expect(newManifest[burgers].symbols["get_cheapest_burger"]).not + .toBeDefined(); + }); + + test("extracts Drink_t", () => { + const symbolsToExtract = new Map< + string, + { filePath: string; symbols: Set } + >(); + symbolsToExtract.set(burgers, { + filePath: burgers, + symbols: new Set(["Drink_t"]), + }); + const extractedFiles = extractor.extractSymbols(symbolsToExtract); + expect(extractedFiles.size).toBe(1); + const newManifest = generateCDependencyManifest( + extractedFiles, + dummyLocalConfig, + ); + expect(newManifest[burgers]).toBeDefined(); + // Expected symbols to be kept + expect(newManifest[burgers].symbols["Drink_t"]).toBeDefined(); + expect(newManifest[burgers].symbols["Drink"]).toBeDefined(); + expect(newManifest[burgers].symbols["BURGERS_H"]).toBeDefined(); + // Expected symbols to be removed + expect(Object.keys(newManifest[burgers].symbols).length).toBe(3); + }); + + test("keeps all.h", () => { + const symbolsToExtract = new Map< + string, + { filePath: string; symbols: Set } + >(); + symbolsToExtract.set(main, { + filePath: main, + symbols: new Set(["main"]), + }); + const extractedFiles = extractor.extractSymbols(symbolsToExtract); + expect(extractedFiles.size).toBe(6); + const newManifest = generateCDependencyManifest( + extractedFiles, + dummyLocalConfig, + ); + expect(newManifest[all]).toBeDefined(); + }); + + test("deletes impossible include", () => { + const symbolsToExtract = new Map< + string, + { filePath: string; symbols: Set } + >(); + symbolsToExtract.set(errorsh, { + filePath: errorsh, + symbols: new Set(["typedef"]), + }); + const extractedFiles = extractor.extractSymbols(symbolsToExtract); + expect(extractedFiles.size).toBe(1); + expect(extractedFiles.get(errorsh)).toBeDefined(); + expect( + extractedFiles.get(errorsh)?.content.includes( + `#include "thisfiledoesnotexist.h"`, + ), + ).toBe(false); + }); +}); diff --git a/packages/cli/src/languagePlugins/c/extractor/index.ts b/packages/cli/src/languagePlugins/c/extractor/index.ts new file mode 100644 index 00000000..42530354 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/extractor/index.ts @@ -0,0 +1,323 @@ +import type { DependencyManifest } from "@napi/shared"; +import { CSymbolRegistry } from "../symbolRegistry/index.ts"; +import { CIncludeResolver } from "../includeResolver/index.ts"; +import type Parser from "npm:tree-sitter"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; +import type { CFile, Symbol } from "../symbolRegistry/types.ts"; +import type { ExportedFile } from "./types.ts"; +import { C_DECLARATION_QUERY } from "../headerResolver/queries.ts"; +import { C_IFDEF_QUERY } from "./queries.ts"; +import { CInvocationResolver } from "../invocationResolver/index.ts"; +import type z from "npm:zod"; +import type { localConfigSchema } from "../../../config/localConfig.ts"; +import { join } from "@std/path"; + +export class CExtractor { + manifest: DependencyManifest; + registry: Map; + includeResolver: CIncludeResolver; + invocationResolver: CInvocationResolver; + + constructor( + files: Map, + manifest: DependencyManifest, + napiConfig: z.infer, + ) { + this.manifest = manifest; + const parsedFiles = new Map< + string, + { path: string; rootNode: Parser.SyntaxNode } + >(); + for (const [filePath, file] of files) { + parsedFiles.set(filePath, { + path: file.path, + rootNode: cParser.parse(file.content).rootNode, + }); + } + const symbolRegistry = new CSymbolRegistry(parsedFiles); + this.registry = symbolRegistry.getRegistry(); + const outDir = napiConfig.outDir; + const includeDirs = napiConfig["c"]?.includedirs ?? []; + if (outDir) { + includeDirs.push(...includeDirs.map((i) => join(outDir, i))); + } + this.includeResolver = new CIncludeResolver(symbolRegistry, includeDirs); + this.invocationResolver = new CInvocationResolver(this.includeResolver); + } + + /** + * Finds the first-level dependencies of a symbol in the manifest. + * @param symbol - The symbol to find dependencies for. + * @returns An array of symbols that are dependencies of the given symbol. + */ + #findDependencies(symbol: Symbol): Symbol[] { + const dependencies: Symbol[] = [symbol]; + const symbolDependencies = this.manifest[symbol.declaration.filepath] + ?.symbols[symbol.name].dependencies; + for ( + const [filepath, dependencyinfo] of Object.entries(symbolDependencies) + ) { + const dependencyFile = this.registry.get(filepath); + if (dependencyFile) { + for (const symbolName of Object.keys(dependencyinfo.symbols)) { + const dependencySymbol = dependencyFile.symbols.get(symbolName); + if (dependencySymbol) { + dependencies.push(dependencySymbol); + } + } + } + } + return dependencies; + } + + /** + * Finds all dependencies of a given symbol. + * @param symbol - The symbol for which to find dependencies. + * @returns An array of symbols. + */ + #findAllDependencies(symbol: Symbol): Symbol[] { + const dependencies: Symbol[] = []; + const visited = new Set(); + const stack = [symbol]; + + while (stack.length > 0) { + const currentSymbol = stack.pop()!; + if (visited.has(currentSymbol)) { + continue; + } + visited.add(currentSymbol); + dependencies.push(currentSymbol); + const currentDependencies = this.#findDependencies(currentSymbol); + stack.push(...currentDependencies); + } + return dependencies; + } + + /** + * Build a map of files and their symbols to keep. + * @param symbolsToKeep - The symbols to keep. + * @returns A map of file paths to their corresponding ExportedFile objects. + */ + #buildFileMap(symbolsToKeep: Symbol[]): Map { + const exportedFiles = new Map(); + for (const symbol of symbolsToKeep) { + const filepath = symbol.declaration.filepath; + if (!exportedFiles.has(filepath)) { + const fileInRegistry = this.registry.get(filepath); + if (!fileInRegistry) { + throw new Error(`File not found: ${filepath}`); + } + const originalFile = fileInRegistry.file; + // Ifdefs are instances of things that aren't symbols yet invoke a symbol + const ifdefs = C_IFDEF_QUERY.captures(originalFile.rootNode).map((n) => + n.node.text + ); + const definesToKeep = ifdefs.map((i) => fileInRegistry.symbols.get(i)!) + .filter((i) => i); + const symbols = new Map(); + for (const define of definesToKeep) { + symbols.set(define.name, define); + } + exportedFiles.set(filepath, { + symbols, + originalFile, + strippedFile: originalFile, + }); + } + const exportedFile = exportedFiles.get(filepath)!; + const symbolName = symbol.name; + if (!exportedFile.symbols.has(symbolName)) { + exportedFile.symbols.set(symbolName, symbol); + } + // Keep the files that recursively lead to a symbol we need + const invocations = this.invocationResolver.getInvocationsForSymbol( + symbol, + ); + const filestokeep = Array.from( + invocations.resolved.values().map((s) => + this.includeResolver.findInclusionChain( + symbol.declaration.filepath, + s.symbol, + ) + ).filter((c) => c !== undefined), + ).flatMap((c) => c.flatMap((f) => this.registry.get(f)!)); + for (const f of filestokeep) { + if (!exportedFiles.has(f.file.path)) { + const ifdefs = C_IFDEF_QUERY.captures(f.file.rootNode).map((n) => + n.node.text + ); + const definesToKeep = ifdefs.map((i) => f.symbols.get(i)!) + .filter((i) => i); + const symbols = new Map(); + for (const define of definesToKeep) { + symbols.set(define.name, define); + } + exportedFiles.set(f.file.path, { + symbols, + originalFile: f.file, + strippedFile: f.file, + }); + } + } + } + return exportedFiles; + } + + /** + * Edits the files to include only the symbols that are needed. + * @param files - The files to edit. + */ + #stripFiles(files: Map) { + for (const [, file] of files) { + const rootNode = file.originalFile.rootNode; + const originalText = rootNode.text; // Original file content + const symbolsToKeep = new Set( + file.symbols.values().map((s) => s.declaration.node), + ); + const symbolsToRemove = new Set(); + const matches = C_DECLARATION_QUERY.captures(rootNode); + + for (const match of matches) { + const symbolNode = match.node; + if (!symbolsToKeep.has(symbolNode)) { + symbolsToRemove.add(symbolNode); + } + } + + // Helper function to recursively filter nodes + const filterNodes = (node: Parser.SyntaxNode): string => { + if (symbolsToRemove.has(node)) { + return ""; // Skip this node + } + + // If the node has children, process them recursively + if (["translation_unit", "preproc_ifdef"].includes(node.type)) { + let result = ""; + let lastEndIndex = node.startIndex; + + for (const child of node.children) { + // Append the text between the last node and the current child + result += originalText.slice(lastEndIndex, child.startIndex); + result += filterNodes(child); // Process the child + lastEndIndex = child.endIndex; + } + + // Append the text after the last child + result += originalText.slice(lastEndIndex, node.endIndex); + return result; + } + + // If the node has no children, return its text + return originalText.slice(node.startIndex, node.endIndex); + }; + + // Rebuild the file content by filtering nodes + const newFileContent = filterNodes(rootNode); + + // Compactify the file content + const compactedContent = this.#compactifyFile(newFileContent); + + // Parse the new content and update the stripped file + const strippedFile = cParser.parse(compactedContent); + file.strippedFile = { + path: file.originalFile.path, + rootNode: strippedFile.rootNode, + }; + } + } + + #removeDeletedIncludes( + files: Map, + ) { + const newproject: Map< + string, + { path: string; rootNode: Parser.SyntaxNode } + > = new Map(); + for (const [key, value] of files) { + newproject.set(key, value.strippedFile); + } + const newregistry = new CSymbolRegistry(newproject); + const newincluderes = new CIncludeResolver( + newregistry, + this.includeResolver.includeDirs, + ); + newincluderes.getInclusions(); + for (const [key, value] of files) { + const unresolved = newincluderes.unresolvedDirectives.get(key); + if (unresolved) { + let filetext = value.strippedFile.rootNode.text; + for (const path of unresolved) { + filetext = filetext.replace(`#include "${path}"`, ""); + } + filetext = this.#compactifyFile(filetext); + value.strippedFile.rootNode = cParser.parse(filetext).rootNode; + } + } + } + + #compactifyFile( + filetext: string, + ): string { + // Remove empty lines and useless semicolons + filetext = filetext.replace(/^\s*;\s*$/gm, ""); // Remove empty lines with semicolons + filetext = filetext.replace(/^\s*[\r\n]+/gm, "\n"); // Remove empty lines + return filetext; + } + + /** + * Finds the dependencies for a map of symbols. + * @param symbolsMap - A map of file paths to their corresponding symbols. + * @returns A set of symbols that are dependencies of the given symbols. + */ + #findDependenciesForMap( + symbolsMap: Map< + string, + { + filePath: string; + symbols: Set; + } + >, + ): Symbol[] { + const symbolsToExtract: Symbol[] = []; + for (const [filePath, symbolInfo] of symbolsMap) { + const file = this.registry.get(filePath); + if (!file) { + throw new Error(`File not found: ${filePath}`); + } + const symbols = Array.from(symbolInfo.symbols); + for (const symbolName of symbols) { + const symbol = file.symbols.get(symbolName); + if (symbol) { + const dependencies = this.#findAllDependencies(symbol); + symbolsToExtract.push(...dependencies); + } + } + } + // Remove duplicates + return Array.from(new Set(symbolsToExtract)); + } + + extractSymbols( + symbolsMap: Map< + string, + { + filePath: string; + symbols: Set; + } + >, + ): Map { + const symbolsToExtract = this.#findDependenciesForMap(symbolsMap); + const filesToExport = this.#buildFileMap(symbolsToExtract); + this.#stripFiles(filesToExport); + this.#removeDeletedIncludes(filesToExport); + const exportedFiles = new Map(); + for (const [filePath, file] of filesToExport) { + const content = file.strippedFile.rootNode.text; + exportedFiles.set(filePath, { + path: filePath, + content, + }); + } + return exportedFiles; + } +} diff --git a/packages/cli/src/languagePlugins/c/extractor/queries.ts b/packages/cli/src/languagePlugins/c/extractor/queries.ts new file mode 100644 index 00000000..fdecd645 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/extractor/queries.ts @@ -0,0 +1,8 @@ +import Parser from "npm:tree-sitter"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; + +export const C_IFDEF_QUERY = new Parser.Query( + cParser.getLanguage(), + `(preproc_ifdef + name: (identifier) @def)`, +); diff --git a/packages/cli/src/languagePlugins/c/extractor/types.ts b/packages/cli/src/languagePlugins/c/extractor/types.ts new file mode 100644 index 00000000..f123fbbd --- /dev/null +++ b/packages/cli/src/languagePlugins/c/extractor/types.ts @@ -0,0 +1,14 @@ +import type { Symbol } from "../symbolRegistry/types.ts"; +import type Parser from "npm:tree-sitter"; + +export interface ExportedFile { + symbols: Map; + originalFile: { + path: string; + rootNode: Parser.SyntaxNode; + }; + strippedFile: { + path: string; + rootNode: Parser.SyntaxNode; + }; +} diff --git a/packages/cli/src/languagePlugins/c/headerResolver/index.test.ts b/packages/cli/src/languagePlugins/c/headerResolver/index.test.ts new file mode 100644 index 00000000..86a81af5 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/headerResolver/index.test.ts @@ -0,0 +1,325 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { cFilesFolder, getCFilesMap } from "../testFiles/index.ts"; +import { CHeaderResolver } from "./index.ts"; +import { join } from "@std/path"; + +describe("CHeaderResolver", () => { + const cFilesMap = getCFilesMap(); + const resolver = new CHeaderResolver(); + const burgers = join(cFilesFolder, "burgers.h"); + const crashcases = join(cFilesFolder, "crashcases.h"); + const errorsh = join(cFilesFolder, "errors.h"); + const oldmanh = join(cFilesFolder, "oldman.h"); + const file = cFilesMap.get(burgers); + if (!file) { + throw new Error(`File not found: ${burgers}`); + } + const ccfile = cFilesMap.get(crashcases); + if (!ccfile) { + throw new Error(`File not found: ${crashcases}`); + } + const errorsfile = cFilesMap.get(errorsh); + if (!errorsfile) { + throw new Error(`File not found: ${errorsh}`); + } + const exportedSymbols = resolver.resolveSymbols(file); + + test("should resolve symbols in C header files", () => { + expect(exportedSymbols).toHaveLength(16); + }); + + test("resolves structs", () => { + const burger = exportedSymbols.find((symbol) => symbol.name === "Burger"); + expect(burger).toBeDefined(); + if (!burger) { + throw new Error("burger is undefined"); + } + expect(burger.type).toBe("struct"); + expect(burger.specifiers).toEqual([]); + expect(burger.qualifiers).toEqual([]); + expect(burger.node.type).toBe("struct_specifier"); + if (!burger.identifierNode) { + throw new Error("burger.identifierNode is undefined"); + } + expect(burger.identifierNode.type).toBe("type_identifier"); + expect(burger.filepath).toBe(burgers); + }); + + test("resolves unions", () => { + const sauce = exportedSymbols.find((symbol) => symbol.name === "Sauce"); + expect(sauce).toBeDefined(); + if (!sauce) { + throw new Error("sauce is undefined"); + } + expect(sauce.type).toBe("union"); + expect(sauce.specifiers).toEqual([]); + expect(sauce.qualifiers).toEqual([]); + expect(sauce.node.type).toBe("union_specifier"); + if (!sauce.identifierNode) { + throw new Error("sauce.identifierNode is undefined"); + } + expect(sauce.identifierNode.type).toBe("type_identifier"); + expect(sauce.filepath).toBe(burgers); + }); + + test("resolves enums", () => { + const condiment = exportedSymbols.find( + (symbol) => symbol.name === "Condiment", + ); + expect(condiment).toBeDefined(); + if (!condiment) { + throw new Error("condiment is undefined"); + } + expect(condiment.type).toBe("enum"); + expect(condiment.specifiers).toEqual([]); + expect(condiment.qualifiers).toEqual([]); + expect(condiment.node.type).toBe("enum_specifier"); + if (!condiment.identifierNode) { + throw new Error("condiment.identifierNode is undefined"); + } + expect(condiment.identifierNode.type).toBe("type_identifier"); + expect(condiment.filepath).toBe(burgers); + + const classicsauces = exportedSymbols.find( + (symbol) => symbol.name === "ClassicSauces", + ); + expect(classicsauces).toBeDefined(); + if (!classicsauces) { + throw new Error("classicsauces is undefined"); + } + expect(classicsauces.type).toBe("enum"); + + const drink_t = exportedSymbols.find((symbol) => symbol.name === "Drink_t"); + expect(drink_t).toBeDefined(); + if (!drink_t) { + throw new Error("drink_t is undefined"); + } + expect(drink_t.type).toBe("enum"); + expect(drink_t.specifiers).toEqual([]); + expect(drink_t.qualifiers).toEqual([]); + expect(drink_t.node.type).toBe("enum_specifier"); + if (!drink_t.identifierNode) { + throw new Error("drink_t.identifierNode is undefined"); + } + expect(drink_t.identifierNode.type).toBe("type_identifier"); + expect(drink_t.filepath).toBe(burgers); + }); + + test("resolves variables", () => { + const classicburger = exportedSymbols.find( + (symbol) => symbol.name === "classicBurger", + ); + expect(classicburger).toBeDefined(); + if (!classicburger) { + throw new Error("classicburger is undefined"); + } + expect(classicburger.type).toBe("variable"); + expect(classicburger.specifiers).toEqual([]); + expect(classicburger.qualifiers).toEqual(["const"]); + expect(classicburger.node.type).toBe("declaration"); + if (!classicburger.identifierNode) { + throw new Error("classicburger.identifierNode is undefined"); + } + expect(classicburger.identifierNode.type).toBe("identifier"); + expect(classicburger.filepath).toBe(burgers); + + const burger_count = exportedSymbols.find( + (symbol) => symbol.name === "burger_count", + ); + expect(burger_count).toBeDefined(); + if (!burger_count) { + throw new Error("burger_count is undefined"); + } + expect(burger_count.type).toBe("variable"); + expect(burger_count.specifiers).toEqual(["static"]); + expect(burger_count.qualifiers).toEqual([]); + expect(burger_count.node.type).toBe("declaration"); + if (!burger_count.identifierNode) { + throw new Error("burger_count.identifierNode is undefined"); + } + expect(burger_count.identifierNode.type).toBe("identifier"); + expect(burger_count.filepath).toBe(burgers); + }); + + test("resolves macro constants", () => { + const burgers_h = exportedSymbols.find( + (symbol) => symbol.name === "BURGERS_H", + ); + expect(burgers_h).toBeDefined(); + if (!burgers_h) { + throw new Error("burgers_h is undefined"); + } + expect(burgers_h.type).toBe("macro_constant"); + expect(burgers_h.specifiers).toEqual([]); + expect(burgers_h.qualifiers).toEqual([]); + expect(burgers_h.node.type).toBe("preproc_def"); + if (!burgers_h.identifierNode) { + throw new Error("burgers_h.identifierNode is undefined"); + } + expect(burgers_h.identifierNode.type).toBe("identifier"); + expect(burgers_h.filepath).toBe(burgers); + + const max_burgers = exportedSymbols.find( + (symbol) => symbol.name === "MAX_BURGERS", + ); + expect(max_burgers).toBeDefined(); + if (!max_burgers) { + throw new Error("max_burgers is undefined"); + } + expect(max_burgers.type).toBe("macro_constant"); + expect(max_burgers.specifiers).toEqual([]); + expect(max_burgers.qualifiers).toEqual([]); + expect(max_burgers.node.type).toBe("preproc_def"); + if (!max_burgers.identifierNode) { + throw new Error("max_burgers.identifierNode is undefined"); + } + expect(max_burgers.identifierNode.type).toBe("identifier"); + expect(max_burgers.filepath).toBe(burgers); + }); + + test("resolves function signatures", () => { + const function_names = exportedSymbols + .filter((symbol) => symbol.type === "function_signature") + .map((symbol) => symbol.name); + expect(function_names).toHaveLength(4); + expect(function_names).toContain("get_burger_by_id"); + expect(function_names).toContain("get_cheapest_burger"); + expect(function_names).toContain("create_burger"); + expect(function_names).toContain("destroy_burger"); + expect(function_names).not.toContain("MAX"); + }); + + test("resolves macro functions", () => { + const max_macro = exportedSymbols.find((symbol) => symbol.name === "MAX"); + expect(max_macro).toBeDefined(); + if (!max_macro) { + throw new Error("max_macro is undefined"); + } + expect(max_macro.type).toBe("macro_function"); + expect(max_macro.specifiers).toEqual([]); + expect(max_macro.qualifiers).toEqual([]); + expect(max_macro.node.type).toBe("preproc_function_def"); + if (!max_macro.identifierNode) { + throw new Error("max_macro.identifierNode is undefined"); + } + expect(max_macro.identifierNode.type).toBe("identifier"); + expect(max_macro.filepath).toBe(burgers); + }); + + test("resolves typedefs", () => { + const fries = exportedSymbols.find((symbol) => symbol.name === "Fries"); + expect(fries).toBeDefined(); + if (!fries) { + throw new Error("fries is undefined"); + } + expect(fries.type).toBe("typedef"); + expect(fries.specifiers).toEqual([]); + expect(fries.qualifiers).toEqual([]); + expect(fries.node.type).toBe("type_definition"); + if (!fries.identifierNode) { + throw new Error("fries.identifierNode is undefined"); + } + expect(fries.identifierNode.type).toBe("type_identifier"); + expect(fries.filepath).toBe(burgers); + + const drink = exportedSymbols.find((symbol) => symbol.name === "Drink"); + expect(drink).toBeDefined(); + if (!drink) { + throw new Error("drink is undefined"); + } + expect(drink.type).toBe("typedef"); + expect(drink.specifiers).toEqual([]); + expect(drink.qualifiers).toEqual([]); + expect(drink.node.type).toBe("type_definition"); + if (!drink.identifierNode) { + throw new Error("drink.identifierNode is undefined"); + } + expect(drink.identifierNode.type).toBe("type_identifier"); + expect(drink.filepath).toBe(burgers); + }); + + test("Crash Cases", () => { + const ccexportedSymbols = resolver.resolveSymbols(ccfile); + expect(ccexportedSymbols).toBeDefined(); + + const sprite = ccexportedSymbols.find((s) => s.name === "Sprite"); + expect(sprite).toBeDefined(); + if (!sprite) { + throw new Error("sprite is undefined"); + } + expect(sprite.type).toBe("struct"); + expect(sprite.specifiers).toEqual([]); + expect(sprite.qualifiers).toEqual([]); + expect(sprite.node.type).toBe("struct_specifier"); + if (!sprite.identifierNode) { + throw new Error("sprite.identifierNode is undefined"); + } + expect(sprite.identifierNode.type).toBe("type_identifier"); + expect(sprite.filepath).toBe(crashcases); + + const placeholderfunction = ccexportedSymbols.find( + (s) => s.name === "PlaceholderFunction", + ); + expect(placeholderfunction).toBeDefined(); + if (!placeholderfunction) { + throw new Error("placeholderfunction is undefined"); + } + expect(placeholderfunction.type).toBe("function_signature"); + expect(placeholderfunction.specifiers).toEqual([]); + expect(placeholderfunction.qualifiers).toEqual([]); + expect(placeholderfunction.node.type).toBe("declaration"); + if (!placeholderfunction.identifierNode) { + throw new Error("placeholderfunction.identifierNode is undefined"); + } + expect(placeholderfunction.identifierNode.type).toBe("identifier"); + expect(placeholderfunction.filepath).toBe(crashcases); + + const gmtfwa = ccexportedSymbols.find( + (s) => s.name === "gMovementTypeFuncs_WanderAround", + ); + expect(gmtfwa).toBeDefined(); + if (!gmtfwa) { + throw new Error("gmtfwa is undefined"); + } + expect(gmtfwa.type).toBe("variable"); + expect(gmtfwa.specifiers).toEqual([]); + expect(gmtfwa.qualifiers).toEqual([]); + expect(gmtfwa.node.type).toBe("declaration"); + if (!gmtfwa.identifierNode) { + throw new Error("gmtfwa.identifierNode is undefined"); + } + expect(gmtfwa.identifierNode.type).toBe("identifier"); + expect(gmtfwa.filepath).toBe(crashcases); + }); + + test("resolves unnamed types", () => { + const exportedErrorSymbols = resolver.resolveSymbols(errorsfile); + expect(exportedErrorSymbols).toBeDefined(); + const symbolNames = exportedErrorSymbols.map((symbol) => symbol.name); + expect(symbolNames).toContain("#NAPI_UNNAMED_ENUM_0"); + }); + + test("resolves typedefs with same name as associated struct", () => { + const oldmanSymbols = resolver.resolveSymbols( + cFilesMap.get(oldmanh)!, + ); + expect(oldmanSymbols).toBeDefined(); + const oldmen = oldmanSymbols.filter((s) => s.name === "OldMan"); + expect(oldmen).toHaveLength(2); + const oldman = oldmen.find((s) => s.type === "typedef"); + expect(oldman).toBeDefined(); + if (!oldman) { + throw new Error("OldMan is undefined"); + } + expect(oldman.type).toBe("typedef"); + expect(oldman.specifiers).toEqual([]); + expect(oldman.qualifiers).toEqual([]); + expect(oldman.node.type).toBe("type_definition"); + if (!oldman.identifierNode) { + throw new Error("oldman.identifierNode is undefined"); + } + expect(oldman.identifierNode.type).toBe("type_identifier"); + expect(oldman.filepath).toBe(oldmanh); + }); +}); diff --git a/packages/cli/src/languagePlugins/c/headerResolver/index.ts b/packages/cli/src/languagePlugins/c/headerResolver/index.ts new file mode 100644 index 00000000..3f92c298 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/headerResolver/index.ts @@ -0,0 +1,110 @@ +import type { + ExportedSymbol, + StorageClassSpecifier, + SymbolType, + TypeQualifier, +} from "./types.ts"; +import { C_DECLARATION_QUERY } from "./queries.ts"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; +import type Parser from "npm:tree-sitter"; + +export class CHeaderResolver { + parser: Parser = cParser; + #unnamedSymbolCounter = 0; + + /** + * Resolves the symbols in a C header file. + * @param file The file to resolve. + * @returns An array of exported symbols. + */ + resolveSymbols(file: { + path: string; + rootNode: Parser.SyntaxNode; + }): ExportedSymbol[] { + const exportedSymbols: ExportedSymbol[] = []; + const query = C_DECLARATION_QUERY; + const captures = query.captures(file.rootNode); + for (const capture of captures) { + if (capture.name !== "decl" && capture.name !== "function_definition") { + let idNode: Parser.SyntaxNode | null; + if (capture.name !== "typedef") { + idNode = capture.node.childForFieldName("name"); + } else { + idNode = capture.node.childForFieldName( + "declarator", + ); + } + let name: string; + if (!idNode) { + name = `#NAPI_UNNAMED_${capture.name.toUpperCase()}_${this + .#unnamedSymbolCounter++}`; + } else { + name = idNode.text; + } + exportedSymbols.push({ + name: name, + type: capture.name as SymbolType, + node: capture.node, + identifierNode: idNode, + filepath: file.path, + specifiers: [], + qualifiers: [], + }); + } else { + const specifiers = capture.node.children + .filter((child) => child.type === "storage_class_specifier") + .map((child) => child.text); + const qualifiers = capture.node.children + .filter((child) => child.type === "type_qualifier") + .map((child) => child.text); + let currentNode = capture.node; + // Traverse the tree to find the identifier node + // This is a workaround for the fact that the identifier node is not always the first child + // (e.g. in pointers or arrays) + while ( + !currentNode.childForFieldName("declarator") || + currentNode.childForFieldName("declarator")?.type !== "identifier" + ) { + if (!currentNode.childForFieldName("declarator")) { + if (!currentNode.firstNamedChild) { + throw new Error( + `Could not find a named child for ${currentNode.text}`, + ); + } + currentNode = currentNode.firstNamedChild; + } else { + if (!currentNode.childForFieldName("declarator")) { + throw new Error( + `Could not find a declarator for ${currentNode.text}`, + ); + } + currentNode = currentNode.childForFieldName( + "declarator", + ) as Parser.SyntaxNode; + } + } + const type = capture.name === "function_definition" + ? "function_definition" + : currentNode.type === "function_declarator" + ? "function_signature" + : "variable"; + const idNode = currentNode.childForFieldName("declarator"); + if (!idNode) { + throw new Error( + `Couldn't find identifier node for symbol :\n${capture.node.text}`, + ); + } + exportedSymbols.push({ + name: idNode.text, + type: type as SymbolType, + node: capture.node, + identifierNode: idNode, + filepath: file.path, + specifiers: specifiers as StorageClassSpecifier[], + qualifiers: qualifiers as TypeQualifier[], + }); + } + } + return exportedSymbols; + } +} diff --git a/packages/cli/src/languagePlugins/c/headerResolver/queries.ts b/packages/cli/src/languagePlugins/c/headerResolver/queries.ts new file mode 100644 index 00000000..114a8cc5 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/headerResolver/queries.ts @@ -0,0 +1,75 @@ +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; +import Parser from "npm:tree-sitter"; + +/** Query that catches every declaration including macros in a header file + * Does not catch function definitions + */ +export const C_DECLARATION_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (translation_unit + [ + (declaration) @decl + (struct_specifier + name: (_)) @struct + (enum_specifier) @enum + (union_specifier + name: (_)) @union + (function_definition) @function_definition + (type_definition + type:[ + (struct_specifier + name: (_) + body: (_)) @struct + (struct_specifier !name) + (struct_specifier !body) + (enum_specifier + name: (_) + body: (_)) @enum + (enum_specifier !name) + (enum_specifier !body) + (union_specifier + name: (_) + body: (_)) @union + (union_specifier !name) + (union_specifier !body) + (type_identifier) + (primitive_type) + ] + ) @typedef + ]) + (preproc_ifdef + [ + (declaration) @decl + (struct_specifier + name: (_)) @struct + (enum_specifier) @enum + (union_specifier + name: (_)) @union + (function_definition) @function_definition + (type_definition + type:[ + (struct_specifier + name: (_) + body: (_)) @struct + (struct_specifier !name) + (struct_specifier !body) + (enum_specifier + name: (_) + body: (_)) @enum + (enum_specifier !name) + (enum_specifier !body) + (union_specifier + name: (_) + body: (_)) @union + (union_specifier !name) + (union_specifier !body) + (type_identifier) + (primitive_type) + ] + ) @typedef + ]) + (preproc_def) @macro_constant + (preproc_function_def) @macro_function + `, +); diff --git a/packages/cli/src/languagePlugins/c/headerResolver/types.ts b/packages/cli/src/languagePlugins/c/headerResolver/types.ts new file mode 100644 index 00000000..7e68a519 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/headerResolver/types.ts @@ -0,0 +1,67 @@ +import type Parser from "npm:tree-sitter"; + +// Constants representing different types of symbols in C +export const C_STRUCT_TYPE = "struct"; +export const C_UNION_TYPE = "union"; +export const C_ENUM_TYPE = "enum"; +export const C_FUNCTION_DEFINITION_TYPE = "function_definition"; +export const C_FUNCTION_SIGNATURE_TYPE = "function_signature"; +export const C_MACRO_FUNCTION_TYPE = "macro_function"; +export const C_MACRO_CONSTANT_TYPE = "macro_constant"; +export const C_VARIABLE_TYPE = "variable"; +export const C_TYPEDEF_TYPE = "typedef"; + +// Constants representing different storage class specifiers in C +export const C_AUTO_SPECIFIER = "auto"; +export const C_REGISTER_SPECIFIER = "register"; +export const C_STATIC_SPECIFIER = "static"; +export const C_EXTERN_SPECIFIER = "extern"; + +// Constants representing different type qualifiers in C +export const C_CONST_QUALIFIER = "const"; +export const C_VOLATILE_QUALIFIER = "volatile"; +export const C_RESTRICT_QUALIFIER = "restrict"; +export const C_ATOMIC_QUALIFIER = "_Atomic"; + +/** Type alias for the different symbol types */ +export type SymbolType = + | typeof C_STRUCT_TYPE + | typeof C_UNION_TYPE + | typeof C_ENUM_TYPE + | typeof C_FUNCTION_DEFINITION_TYPE + | typeof C_FUNCTION_SIGNATURE_TYPE + | typeof C_MACRO_FUNCTION_TYPE + | typeof C_MACRO_CONSTANT_TYPE + | typeof C_VARIABLE_TYPE + | typeof C_TYPEDEF_TYPE; + +/** Type alias for the different storage class specifiers */ +export type StorageClassSpecifier = + | typeof C_AUTO_SPECIFIER + | typeof C_REGISTER_SPECIFIER + | typeof C_STATIC_SPECIFIER + | typeof C_EXTERN_SPECIFIER; + +/** Type alias for the different type qualifiers */ +export type TypeQualifier = + | typeof C_CONST_QUALIFIER + | typeof C_VOLATILE_QUALIFIER + | typeof C_RESTRICT_QUALIFIER; + +/** Interface representing an exported symbol */ +export interface ExportedSymbol { + /** The name of the symbol */ + name: string; + /** The type of the symbol (i.e. struct, union, enum, etc.) */ + type: SymbolType; + /** The storage class specifiers of the symbol (i.e. static, extern) */ + specifiers: StorageClassSpecifier[]; + /** The type qualifiers of the symbol (i.e. const, volatile) */ + qualifiers: TypeQualifier[]; + /** The syntax node corresponding to the symbol */ + node: Parser.SyntaxNode; + /** The syntax node corresponding to the identifier */ + identifierNode: Parser.SyntaxNode | null; + /** The path of the symbol's file */ + filepath: string; +} diff --git a/packages/cli/src/languagePlugins/c/includeResolver/index.test.ts b/packages/cli/src/languagePlugins/c/includeResolver/index.test.ts new file mode 100644 index 00000000..c2faa1dc --- /dev/null +++ b/packages/cli/src/languagePlugins/c/includeResolver/index.test.ts @@ -0,0 +1,92 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { cFilesFolder, getCFilesMap } from "../testFiles/index.ts"; +import { CSymbolRegistry } from "../symbolRegistry/index.ts"; +import { CIncludeResolver } from "./index.ts"; +import { join } from "@std/path"; +import type { FunctionSignature } from "../symbolRegistry/types.ts"; + +describe("CIncludeResolver", () => { + const cFilesMap = getCFilesMap(); + const registry = new CSymbolRegistry(cFilesMap); + const includeResolver = new CIncludeResolver(registry); + const burgersh = join(cFilesFolder, "burgers.h"); + const burgersc = join(cFilesFolder, "burgers.c"); + const allh = join(cFilesFolder, "all.h"); + const main = join(cFilesFolder, "main.c"); + const errorsh = join(cFilesFolder, "errors.h"); + const inclusions = includeResolver.getInclusions(); + + test("resolves inclusions for burgers.h", () => { + const bhinclusions = inclusions.get(burgersh); + if (!bhinclusions) { + throw new Error(`Inclusions not found for: ${burgersh}`); + } + expect(bhinclusions.filepath).toBe(burgersh); + expect(bhinclusions.symbols.size).toBe(0); + expect(bhinclusions.internal.children.size).toBe(0); + expect(bhinclusions.standard.size).toBe(1); + expect(Array.from(bhinclusions.standard.keys())[0]).toBe(""); + }); + + test("resolves inclusions for burgers.c", () => { + const bcinclusions = inclusions.get(burgersc); + if (!bcinclusions) { + throw new Error(`Inclusions not found for: ${burgersc}`); + } + expect(bcinclusions.filepath).toBe(burgersc); + expect(bcinclusions.symbols.size).toBe(32); + expect(bcinclusions.internal.children.size).toBe(1); + expect(bcinclusions.internal.children.get("burgers.h")).toBeDefined(); + expect(bcinclusions.standard.size).toBe(4); + const stdincludes = Array.from(bcinclusions.standard.keys()); + expect(stdincludes).toContain(""); + expect(stdincludes).toContain(""); + expect(stdincludes).toContain(""); + }); + + test("resolves inclusions for main.c", () => { + const maininclusions = inclusions.get(main); + if (!maininclusions) { + throw new Error(`Inclusions not found for: ${main}`); + } + expect(maininclusions.filepath).toBe(main); + expect(maininclusions.symbols.size).toBe(32 + 17); + expect(maininclusions.symbols.get("create_burger")?.includefile.file.path) + .toBe(allh); + expect(maininclusions.internal.children.size).toBe(1); + expect(maininclusions.internal.children.get("burgers.h")).not.toBeDefined(); + expect(maininclusions.internal.children.get("personnel.h")).not + .toBeDefined(); + expect(maininclusions.internal.children.get("all.h")).toBeDefined(); + const allinclusions = maininclusions.internal.children.get("all.h")!; + expect(allinclusions.children.get("burgers.h")).toBeDefined(); + expect(allinclusions.children.get("personnel.h")).toBeDefined(); + expect(maininclusions.standard.size).toBe(2); + const stdincludes = Array.from(maininclusions.standard.keys()); + expect(stdincludes).toContain(""); + }); + + test("finds signatures for functions", () => { + const burgers = registry.getRegistry().get(burgersh)?.symbols; + if (!burgers) { + throw new Error(`File not found: ${burgersh}`); + } + const create_burger = burgers.get("create_burger") as FunctionSignature; + const destroy = burgers.get("destroy_burger") as FunctionSignature; + const get = burgers.get("get_burger_by_id") as FunctionSignature; + const cheapest = burgers.get("get_cheapest_burger") as FunctionSignature; + expect(create_burger.definition).toBeDefined(); + expect(destroy.definition).toBeDefined(); + expect(get.definition).toBeDefined(); + expect(cheapest.definition).toBeDefined(); + }); + + test("correctly registers inexistant files", () => { + const unresolvedIncludes = includeResolver.unresolvedDirectives.get( + errorsh, + ); + expect(unresolvedIncludes).toBeDefined(); + expect(unresolvedIncludes).toContainEqual("thisfiledoesnotexist.h"); + }); +}); diff --git a/packages/cli/src/languagePlugins/c/includeResolver/index.ts b/packages/cli/src/languagePlugins/c/includeResolver/index.ts new file mode 100644 index 00000000..cfbdfe17 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/includeResolver/index.ts @@ -0,0 +1,204 @@ +import type { InclusionNode, Inclusions } from "./types.ts"; +import { C_INCLUDE_QUERY, C_STANDARD_INCLUDE_QUERY } from "./queries.ts"; +import type { CSymbolRegistry } from "../symbolRegistry/index.ts"; +import type Parser from "npm:tree-sitter"; +import { + type CFile, + FunctionDefinition, + FunctionSignature, + type Symbol, +} from "../symbolRegistry/types.ts"; +import { dirname, join } from "@std/path"; + +export class CIncludeResolver { + symbolRegistry: Map; + files: Map; + unresolvedDirectives: Map>; + includeDirs: string[] = []; + #inclusions?: Map; + #inclusionCache: Map; + + constructor(symbolRegistry: CSymbolRegistry, includeDirs: string[] = []) { + this.symbolRegistry = symbolRegistry.getRegistry(); + this.files = symbolRegistry.files; + this.#inclusionCache = new Map(); + this.unresolvedDirectives = new Map(); + this.includeDirs = includeDirs; + } + + getFile(filepath: string, sourcepath: string): CFile | undefined { + const filepaths = Array.from(this.symbolRegistry.keys()); + // 1. Check current file's directory + const sourceDir = dirname(sourcepath); + const pathfromrelative = join(sourceDir, filepath); + const corresponding1 = filepaths.find((f) => f === pathfromrelative); + if (corresponding1) { + return this.symbolRegistry.get(corresponding1); + } + // 2. Check include directories + for (const dir of this.includeDirs) { + const pathfrominclude = join(dir, filepath); + const corresponding = filepaths.find((f) => f === pathfrominclude); + if (corresponding) { + return this.symbolRegistry.get(corresponding); + } + } + // 3. Check from workspace root + const corresponding2 = filepaths.find((f) => f === filepath); + if (corresponding2) { + return this.symbolRegistry.get(corresponding2); + } + return undefined; + } + + /** + * Looks for a chain of inclusions starting from the given file + * that leads to the given symbol. + * @param file The file to start the search from. + * @param symbol The symbol to search for. + * @returns An array of file paths representing the chain of inclusions. + */ + findInclusionChain( + start: string, + symbol: Symbol, + ): string[] | undefined { + const inclusions = this.getInclusions().get(start); + if (!inclusions) { + return undefined; + } + const chain: string[] = []; + // Look inside the tree for a node that has a filepath that matches the symbol's file path + const findInclusion = (node: InclusionNode): boolean => { + if (node.filepath === symbol.declaration.filepath) { + chain.push(node.filepath); + return true; + } + for (const child of node.children.values()) { + if (findInclusion(child)) { + chain.push(node.filepath); + return true; + } + } + return false; + }; + if (findInclusion(inclusions.internal)) { + chain.reverse(); // Reverse to get the chain from start to symbol + return chain; + } + return undefined; + } + + /** + * Resolves the inclusions of a file. + * @param file The file to resolve inclusions for. + * @returns The inclusions of the file. + */ + #resolveInclusions( + file: CFile, + visitedFiles = new Set(), + ): Inclusions { + const inclusions: Inclusions = { + filepath: file.file.path, + symbols: new Map(), + internal: { + name: ".", + children: new Map(), + filepath: file.file.path, + }, + standard: new Map(), + }; + + // Add the current file to the visited set to prevent infinite recursion + visitedFiles.add(file.file.path); + + // Check for file in cache + if (this.#inclusionCache.has(file)) { + return this.#inclusionCache.get(file)!; + } + + const includeNodes = C_INCLUDE_QUERY.captures(file.file.rootNode); + const standardIncludeNodes = C_STANDARD_INCLUDE_QUERY.captures( + file.file.rootNode, + ); + + for (const node of includeNodes) { + const path = node.node.text; + const includedfile = this.getFile(path, file.file.path); + if (!includedfile) { + if (!this.unresolvedDirectives.has(file.file.path)) { + this.unresolvedDirectives.set(file.file.path, new Set()); + } + this.unresolvedDirectives.get(file.file.path)?.add(path); + } else if (!visitedFiles.has(includedfile.file.path)) { + // Add the included file's symbols to the current file's symbols + for (const [name, symbol] of includedfile.symbols) { + inclusions.symbols.set(name, { + symbol: symbol, + includefile: includedfile, + }); + } + // Recursively resolve inclusions for the included file + const nestedInclusions = this.#resolveInclusions( + includedfile, + visitedFiles, + ); + for (const [name, symbol] of nestedInclusions.symbols) { + inclusions.symbols.set(name, { + symbol: symbol.symbol, + includefile: includedfile, + }); + } + inclusions.internal.children.set(path, { + name: path, + filepath: includedfile.file.path, + children: nestedInclusions.internal.children, + parent: inclusions.internal, + }); + for (const node of nestedInclusions.standard) { + inclusions.standard.set(node[0], node[1]); + } + // Associate function definitions to their signatures + const funcdefs = Array.from( + file.symbols.entries().filter(([, s]) => + s instanceof FunctionDefinition + ).map(([, s]) => s as FunctionDefinition), + ); + for (const funcdef of funcdefs) { + if (inclusions.symbols.has(funcdef.name)) { + const symbol = inclusions.symbols.get(funcdef.name); + if (symbol && symbol.symbol instanceof FunctionSignature) { + funcdef.signature = symbol.symbol; + symbol.symbol.definition = funcdef; + } + } + } + } + } + + for (const node of standardIncludeNodes) { + const child = node.node.childForFieldName("path"); + if (child) { + inclusions.standard.set(child.text, node.node); + } + } + + // Save inclusions in cache + this.#inclusionCache.set(file, inclusions); + return inclusions; + } + + /** + * Retrieves the inclusions of all files and caches the results. + * @returns A map of file paths to their inclusions. + */ + getInclusions() { + if (!this.#inclusions) { + this.#inclusions = new Map(); + for (const file of this.symbolRegistry.values()) { + const inclusions = this.#resolveInclusions(file); + this.#inclusions.set(file.file.path, inclusions); + } + } + return this.#inclusions; + } +} diff --git a/packages/cli/src/languagePlugins/c/includeResolver/queries.ts b/packages/cli/src/languagePlugins/c/includeResolver/queries.ts new file mode 100644 index 00000000..4b202859 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/includeResolver/queries.ts @@ -0,0 +1,19 @@ +import Parser from "npm:tree-sitter"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; + +export const C_INCLUDE_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (preproc_include + path: (string_literal + (string_content) @include)) + `, +); + +export const C_STANDARD_INCLUDE_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (preproc_include + path: (system_lib_string)) @include + `, +); diff --git a/packages/cli/src/languagePlugins/c/includeResolver/types.ts b/packages/cli/src/languagePlugins/c/includeResolver/types.ts new file mode 100644 index 00000000..34629678 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/includeResolver/types.ts @@ -0,0 +1,36 @@ +import type Parser from "npm:tree-sitter"; +import type { CFile, Symbol } from "../symbolRegistry/types.ts"; + +/** Interface representing the #include statements of a file */ +export interface Inclusions { + /** The path of the file */ + filepath: string; + /** The imported symbols from internal imports */ + symbols: Map; + /** The tree of recursive inclusions of the internal imports */ + internal: InclusionNode; + /** The list of include directives of the standard imports */ + standard: Map; +} + +/** Interface representing a symbol imported through an include directive */ +export interface IncludedSymbol { + /** The corresponding symbol */ + symbol: Symbol; + /** The file that allows the usage of this symbol */ + includefile: CFile; + // Due to recursive inclusions, the file may not be the one that exports + // said symbol. Check "all.h" in the test files. +} + +/** Interface representing a node in the internal inclusion tree */ +export interface InclusionNode { + /** The path to the inclusion relative to its parent, or "." if root */ + name: string; + /** The complete file path */ + filepath: string; + /** The inclusions it contains, relative to itself */ + children: Map; + /** The parent of the node */ + parent?: InclusionNode; +} diff --git a/packages/cli/src/languagePlugins/c/invocationResolver/index.test.ts b/packages/cli/src/languagePlugins/c/invocationResolver/index.test.ts new file mode 100644 index 00000000..1d8d591d --- /dev/null +++ b/packages/cli/src/languagePlugins/c/invocationResolver/index.test.ts @@ -0,0 +1,153 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { cFilesFolder, getCFilesMap } from "../testFiles/index.ts"; +import { CSymbolRegistry } from "../symbolRegistry/index.ts"; +import { CIncludeResolver } from "../includeResolver/index.ts"; +import { CInvocationResolver } from "./index.ts"; +import type { Symbol } from "../symbolRegistry/types.ts"; +import { join } from "@std/path"; + +describe("CInvocationResolver", () => { + const cFilesMap = getCFilesMap(); + const symbolRegistry = new CSymbolRegistry(cFilesMap); + const includeResolver = new CIncludeResolver(symbolRegistry); + const invocationResolver = new CInvocationResolver(includeResolver); + const burgersh = join(cFilesFolder, "burgers.h"); + const burgersc = join(cFilesFolder, "burgers.c"); + const personnelc = join(cFilesFolder, "personnel.c"); + const crashcasesh = join(cFilesFolder, "crashcases.h"); + const errorsh = join(cFilesFolder, "errors.h"); + const main = join(cFilesFolder, "main.c"); + const registry = symbolRegistry.getRegistry(); + + test("resolves invocations for burgers.h", () => { + const symbols = registry.get(burgersh)?.symbols; + if (!symbols) { + throw new Error(`Symbol not found for: ${burgersh}`); + } + const sauce = symbols.get("Sauce") as Symbol; + const sauceinvocations = invocationResolver.getInvocationsForSymbol(sauce); + expect(sauceinvocations.resolved.size).toBe(1); + expect(sauceinvocations.resolved.get("ClassicSauces")).toBeDefined(); + + const fries = symbols.get("Fries") as Symbol; + const friesinvocations = invocationResolver.getInvocationsForSymbol(fries); + expect(friesinvocations.resolved.size).toBe(1); + expect(friesinvocations.resolved.get("Sauce")).toBeDefined(); + + const burger = symbols.get("Burger") as Symbol; + const burgerinvocations = invocationResolver.getInvocationsForSymbol( + burger, + ); + expect(burgerinvocations.resolved.size).toBe(2); + expect(burgerinvocations.resolved.get("Condiment")).toBeDefined(); + expect(burgerinvocations.resolved.get("Sauce")).toBeDefined(); + }); + + test("resolves invocations for burgers.c", () => { + const symbols = registry.get(burgersc)?.symbols; + if (!symbols) { + throw new Error(`Symbol not found for: ${burgersc}`); + } + const create_burger = symbols.get("create_burger") as Symbol; + const create_burger_invocations = invocationResolver + .getInvocationsForSymbol(create_burger); + const create_resolved = Array.from( + create_burger_invocations.resolved.keys(), + ); + expect(create_resolved.length).toBe(5); + expect(create_resolved).toContain("create_burger"); + expect(create_resolved).toContain("Burger"); + expect(create_resolved).toContain("Condiment"); + expect(create_resolved).toContain("Sauce"); + expect(create_resolved).toContain("burger_count"); + + const destroy_burger = symbols.get("destroy_burger") as Symbol; + const destroy_burger_invocations = invocationResolver + .getInvocationsForSymbol(destroy_burger); + const destroy_resolved = Array.from( + destroy_burger_invocations.resolved.keys(), + ); + expect(destroy_resolved.length).toBe(2); + expect(destroy_resolved).toContain("destroy_burger"); + expect(destroy_resolved).toContain("Burger"); + + const symbolsc = registry.get(burgersc)?.symbols; + if (!symbolsc) { + throw new Error(`Symbol not found for: ${burgersc}`); + } + + const burgers = symbolsc.get("burgers") as Symbol; + const burgers_invocations = invocationResolver.getInvocationsForSymbol( + burgers, + ); + const burgers_resolved = Array.from(burgers_invocations.resolved.keys()); + expect(burgers_resolved.length).toBe(2); + expect(burgers_resolved).toContain("Burger"); + expect(burgers_resolved).toContain("MAX_BURGERS"); + }); + + test("resolves invocations for personnel.c", () => { + const symbols = registry.get(personnelc)?.symbols; + if (!symbols) { + throw new Error(`Symbol not found for: ${personnelc}`); + } + const create_employee = symbols.get("create_employee") as Symbol; + const create_employee_invocations = invocationResolver + .getInvocationsForSymbol(create_employee); + const create_resolved = Array.from( + create_employee_invocations.resolved.keys(), + ); + expect(create_resolved.length).toBe(6); + expect(create_resolved).toContain("create_employee"); + expect(create_resolved).toContain("Employee"); + expect(create_resolved).toContain("Department"); + expect(create_resolved).toContain("MAX_EMPLOYEES"); + expect(create_resolved).toContain("employee_count"); + expect(create_resolved).toContain("employees"); + }); + + test("resolves invocations for main.c", () => { + const symbols = registry.get(main)?.symbols; + if (!symbols) { + throw new Error(`Symbol not found for: ${main}`); + } + const main_func = symbols.get("main") as Symbol; + const main_invocations = invocationResolver.getInvocationsForSymbol( + main_func, + ); + const main_resolved = Array.from(main_invocations.resolved.keys()); + expect(main_resolved.length).toBe(9); + expect(main_resolved).toContain("create_burger"); + expect(main_resolved).toContain("Burger"); + expect(main_resolved).toContain("create_employee"); + expect(main_resolved).toContain("Employee"); + expect(main_resolved).toContain("Condiment"); + expect(main_resolved).toContain("Sauce"); + }); + + test("Crash Cases", () => { + const symbols = registry.get(crashcasesh)?.symbols; + if (!symbols) { + throw new Error(`Symbol not found for: ${crashcasesh}`); + } + const crash = symbols.get("CpuFastFill") as Symbol; + const crash_invocations = invocationResolver.getInvocationsForSymbol(crash); + const crash_resolved = Array.from(crash_invocations.resolved.keys()); + expect(crash_resolved.length).toBe(2); + expect(crash_resolved).toContain("CpuFastSet"); + expect(crash_resolved).toContain("CPU_FAST_SET_SRC_FIXED"); + }); + + test("Errors", () => { + const symbols = registry.get(errorsh)?.symbols; + if (!symbols) { + throw new Error(`Symbol not found for: ${errorsh}`); + } + const xbox = symbols.get("xbox") as Symbol; + const xbox_invocations = invocationResolver.getInvocationsForSymbol(xbox); + const xbox_resolved = Array.from(xbox_invocations.resolved.keys()); + expect(xbox_resolved.length).toBe(1); + expect(xbox_resolved).toContain("#NAPI_UNNAMED_ENUM_0"); + }); +}); diff --git a/packages/cli/src/languagePlugins/c/invocationResolver/index.ts b/packages/cli/src/languagePlugins/c/invocationResolver/index.ts new file mode 100644 index 00000000..3cfb63e8 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/invocationResolver/index.ts @@ -0,0 +1,167 @@ +import type { Invocations } from "./types.ts"; +import { C_INVOCATION_QUERY, C_MACRO_CONTENT_QUERY } from "./queries.ts"; +import type { CIncludeResolver } from "../includeResolver/index.ts"; +import { + type CFile, + DataType, + EnumMember, + FunctionDefinition, + FunctionSignature, + type Symbol, +} from "../symbolRegistry/types.ts"; +import type Parser from "npm:tree-sitter"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; +import type { IncludedSymbol } from "../includeResolver/types.ts"; + +export class CInvocationResolver { + includeResolver: CIncludeResolver; + + constructor(includeResolver: CIncludeResolver) { + this.includeResolver = includeResolver; + } + + getInvocationsForNode( + node: Parser.SyntaxNode, + filepath: string, + symbolname: string | undefined = undefined, + ): Invocations { + const availableSymbols = this.includeResolver + .getInclusions() + .get(filepath)?.symbols; + const currentfile = this.includeResolver.symbolRegistry.get(filepath)!; + const localSymbols = currentfile.symbols; + const unresolved = new Set(); + const resolved = new Map(); + const captures = C_INVOCATION_QUERY.captures(node); + for (const capture of captures) { + const name = capture.node.text; + // if the symbol name is the same as the one we are looking at, skip it + if (symbolname && name === symbolname) { + continue; + } + if (availableSymbols && availableSymbols.has(name)) { + const availableSymbol = availableSymbols.get(name); + if (!availableSymbol) { + unresolved.add(name); + continue; + } + resolved.set(name, availableSymbol); + } else if (localSymbols && localSymbols.has(name)) { + const localSymbol = localSymbols.get(name); + if (!localSymbol) { + unresolved.add(name); + continue; + } + resolved.set(name, { + includefile: currentfile, + symbol: localSymbol, + }); + } else { + unresolved.add(name); + } + } + // Check for macro invocations + // The logic of a macro is set in a single (preproc_arg) node. + // If we parse that node's text, we can find the invocations. + const macroCaptures = C_MACRO_CONTENT_QUERY.captures(node); + for (const capture of macroCaptures) { + const contentNode = cParser.parse(capture.node.text).rootNode; + const contentInvocations = this.getInvocationsForNode( + contentNode, + filepath, + symbolname, + ); + for (const [key, value] of contentInvocations.resolved) { + if (!resolved.has(key)) { + resolved.set(key, value); + } + } + for (const value of contentInvocations.unresolved) { + unresolved.add(value); + } + } + return { + resolved, + unresolved, + }; + } + + #getSymbolFile(symbol: Symbol): CFile { + const file = this.includeResolver.symbolRegistry.get( + symbol.declaration.filepath, + ); + if (!file) { + throw Error(`File ${symbol.declaration.filepath} not found!`); + // Exclamation point because that's really out of the ordinary + } + return file; + } + + getInvocationsForSymbol(symbol: Symbol) { + const filepath = symbol.declaration.filepath; + const node = symbol.declaration.node; + const name = symbol.name; + const invocations = this.getInvocationsForNode(node, filepath, name); + const resolved = invocations.resolved; + if (symbol instanceof FunctionSignature && symbol.definition) { + resolved.set(symbol.name, { + includefile: this.#getSymbolFile(symbol.definition), + symbol: symbol.definition, + }); + } + if (symbol instanceof FunctionDefinition && symbol.signature) { + resolved.set(symbol.name, { + includefile: this.#getSymbolFile(symbol.signature), + symbol: symbol.signature, + }); + } + if (symbol instanceof DataType) { + const typedefs = symbol.typedefs; + for (const [key, value] of typedefs) { + resolved.set(key, { + includefile: this.#getSymbolFile(value), + symbol: value, + }); + } + } + // Replace enum members with their parent enum + for (const [key, value] of resolved) { + if (value.symbol instanceof EnumMember) { + const parent = value.symbol.parent; + if (!resolved.has(parent.name) && parent.name !== symbol.name) { + resolved.set(parent.name, { + includefile: value.includefile, + symbol: parent, + }); + } + resolved.delete(key); + } + } + return { + resolved: invocations.resolved, + unresolved: invocations.unresolved, + }; + } + + getInvocationsForFile(filepath: string): Invocations { + let symbols = this.includeResolver.symbolRegistry.get(filepath)?.symbols; + if (!symbols) { + symbols = new Map(); + } + let unresolved = new Set(); + const resolved = new Map(); + for (const symbol of symbols.values()) { + const invocations = this.getInvocationsForSymbol(symbol); + unresolved = new Set([...unresolved, ...invocations.unresolved]); + for (const [key, value] of invocations.resolved) { + if (!resolved.has(key)) { + resolved.set(key, value); + } + } + } + return { + resolved, + unresolved, + }; + } +} diff --git a/packages/cli/src/languagePlugins/c/invocationResolver/queries.ts b/packages/cli/src/languagePlugins/c/invocationResolver/queries.ts new file mode 100644 index 00000000..ad846d4b --- /dev/null +++ b/packages/cli/src/languagePlugins/c/invocationResolver/queries.ts @@ -0,0 +1,17 @@ +import Parser from "npm:tree-sitter"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; + +export const C_INVOCATION_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (type_identifier) @type + (identifier) @id + `, // The nuclear option, but unlike C# we can afford to do it. +); + +export const C_MACRO_CONTENT_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (preproc_arg) @macro + `, +); diff --git a/packages/cli/src/languagePlugins/c/invocationResolver/types.ts b/packages/cli/src/languagePlugins/c/invocationResolver/types.ts new file mode 100644 index 00000000..a463561d --- /dev/null +++ b/packages/cli/src/languagePlugins/c/invocationResolver/types.ts @@ -0,0 +1,9 @@ +import type { IncludedSymbol } from "../includeResolver/types.ts"; + +/** Interface representing the invoked symbols in a file */ +export interface Invocations { + /** Symbols that are part of the project */ + resolved: Map; + /** Symbols that are not part of the project or local variables */ + unresolved: Set; +} diff --git a/packages/cli/src/languagePlugins/c/metrics/index.test.ts b/packages/cli/src/languagePlugins/c/metrics/index.test.ts new file mode 100644 index 00000000..5843838c --- /dev/null +++ b/packages/cli/src/languagePlugins/c/metrics/index.test.ts @@ -0,0 +1,46 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { CMetricsAnalyzer } from "./index.ts"; +import { cFilesFolder, getCFilesMap } from "../testFiles/index.ts"; +import { join } from "@std/path"; + +describe("CMetricsAnalyzer", () => { + const analyzer = new CMetricsAnalyzer(); + const files = getCFilesMap(); + + const analyzeFile = (filePath: string) => { + const absolutePath = join(cFilesFolder, filePath); + const file = files.get(absolutePath); + if (!file) { + throw new Error(`File not found: ${absolutePath}`); + } + return analyzer.analyzeNode(file.rootNode); + }; + + test("burgers.c", () => { + const metrics = analyzeFile("burgers.c"); + expect(metrics.characterCount >= 1485).toBe(true); + expect(metrics.codeCharacterCount < 1485).toBe(true); + expect(metrics.linesCount >= 53).toBe(true); + expect(metrics.codeLinesCount < 53).toBe(true); + expect(metrics.cyclomaticComplexity >= 6).toBe(true); + }); + + test("burgers.h", () => { + const metrics = analyzeFile("burgers.h"); + expect(metrics.characterCount >= 1267).toBe(true); + expect(metrics.codeCharacterCount < 1267).toBe(true); + expect(metrics.linesCount >= 71).toBe(true); + expect(metrics.codeLinesCount < 71).toBe(true); + expect(metrics.cyclomaticComplexity).toBe(1); + }); + + test("crashcases.h", () => { + const metrics = analyzeFile("crashcases.h"); + expect(metrics.characterCount >= 956).toBe(true); + expect(metrics.codeCharacterCount < 956).toBe(true); + expect(metrics.linesCount >= 33).toBe(true); + expect(metrics.codeLinesCount < 33).toBe(true); + expect(metrics.cyclomaticComplexity).toBe(0); + }); +}); diff --git a/packages/cli/src/languagePlugins/c/metrics/index.ts b/packages/cli/src/languagePlugins/c/metrics/index.ts new file mode 100644 index 00000000..c6b96ec3 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/metrics/index.ts @@ -0,0 +1,164 @@ +import { C_COMMENT_QUERY, C_COMPLEXITY_QUERY } from "./queries.ts"; +import type { CComplexityMetrics, CodeCounts, CommentSpan } from "./types.ts"; +import type Parser from "npm:tree-sitter"; + +export class CMetricsAnalyzer { + /** + * Calculates metrics for a C symbol. + * @param node - The syntax node to analyze. + * @returns An object containing the complexity metrics. + */ + public analyzeNode(node: Parser.SyntaxNode): CComplexityMetrics { + if (node.type === "preproc_function_def") { + const value = node.childForFieldName("value"); + if (value) { + node = value; + } + } + const complexityCount = this.getComplexityCount(node); + const linesCount = node.endPosition.row - node.startPosition.row + 1; + const codeCounts = this.getCodeCounts(node); + const codeLinesCount = codeCounts.lines; + const characterCount = node.endIndex - node.startIndex; + const codeCharacterCount = codeCounts.characters; + + return { + cyclomaticComplexity: complexityCount, + linesCount, + codeLinesCount, + characterCount, + codeCharacterCount, + }; + } + + private getComplexityCount(node: Parser.SyntaxNode): number { + const complexityMatches = C_COMPLEXITY_QUERY.captures(node); + return complexityMatches.length; + } + + /** + * Finds comments in the given node and returns their spans. + * @param node - The AST node to analyze + * @returns An object containing pure comment lines and comment spans + */ + private findComments( + node: Parser.SyntaxNode, + lines: string[], + ): { + pureCommentLines: Set; + commentSpans: CommentSpan[]; + } { + const pureCommentLines = new Set(); + const commentSpans: CommentSpan[] = []; + + const commentCaptures = C_COMMENT_QUERY.captures(node); + + for (const capture of commentCaptures) { + const commentNode = capture.node; + + // Record the comment span for character counting + commentSpans.push({ + start: { + row: commentNode.startPosition.row, + column: commentNode.startPosition.column, + }, + end: { + row: commentNode.endPosition.row, + column: commentNode.endPosition.column, + }, + }); + + // Check if the comment starts at the beginning of the line (ignoring whitespace) + const lineIdx = commentNode.startPosition.row - node.startPosition.row; + if (lineIdx >= 0 && lineIdx < lines.length) { + const lineText = lines[lineIdx]; + const textBeforeComment = lineText.substring( + 0, + commentNode.startPosition.column, + ); + + // If there's only whitespace before the comment, it's a pure comment line + if (textBeforeComment.trim().length === 0) { + for ( + let line = commentNode.startPosition.row; + line <= commentNode.endPosition.row; + line++ + ) { + pureCommentLines.add(line); + } + } + } + } + + return { pureCommentLines, commentSpans }; + } + + /** + * Finds all empty lines in a node + * + * @param node The syntax node to analyze + * @param lines The lines of text in the node + * @returns Set of line numbers that are empty + */ + private findEmptyLines( + node: Parser.SyntaxNode, + lines: string[], + ): Set { + const emptyLines = new Set(); + for (let i = 0; i < lines.length; i++) { + const lineIndex = node.startPosition.row + i; + if (lines[i].trim().length === 0) { + emptyLines.add(lineIndex); + } + } + + return emptyLines; + } + + private getCodeCounts(node: Parser.SyntaxNode): CodeCounts { + const lines = node.text.split(/\r?\n/); + const linesCount = lines.length; + // Find comments and their spans + const { pureCommentLines, commentSpans } = this.findComments(node, lines); + + // Find empty lines + const emptyLines = this.findEmptyLines(node, lines); + + // Calculate code lines + const nonCodeLines = new Set([...pureCommentLines, ...emptyLines]); + const codeLinesCount = linesCount - nonCodeLines.size; + + let codeCharCount = 0; + + // Process each line individually + for (let i = 0; i < lines.length; i++) { + const lineIndex = node.startPosition.row + i; + const line = lines[i]; + + // Skip empty lines and pure comment lines + if (emptyLines.has(lineIndex) || pureCommentLines.has(lineIndex)) { + continue; + } + + // Process line for code characters + let lineText = line; + + // Remove comment content from the line if present + for (const span of commentSpans) { + if (span.start.row === lineIndex) { + // Comment starts on this line + lineText = lineText.substring(0, span.start.column); + } + } + + // Count normalized code characters (trim excessive whitespace) + const normalizedText = lineText.trim().replace(/\s+/g, " "); + codeCharCount += normalizedText.length; + } + + return { + lines: codeLinesCount, + characters: codeCharCount, + }; + } +} diff --git a/packages/cli/src/languagePlugins/c/metrics/queries.ts b/packages/cli/src/languagePlugins/c/metrics/queries.ts new file mode 100644 index 00000000..dc16b7bc --- /dev/null +++ b/packages/cli/src/languagePlugins/c/metrics/queries.ts @@ -0,0 +1,23 @@ +import Parser from "npm:tree-sitter"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; + +// Tree-sitter query to find complexity-related nodes +export const C_COMPLEXITY_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (if_statement) @complexity + (while_statement) @complexity + (for_statement) @complexity + (do_statement) @complexity + (case_statement) @complexity + (conditional_expression) @complexity + (preproc_ifdef) @complexity + `, +); + +export const C_COMMENT_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (comment) @comment + `, +); diff --git a/packages/cli/src/languagePlugins/c/metrics/types.ts b/packages/cli/src/languagePlugins/c/metrics/types.ts new file mode 100644 index 00000000..e0b9bb57 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/metrics/types.ts @@ -0,0 +1,30 @@ +/** + * Interface for code volumes + */ +export interface CodeCounts { + /** Number of lines of code */ + lines: number; + /** Number of characters of code */ + characters: number; +} + +export interface CommentSpan { + start: { row: number; column: number }; + end: { row: number; column: number }; +} + +/** + * Represents complexity metrics for a C symbol + */ +export interface CComplexityMetrics { + /** Cyclomatic complexity (McCabe complexity) */ + cyclomaticComplexity: number; + /** Code lines (not including whitespace or comments) */ + codeLinesCount: number; + /** Total lines (including whitespace and comments) */ + linesCount: number; + /** Characters of actual code (excluding comments and excessive whitespace) */ + codeCharacterCount: number; + /** Total characters in the entire symbol */ + characterCount: number; +} diff --git a/packages/cli/src/languagePlugins/c/symbolRegistry/index.test.ts b/packages/cli/src/languagePlugins/c/symbolRegistry/index.test.ts new file mode 100644 index 00000000..4920db39 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/symbolRegistry/index.test.ts @@ -0,0 +1,172 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { cFilesFolder, getCFilesMap } from "../testFiles/index.ts"; +import { CSymbolRegistry } from "./index.ts"; +import { + DataType, + FunctionDefinition, + FunctionSignature, + Typedef, + Variable, +} from "./types.ts"; +import { join } from "@std/path"; + +describe("CSymbolRegistry", () => { + const cFilesMap = getCFilesMap(); + const registry = new CSymbolRegistry(cFilesMap).getRegistry(); + const burgersh = join(cFilesFolder, "burgers.h"); + const burgersc = join(cFilesFolder, "burgers.c"); + const oldmanh = join(cFilesFolder, "oldman.h"); + const hsymbols = registry.get(burgersh); + if (!hsymbols) { + throw new Error(`File not found: ${burgersh}`); + } + const csymbols = registry.get(burgersc); + if (!csymbols) { + throw new Error(`File not found: ${burgersc}`); + } + + test("registers symbols for burgers.h", () => { + expect(hsymbols.type).toBe(".h"); + expect(hsymbols.symbols.size).toBe(32); + }); + + test("registers datatypes for burgers.h", () => { + expect(hsymbols.symbols.get("Burger")).toBeDefined(); + expect(hsymbols.symbols.get("Sauce")).toBeDefined(); + expect(hsymbols.symbols.get("Condiment")).toBeDefined(); + expect(hsymbols.symbols.get("ClassicSauces")).toBeDefined(); + expect(hsymbols.symbols.get("Drink_t")).toBeDefined(); + expect(hsymbols.symbols.get("Burger")).toBeInstanceOf(DataType); + expect(hsymbols.symbols.get("Sauce")).toBeInstanceOf(DataType); + expect(hsymbols.symbols.get("Condiment")).toBeInstanceOf(DataType); + expect(hsymbols.symbols.get("ClassicSauces")).toBeInstanceOf(DataType); + expect(hsymbols.symbols.get("Drink_t")).toBeInstanceOf(DataType); + }); + + test("registers enum members for burgers.h", () => { + expect(hsymbols.symbols.get("NONE")).toBeDefined(); + expect(hsymbols.symbols.get("SALAD")).toBeDefined(); + expect(hsymbols.symbols.get("TOMATO")).toBeDefined(); + expect(hsymbols.symbols.get("ONION")).toBeDefined(); + expect(hsymbols.symbols.get("CHEESE")).toBeDefined(); + expect(hsymbols.symbols.get("PICKLE")).toBeDefined(); + expect(hsymbols.symbols.get("KETCHUP")).toBeDefined(); + expect(hsymbols.symbols.get("MUSTARD")).toBeDefined(); + expect(hsymbols.symbols.get("BBQ")).toBeDefined(); + expect(hsymbols.symbols.get("MAYO")).toBeDefined(); + expect(hsymbols.symbols.get("SPICY")).toBeDefined(); + expect(hsymbols.symbols.get("COKE")).toBeDefined(); + expect(hsymbols.symbols.get("ICED_TEA")).toBeDefined(); + expect(hsymbols.symbols.get("LEMONADE")).toBeDefined(); + expect(hsymbols.symbols.get("WATER")).toBeDefined(); + expect(hsymbols.symbols.get("COFFEE")).toBeDefined(); + }); + + test("registers typedefs for burgers.h", () => { + const drink = hsymbols.symbols.get("Drink"); + const fries = hsymbols.symbols.get("Fries"); + expect(drink).toBeDefined(); + expect(fries).toBeDefined(); + expect(drink).toBeInstanceOf(Typedef); + expect(fries).toBeInstanceOf(Typedef); + expect((drink as Typedef).datatype).toBeDefined(); + expect((fries as Typedef).datatype).not.toBeDefined(); + expect((drink as Typedef).datatype).toBeInstanceOf(DataType); + expect((drink as Typedef).datatype?.name).toBe("Drink_t"); + }); + + test("registers functions for burgers.h", () => { + const max = hsymbols.symbols.get("MAX"); + const create = hsymbols.symbols.get("create_burger"); + const destroy = hsymbols.symbols.get("destroy_burger"); + const get = hsymbols.symbols.get("get_burger_by_id"); + const cheapest = hsymbols.symbols.get("get_cheapest_burger"); + expect(max).toBeDefined(); + expect(create).toBeDefined(); + expect(destroy).toBeDefined(); + expect(get).toBeDefined(); + expect(cheapest).toBeDefined(); + expect(max).toBeInstanceOf(FunctionDefinition); + expect(create).toBeInstanceOf(FunctionSignature); + expect(destroy).toBeInstanceOf(FunctionSignature); + expect(get).toBeInstanceOf(FunctionSignature); + expect(cheapest).toBeInstanceOf(FunctionSignature); + expect((max as FunctionDefinition).isMacro).toBe(true); + expect((create as FunctionSignature).isMacro).toBe(false); + expect((destroy as FunctionSignature).isMacro).toBe(false); + expect((get as FunctionSignature).isMacro).toBe(false); + expect((cheapest as FunctionSignature).isMacro).toBe(false); + expect((max as FunctionDefinition).declaration.node.type).toBe( + "preproc_function_def", + ); + expect((create as FunctionSignature).definition).not.toBeDefined(); + expect((destroy as FunctionSignature).definition).not.toBeDefined(); + expect((get as FunctionSignature).definition).not.toBeDefined(); + expect((cheapest as FunctionSignature).definition).not.toBeDefined(); + }); + + test("registers variables for burgers.h", () => { + const burgers_count = hsymbols.symbols.get("burger_count"); + const max_burgers = hsymbols.symbols.get("MAX_BURGERS"); + const burgers_h = hsymbols.symbols.get("BURGERS_H"); + expect(burgers_h).toBeDefined(); + expect(max_burgers).toBeDefined(); + expect(burgers_count).toBeDefined(); + expect(burgers_h).toBeInstanceOf(Variable); + expect(max_burgers).toBeInstanceOf(Variable); + expect(burgers_count).toBeInstanceOf(Variable); + expect((burgers_h as Variable).isMacro).toBe(true); + expect((max_burgers as Variable).isMacro).toBe(true); + expect((burgers_count as Variable).isMacro).toBe(false); + }); + + test("registers symbols for burgers.c", () => { + expect(csymbols.type).toBe(".c"); + expect(csymbols.symbols.size).toBe(5); + }); + + test("registers variables for burgers.c", () => { + const burgers = csymbols.symbols.get("burgers"); + expect(burgers).toBeDefined(); + expect(burgers).toBeInstanceOf(Variable); + expect((burgers as Variable).isMacro).toBe(false); + }); + + test("registers main", () => { + const main = join(cFilesFolder, "main.c"); + const mainsymbols = registry.get(main); + if (!mainsymbols) { + throw new Error(`File not found: ${main}`); + } + expect(mainsymbols.type).toBe(".c"); + expect(mainsymbols.symbols.size).toBe(1); + const mainfunc = mainsymbols.symbols.get("main"); + expect(mainfunc).toBeDefined(); + expect(mainfunc).toBeInstanceOf(FunctionDefinition); + expect((mainfunc as FunctionDefinition).isMacro).toBe(false); + expect((mainfunc as FunctionDefinition).declaration.node.type).toBe( + "function_definition", + ); + expect((mainfunc as FunctionDefinition).declaration.filepath).toBe(main); + expect((mainfunc as FunctionDefinition).declaration.node.type).toBe( + "function_definition", + ); + }); + + test("prioritizes typedefs", () => { + const oldmanSymbols = registry.get(oldmanh); + if (!oldmanSymbols) { + throw new Error(`File not found: ${oldmanh}`); + } + expect(oldmanSymbols.type).toBe(".h"); + expect(oldmanSymbols.symbols.size).toBe(14); + const oldman = oldmanSymbols.symbols.get("OldMan"); + expect(oldman).toBeDefined(); + expect(oldman).toBeInstanceOf(Typedef); + expect((oldman as Typedef).datatype).not.toBeDefined(); + expect((oldman as Typedef).name).toBe("OldMan"); + expect((oldman as Typedef).declaration.node.type).toBe("type_definition"); + expect((oldman as Typedef).declaration.filepath).toBe(oldmanh); + }); +}); diff --git a/packages/cli/src/languagePlugins/c/symbolRegistry/index.ts b/packages/cli/src/languagePlugins/c/symbolRegistry/index.ts new file mode 100644 index 00000000..596f9f4b --- /dev/null +++ b/packages/cli/src/languagePlugins/c/symbolRegistry/index.ts @@ -0,0 +1,183 @@ +import type { ExportedSymbol } from "../headerResolver/types.ts"; +import { CHeaderResolver } from "../headerResolver/index.ts"; +import { + CFile, + DataType, + Enum, + FunctionDefinition, + FunctionSignature, + type Symbol, + Typedef, + Variable, +} from "./types.ts"; +import { C_TYPEDEF_TYPE_QUERY } from "./queries.ts"; +import type Parser from "npm:tree-sitter"; + +export class CSymbolRegistry { + headerResolver: CHeaderResolver; + files: Map; + #registry?: Map; + + constructor( + files: Map, + ) { + this.headerResolver = new CHeaderResolver(); + this.files = files; + } + + /** + * Converts an ExportedSymbol to a Symbol. + * @param es The ExportedSymbol to convert. + * @returns The converted Symbol. + */ + #convertSymbol(es: ExportedSymbol): Symbol { + if (["struct", "union"].includes(es.type)) { + return new DataType( + es.name, + es, + ); + } + if (es.type === "enum") { + return new Enum( + es.name, + es, + ); + } + if (es.type === "typedef") { + return new Typedef( + es.name, + es, + ); + } + if (es.type === "function_definition" || es.type === "macro_function") { + return new FunctionDefinition( + es.name, + es, + es.node.type === "preproc_function_def", + ); + } + if (es.type === "function_signature") { + return new FunctionSignature( + es.name, + es, + es.node.type === "preproc_function_def", + ); + } + if (es.type === "variable" || es.type === "macro_constant") { + return new Variable( + es.name, + es, + es.node.type === "preproc_def", + ); + } + throw new Error(`Could not find symbol type for ${es.name} (${es.type})`); + } + + /** + * Builds the symbol registry by iterating over all header and source files. + * It resolves the symbols in the header files and associates them with their + * corresponding source files. + */ + #buildRegistry() { + this.#registry = new Map(); + // Iterate over all header files and build the registry + const headerFiles = Array.from(this.files.values()).filter((file) => + file.path.endsWith(".h") + ); + for (const file of headerFiles) { + const exportedSymbols = this.headerResolver.resolveSymbols(file); + const header = new CFile(file, ".h"); + for (const es of exportedSymbols) { + const symbol = this.#convertSymbol(es); + if (symbol instanceof DataType) { + // Typedefs get priority over structs if they both have the same name + // That is because if they both have the same name, that means that the typedef + // also includes the struct's definition + if (!header.symbols.has(symbol.name)) { + header.symbols.set(symbol.name, symbol); + } + } else { + header.symbols.set(symbol.name, symbol); + } + if (symbol instanceof Enum) { + for (const [, enumMember] of symbol.members) { + header.symbols.set(enumMember.name, enumMember); + } + } + } + // Add the header file to the registry + this.#registry.set(file.path, header); + } + + // Iterate over all source files to find function definitions. + const sourceFiles = Array.from(this.files.values()).filter((file) => + file.path.endsWith(".c") + ); + for (const file of sourceFiles) { + // We still need to resolve the symbols in the source files + // because they can depend on eachother. + // However they are not global. + const exportedSymbols = this.headerResolver.resolveSymbols(file); + const source = new CFile(file, ".c"); + for (const es of exportedSymbols) { + const symbol = this.#convertSymbol(es); + if (symbol instanceof DataType && !source.symbols.has(symbol.name)) { + // Typedefs get priority over structs if they both have the same name + // That is because if they both have the same name, that means that the typedef + // also includes the struct's definition + source.symbols.set(symbol.name, symbol); + } else { + source.symbols.set(symbol.name, symbol); + } + if (symbol instanceof Enum) { + for (const [name, enumMember] of symbol.members) { + source.symbols.set(name, enumMember); + } + } + } + // Add the source file to the registry + this.#registry.set(file.path, source); + } + + // Associate typedefs with their corresponding data types + for (const [, header] of this.#registry.entries()) { + for ( + const symbol of header.symbols + .values() + .filter((s) => s instanceof Typedef) + .map((s) => s as Typedef) + ) { + const typedefNode = symbol.declaration.node; + const typeCaptures = C_TYPEDEF_TYPE_QUERY.captures(typedefNode); + if (typeCaptures.length > 0) { + const typeCapture = typeCaptures[0]; + const typeNode = typeCapture.node; + const typeName = typeNode.text; + for (const [, header] of this.#registry.entries()) { + if (header.symbols.has(typeName)) { + const dataType = header.symbols.get(typeName); + if (dataType instanceof DataType) { + symbol.datatype = dataType; + dataType.typedefs.set(symbol.name, symbol); + } + } + } + } + } + } + } + + /** + * Returns the symbol registry. If the registry has not been built yet, it builds it first. + * @returns The symbol registry. + */ + getRegistry(): Map { + if (!this.#registry) { + this.#buildRegistry(); + } + if (!this.#registry) { + throw new Error("Couldn't build registry for project"); + } + return this.#registry; + } +} diff --git a/packages/cli/src/languagePlugins/c/symbolRegistry/queries.ts b/packages/cli/src/languagePlugins/c/symbolRegistry/queries.ts new file mode 100644 index 00000000..1bb048a2 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/symbolRegistry/queries.ts @@ -0,0 +1,22 @@ +import Parser from "npm:tree-sitter"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; + +export const C_FUNCTION_DEF_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (function_definition) @fdef + `, +); + +export const C_TYPEDEF_TYPE_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (type_definition + type: [ + (type_identifier) @name + (struct_specifier name: (type_identifier) @name) + (enum_specifier name: (type_identifier) @name) + (union_specifier name: (type_identifier) @name) + ]) + `, +); diff --git a/packages/cli/src/languagePlugins/c/symbolRegistry/types.ts b/packages/cli/src/languagePlugins/c/symbolRegistry/types.ts new file mode 100644 index 00000000..92165d4a --- /dev/null +++ b/packages/cli/src/languagePlugins/c/symbolRegistry/types.ts @@ -0,0 +1,138 @@ +import { + C_VARIABLE_TYPE, + type ExportedSymbol, +} from "../headerResolver/types.ts"; +import type Parser from "npm:tree-sitter"; + +/** Interface representing a C symbol */ +export class Symbol { + /** The name of the symbol */ + name: string; + /** The corresponding ExportedSymbol object */ + declaration: ExportedSymbol; + constructor(name: string, declaration: ExportedSymbol) { + this.name = name; + this.declaration = declaration; + } +} + +/** Interface representing a C function signature */ +export class FunctionSignature extends Symbol { + /** The definition of the function in a source file */ + definition?: FunctionDefinition; + /** Whether the function is a macro or not */ + isMacro: boolean; + constructor( + name: string, + declaration: ExportedSymbol, + isMacro: boolean, + ) { + super(name, declaration); + this.isMacro = isMacro; + this.definition = undefined; + } +} + +/** Interface representing a C function definition */ +export class FunctionDefinition extends Symbol { + /** The declaration of the function in a header file */ + signature?: FunctionSignature; + /** Whether the function is a macro or not */ + isMacro: boolean; + constructor(name: string, declaration: ExportedSymbol, isMacro: boolean) { + super(name, declaration); + this.isMacro = isMacro; + this.signature = undefined; + } +} + +/** Interface representing a C variable */ +export class DataType extends Symbol { + typedefs: Map; + constructor(name: string, declaration: ExportedSymbol) { + super(name, declaration); + this.typedefs = new Map(); + } +} + +/** Interface representing a C typedef */ +export class Typedef extends Symbol { + datatype?: DataType; +} + +export class Variable extends Symbol { + /** Whether the variable is a macro or not */ + isMacro: boolean; + constructor(name: string, declaration: ExportedSymbol, isMacro: boolean) { + super(name, declaration); + this.isMacro = isMacro; + } +} + +export class EnumMember extends Symbol { + parent: Enum; + constructor(name: string, declaration: ExportedSymbol, parent: Enum) { + super(name, declaration); + this.parent = parent; + } +} + +export class Enum extends DataType { + members: Map; + constructor(name: string, declaration: ExportedSymbol) { + super(name, declaration); + const enumerators = declaration.node.childForFieldName("body"); + this.members = new Map(); + if (enumerators) { + for ( + const enumerator of enumerators.children.filter((c) => + c.type === "enumerator" + ) + ) { + const idNode = enumerator.childForFieldName("name"); + if (!idNode) { + continue; + } + const member = new EnumMember( + idNode.text, + { + name: idNode.text, + type: C_VARIABLE_TYPE, + filepath: declaration.filepath, + specifiers: [], + qualifiers: ["const"], + node: enumerator, + identifierNode: idNode, + }, + this, + ); + this.members.set(enumerator.text, member); + } + } + } +} + +export const C_SOURCE_FILE = ".c"; +export const C_HEADER_FILE = ".h"; +export type CFileType = typeof C_SOURCE_FILE | typeof C_HEADER_FILE; + +/** Interface representing a C file */ +export class CFile { + /** The corresponding header file */ + file: { + path: string; + rootNode: Parser.SyntaxNode; + }; + /** The symbols defined in the header file */ + symbols: Map; + /** The type of the file (i.e. .c or .h) */ + type: CFileType; + constructor( + file: { path: string; rootNode: Parser.SyntaxNode }, + filetype: CFileType, + ) { + this.file = file; + this.type = filetype; + this.symbols = new Map(); + } +} diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/.clangd b/packages/cli/src/languagePlugins/c/testFiles/cFiles/.clangd new file mode 100644 index 00000000..024b5a7c --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/.clangd @@ -0,0 +1,2 @@ +CompileFlags: + Add: [-xc, -std=c99] diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/.napirc b/packages/cli/src/languagePlugins/c/testFiles/cFiles/.napirc new file mode 100644 index 00000000..855d8187 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/.napirc @@ -0,0 +1,42 @@ +{ + "language": "c", + "project": { + "include": [ + "**/*.c", + "**/*.h" + ], + "exclude": [ + ".git/**", + "**/dist/**", + "**/build/**", + "**/bin/**", + "**/obj/**", + "**/packages/**", + "**/.vs/**", + "**/TestResults/**", + "**/*.user", + "**/*.suo", + "**/.nuget/**", + "**/artifacts/**", + "**/packages/**", + "**/util/**", + "**/test/**", + "**/perf/**", + "**/napi_out/**", + "**/rapports/**" + ] + }, + "outDir": "napi_out", + "metrics": { + "file": { + "maxChar": 100000, + "maxLine": 1000, + "maxDep": 10 + }, + "symbol": { + "maxChar": 50000, + "maxLine": 500, + "maxDep": 5 + } + } +} diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/all.h b/packages/cli/src/languagePlugins/c/testFiles/cFiles/all.h new file mode 100644 index 00000000..f4212d20 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/all.h @@ -0,0 +1,2 @@ +#include "burgers.h" +#include "personnel.h" diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/burgers.c b/packages/cli/src/languagePlugins/c/testFiles/cFiles/burgers.c new file mode 100644 index 00000000..306753b9 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/burgers.c @@ -0,0 +1,52 @@ +// TOTAL SYMBOL COUNT : 1 +// TOTAL FUNCTION COUNT : 0 +#include "burgers.h" +#include +#include +#include + +static struct Burger* burgers[MAX_BURGERS]; + +struct Burger* create_burger(char name[50], enum Condiment condiments[5], union Sauce sauce) { + struct Burger* new_burger = malloc(sizeof(struct Burger)); + if (new_burger == NULL) { + return NULL; // Memory allocation failed + } + + new_burger->id = ++burger_count; + strncpy(new_burger->name, name, sizeof(new_burger->name) - 1); + new_burger->name[sizeof(new_burger->name) - 1] = '\0'; // Ensure null-termination + memcpy(new_burger->condiments, condiments, sizeof(new_burger->condiments)); + new_burger->sauce = sauce; + + return new_burger; +} + +void destroy_burger(struct Burger* burger) { + if (burger != NULL) { + free(burger); + } +} + +struct Burger* get_burger_by_id(int id) { + for (int i = 0; i < burger_count; i++) { + if (burgers[i] != NULL && burgers[i]->id == id) { + return burgers[i]; + } + } + return NULL; // Burger not found +} + +struct Burger* get_cheapest_burger() { + struct Burger* cheapest_burger = NULL; + float min_price = __FLT_MAX__; // Initialize to maximum float value + + for (int i = 0; i < burger_count; i++) { + if (burgers[i] != NULL && burgers[i]->price < min_price) { + min_price = burgers[i]->price; + cheapest_burger = burgers[i]; + } + } + + return cheapest_burger; +} diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/burgers.h b/packages/cli/src/languagePlugins/c/testFiles/cFiles/burgers.h new file mode 100644 index 00000000..d909d789 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/burgers.h @@ -0,0 +1,70 @@ +// TOTAL SYMBOL COUNT : 16 +// TOTAL FUNCTION COUNT : 5 +#ifndef BURGERS_H +#define BURGERS_H +#include + +#define MAX_BURGERS 100 +#define MAX(x, y) ((x) > (y) ? (x) : (y)) + +enum Condiment { + NONE = 0, + SALAD = 30, + TOMATO = 40, + ONION = 50, + CHEESE = 60, + PICKLE = 70, +}; + +enum ClassicSauces { + KETCHUP = 0, + MAYO = 1, + MUSTARD = 2, + BBQ = 3, + SPICY = 4, +}; + +union Sauce { + enum ClassicSauces classic_sauce; + char custom_sauce[50]; +}; + +typedef struct { + int id; + union Sauce sauce; + _Bool salted; + float price; +} Fries; + +typedef enum Drink_t { + COKE = 0, + ICED_TEA = 1, + LEMONADE = 2, + COFFEE = 3, + WATER = 4, +} Drink; + +struct Burger { + int id; + char name[50]; + float price; + enum Condiment condiments[5]; + union Sauce sauce; +}; + +const struct Burger classicBurger = { + .id = 1, + .name = "Classic Burger", + .price = 5.99, + .condiments = {SALAD, TOMATO, ONION, CHEESE}, + .sauce = {.classic_sauce = KETCHUP} +}; + +static int burger_count = 0; + +struct Burger* create_burger(char name[50], enum Condiment condiments[5], union Sauce sauce); +void destroy_burger(struct Burger* burger); +struct Burger* get_burger_by_id(int id); +struct Burger* get_cheapest_burger(); + +#endif diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/crashcases.h b/packages/cli/src/languagePlugins/c/testFiles/cFiles/crashcases.h new file mode 100644 index 00000000..20cebac1 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/crashcases.h @@ -0,0 +1,35 @@ +// Code segments taken from the Pokémon Fire Red/Leaf Green decomp project +// https://github.com/pret/pokefirered + +#define CPU_FAST_SET_SRC_FIXED 0x01000000 + +void CpuFastSet(const void *src, void *dest, unsigned int control); + +#define CpuFastFill(value, dest, size) \ +{ \ + vu32 tmp = (vu32)(value); \ + CpuFastSet((void *)&tmp, \ + dest, \ + CPU_FAST_SET_SRC_FIXED | ((size)/(32/8) & 0x1FFFFF)); \ +} + +struct ObjectEvent { + int id; +}; + +struct Sprite { + int x; + int y; +}; + +int PlaceholderFunction(struct ObjectEvent *oe, struct Sprite *s); + +int (*const gMovementTypeFuncs_WanderAround[])(struct ObjectEvent *, struct Sprite *) = { + PlaceholderFunction, + PlaceholderFunction, + PlaceholderFunction, + PlaceholderFunction, + PlaceholderFunction, + PlaceholderFunction, + PlaceholderFunction, +}; diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/errors.h b/packages/cli/src/languagePlugins/c/testFiles/cFiles/errors.h new file mode 100644 index 00000000..97e15ca9 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/errors.h @@ -0,0 +1,20 @@ +#include "thisfiledoesnotexist.h" + +// Unnamed enum +enum { + ONE, TWO, THREE, VIVA, L, ALGERIE +}; +int xbox = ONE; + +struct { + int useless; // USELESS? +}; + +// Non-standard typedef +struct Salut_t { + int id; + int waow; +} typedef Salut; + +// Syntax error +int x =??; diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/main.c b/packages/cli/src/languagePlugins/c/testFiles/cFiles/main.c new file mode 100644 index 00000000..f6bb38eb --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/main.c @@ -0,0 +1,21 @@ +#include "all.h" +#include + +int main(void) { + struct Burger* burger = create_burger("Cheeseburger", (enum Condiment[]){SALAD, ONION}, (union Sauce){KETCHUP}); + printf("Burger ID: %d\n", burger->id); + printf("Burger Name: %s\n", burger->name); + printf("Burger Price: %.2f\n", burger->price); + printf("Burger Condiments: "); + for (int i = 0; i < 5; i++) { + if (burger->condiments[i] != 0) { + printf("%d ", burger->condiments[i]); + } + } + printf("\n"); + printf("Burger Sauce: %d\n", burger->sauce.classic_sauce); + + Employee* emp1 = create_employee(1, "Alice", "Manager", IT, 75000.0); + print_employee_details(emp1); + return 0; +} diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/oldman.h b/packages/cli/src/languagePlugins/c/testFiles/cFiles/oldman.h new file mode 100644 index 00000000..cd2c02c6 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/oldman.h @@ -0,0 +1,76 @@ +// Code segments taken from the Pokémon Fire Red/Leaf Green decomp project +// https://github.com/pret/pokefirered +#include + +#define BARD_SONG_LENGTH 14 +#define PLAYER_NAME_LENGTH 7 +#define TRAINER_ID_LENGTH 4 +#define NUM_STORYTELLER_TALES 5 +#define GIDDY_MAX_TALES 6 +#define GIDDY_MAX_QUESTIONS 10 +#define NUM_TRADER_ITEMS 5 + +struct MauvilleManCommon +{ + int id; +}; + +struct MauvilleManBard +{ + /*0x00*/ int id; + /*0x02*/ int songLyrics[BARD_SONG_LENGTH]; + /*0x0E*/ int temporaryLyrics[BARD_SONG_LENGTH]; + /*0x1A*/ int playerName[PLAYER_NAME_LENGTH + 1]; + /*0x22*/ int filler_2DB6[0x3]; + /*0x25*/ int playerTrainerId[TRAINER_ID_LENGTH]; + /*0x29*/ bool hasChangedSong; + /*0x2A*/ int language; +}; /*size = 0x2C*/ + +struct MauvilleManStoryteller +{ + int id; + bool alreadyRecorded; + int filler2[2]; + int gameStatIDs[NUM_STORYTELLER_TALES]; + int trainerNames[NUM_STORYTELLER_TALES][PLAYER_NAME_LENGTH]; + int statValues[NUM_STORYTELLER_TALES][4]; + int language[NUM_STORYTELLER_TALES]; +}; + +struct MauvilleManGiddy +{ + /*0x00*/ int id; + /*0x01*/ int taleCounter; + /*0x02*/ int questionNum; + /*0x04*/ int randomWords[GIDDY_MAX_TALES]; + /*0x18*/ int questionList[GIDDY_MAX_QUESTIONS]; + /*0x20*/ int language; +}; /*size = 0x2C*/ + +struct MauvilleManHipster +{ + int id; + bool alreadySpoken; + int language; +}; + +struct MauvilleOldManTrader +{ + int id; + int decorIds[NUM_TRADER_ITEMS]; + int playerNames[NUM_TRADER_ITEMS][11]; + int alreadyTraded; + int language[NUM_TRADER_ITEMS]; +}; + +typedef union OldMan +{ + struct MauvilleManCommon common; + struct MauvilleManBard bard; + struct MauvilleManGiddy giddy; + struct MauvilleManHipster hipster; + struct MauvilleOldManTrader trader; + struct MauvilleManStoryteller storyteller; + int filler[0x40]; +} OldMan; diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/personnel.c b/packages/cli/src/languagePlugins/c/testFiles/cFiles/personnel.c new file mode 100644 index 00000000..4e8deb9d --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/personnel.c @@ -0,0 +1,85 @@ +#include "personnel.h" +#include +#include +#include + +Employee* create_employee(int id, const char* name, const char* position, enum Department department, float salary) { + if (employee_count >= MAX_EMPLOYEES) { + return NULL; // Maximum employee limit reached + } + + Employee* new_employee = malloc(sizeof(Employee)); + if (new_employee == NULL) { + return NULL; // Memory allocation failed + } + + new_employee->id = id; + strncpy(new_employee->name, name, sizeof(new_employee->name) - 1); + new_employee->name[sizeof(new_employee->name) - 1] = '\0'; // Ensure null-termination + strncpy(new_employee->position, position, sizeof(new_employee->position) - 1); + new_employee->position[sizeof(new_employee->position) - 1] = '\0'; // Ensure null-termination + new_employee->department = department; + new_employee->salary = salary; + + employees[employee_count++] = new_employee; + + return new_employee; +} + +void destroy_employee(Employee* employee) { + if (employee != NULL) { + free(employee); + } +} + +Employee* get_employee_by_id(int id) { + for (int i = 0; i < employee_count; i++) { + if (employees[i] != NULL && employees[i]->id == id) { + return employees[i]; + } + } + return NULL; // Employee not found +} + +Employee* get_highest_paid_employee() { + Employee* highest_paid_employee = NULL; + float max_salary = -1.0f; // Initialize to a negative value + + for (int i = 0; i < employee_count; i++) { + if (employees[i] != NULL && employees[i]->salary > max_salary) { + max_salary = employees[i]->salary; + highest_paid_employee = employees[i]; + } + } + + return highest_paid_employee; +} + +Employee** get_employees_by_department(enum Department department, int* count) { + Employee** department_employees = malloc(MAX_EMPLOYEES * sizeof(Employee*)); + if (department_employees == NULL) { + *count = 0; + return NULL; // Memory allocation failed + } + + *count = 0; + for (int i = 0; i < employee_count; i++) { + if (employees[i] != NULL && employees[i]->department == department) { + department_employees[(*count)++] = employees[i]; + } + } + + return department_employees; +} + +void print_employee_details(const Employee* employee) { + if (employee != NULL) { + printf("ID: %d\n", employee->id); + printf("Name: %s\n", employee->name); + printf("Position: %s\n", employee->position); + printf("Department: %d\n", employee->department); + printf("Salary: %.2f\n", employee->salary); + } else { + printf("Employee not found.\n"); + } +} diff --git a/packages/cli/src/languagePlugins/c/testFiles/cFiles/personnel.h b/packages/cli/src/languagePlugins/c/testFiles/cFiles/personnel.h new file mode 100644 index 00000000..4561662d --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/cFiles/personnel.h @@ -0,0 +1,35 @@ +// TOTAL SYMBOL COUNT : 12 +// TOTAL FUNCTION COUNT : 6 +#ifndef PERSONNEL_H +#define PERSONNEL_H +#include + +#define MAX_EMPLOYEES 100 + +enum Department { + HR = 0, + IT = 1, + SALES = 2, + MARKETING = 3, + FINANCE = 4, +}; + +typedef struct { + int id; + char name[50]; + char position[50]; + enum Department department; + float salary; +} Employee; + +static int employee_count = 0; +static Employee* employees[MAX_EMPLOYEES]; + +Employee* create_employee(int id, const char* name, const char* position, enum Department department, float salary); +void destroy_employee(Employee* employee); +Employee* get_employee_by_id(int id); +Employee* get_highest_paid_employee(); +Employee** get_employees_by_department(enum Department department, int* count); +void print_employee_details(const Employee* employee); + +#endif diff --git a/packages/cli/src/languagePlugins/c/testFiles/index.ts b/packages/cli/src/languagePlugins/c/testFiles/index.ts new file mode 100644 index 00000000..9b686bcf --- /dev/null +++ b/packages/cli/src/languagePlugins/c/testFiles/index.ts @@ -0,0 +1,76 @@ +import * as fs from "node:fs"; +import { extname, join } from "@std/path"; +import type Parser from "tree-sitter"; +import { cLanguage, cParser } from "../../../helpers/treeSitter/parsers.ts"; + +if (!import.meta.dirname) { + throw new Error("import.meta.dirname is not defined"); +} +export const cFilesFolder = join(import.meta.dirname, "cFiles"); +const cFilesMap = new Map< + string, + { path: string; rootNode: Parser.SyntaxNode } +>(); + +/** + * Recursively finds all C files in the given directory and its subdirectories. + * @param dir - The directory to search in. + */ +function findCFiles(dir: string) { + const files = fs.readdirSync(dir); + files.forEach((file) => { + const fullPath = join(dir, file); + const stat = fs.statSync(fullPath); + if (stat.isDirectory()) { + if ( + !fullPath.includes(".extracted/") && + !fullPath.includes("bin/") && + !fullPath.includes("obj/") + ) { + findCFiles(fullPath); + } + } else if ( + extname(fullPath) === ".c" || + extname(fullPath) === ".h" + ) { + const content = fs.readFileSync(fullPath, "utf8"); + const tree = cParser.parse(content); + cFilesMap.set(fullPath, { path: fullPath, rootNode: tree.rootNode }); + } + }); +} + +export function getCFilesMap(): Map< + string, + { path: string; rootNode: Parser.SyntaxNode } +> { + findCFiles(cFilesFolder); + return cFilesMap; +} + +export function getCFilesContentMap(): Map< + string, + { path: string; content: string } +> { + findCFiles(cFilesFolder); + const contentMap = new Map(); + for (const [filePath, file] of cFilesMap) { + contentMap.set(filePath, { + path: file.path, + content: file.rootNode.text, + }); + } + return contentMap; +} + +export const dummyLocalConfig = { + language: cLanguage, + project: { + include: [], + exclude: [], + }, + outDir: "./dist", + c: { + includedirs: [], + }, +}; diff --git a/packages/cli/src/languagePlugins/c/warnings/index.test.ts b/packages/cli/src/languagePlugins/c/warnings/index.test.ts new file mode 100644 index 00000000..d8aa3cc3 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/warnings/index.test.ts @@ -0,0 +1,28 @@ +import { describe, test } from "@std/testing/bdd"; +import { expect } from "@std/expect"; +import { CWarningManager } from "./index.ts"; +import { cFilesFolder, getCFilesMap } from "../testFiles/index.ts"; +import { join } from "@std/path"; + +describe("CWarningManager", () => { + const files = getCFilesMap(); + const manager = new CWarningManager(files); + const diagnostics = manager.diagnostics; + test("finds all diagnostics", () => { + expect(diagnostics.length).toBe(2); + }); + + test("diagnostics are of correct type", () => { + expect(diagnostics[0].message).toContain("Tree-sitter error"); + expect(diagnostics[1].message).toContain("Unnamed"); + }); + + test("diagnostics are in correct files", () => { + expect(diagnostics[0].filename).toBe( + join(cFilesFolder, "errors.h"), + ); + expect(diagnostics[1].filename).toBe( + join(cFilesFolder, "errors.h"), + ); + }); +}); diff --git a/packages/cli/src/languagePlugins/c/warnings/index.ts b/packages/cli/src/languagePlugins/c/warnings/index.ts new file mode 100644 index 00000000..5fc629ef --- /dev/null +++ b/packages/cli/src/languagePlugins/c/warnings/index.ts @@ -0,0 +1,64 @@ +import { + type ManifestDiagnostics, + TreeSitterError, + UnnamedDatatypeWarning, +} from "./types.ts"; +import type Parser from "npm:tree-sitter"; +import { C_ERROR_QUERY, C_UNNAMED_DATATYPE_QUERY } from "./queries.ts"; + +export class CWarningManager { + diagnostics: ManifestDiagnostics[] = []; + constructor( + files: Map, + ) { + for (const [, { path, rootNode }] of files) { + this.diagnostics.push( + ...this.getDiagnostics(path, rootNode), + ); + } + } + + private getDiagnostics( + path: string, + rootNode: Parser.SyntaxNode, + ): ManifestDiagnostics[] { + const diagnostics: ManifestDiagnostics[] = []; + const errorQuery = C_ERROR_QUERY.captures(rootNode); + const unnamedDatatypeQuery = C_UNNAMED_DATATYPE_QUERY.captures(rootNode); + + for (const capture of errorQuery) { + diagnostics.push(new TreeSitterError(path, capture)); + } + + for (const capture of unnamedDatatypeQuery) { + diagnostics.push(new UnnamedDatatypeWarning(path, capture)); + } + + return diagnostics; + } + + /** + * Print the diagnostics to the console. + * @param count The number of diagnostics to print. If undefined, all diagnostics will be printed. + * @example + * ```ts + * const manager = new CWarningManager(files); + * // Prints up to 5 diagnostics + * manager.printDiagnostics(5); + * // Prints all diagnostics + * manager.printDiagnostics(); + * ``` + */ + public printDiagnostics(count: number | undefined = undefined): void { + if (!count) { + count = this.diagnostics.length; + } + if (count > this.diagnostics.length) { + count = this.diagnostics.length; + } + for (let i = 0; i < count; i++) { + const diagnostic = this.diagnostics[i]; + console.warn(diagnostic.message); + } + } +} diff --git a/packages/cli/src/languagePlugins/c/warnings/queries.ts b/packages/cli/src/languagePlugins/c/warnings/queries.ts new file mode 100644 index 00000000..81ccfdb2 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/warnings/queries.ts @@ -0,0 +1,29 @@ +import Parser from "npm:tree-sitter"; +import { cParser } from "../../../helpers/treeSitter/parsers.ts"; + +export const C_ERROR_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (ERROR) @error +`, +); + +export const C_UNNAMED_DATATYPE_QUERY = new Parser.Query( + cParser.getLanguage(), + ` + (translation_unit + [ + (struct_specifier + !name) @struct + (union_specifier + !name) @union + ]) + (preproc_ifdef + [ + (struct_specifier + !name) @struct + (union_specifier + !name) @union + ]) +`, +); diff --git a/packages/cli/src/languagePlugins/c/warnings/types.ts b/packages/cli/src/languagePlugins/c/warnings/types.ts new file mode 100644 index 00000000..f5a53b98 --- /dev/null +++ b/packages/cli/src/languagePlugins/c/warnings/types.ts @@ -0,0 +1,46 @@ +import type Parser from "npm:tree-sitter"; + +export interface ManifestDiagnostics { + filename: string; + line: number; + column: number; + message: string; +} + +export class TreeSitterError implements ManifestDiagnostics { + filename: string; + line: number; + column: number; + message: string; + + constructor( + filename: string, + capture: Parser.QueryCapture, + ) { + this.filename = filename; + this.line = capture.node.startPosition.row + 1; + this.column = capture.node.startPosition.column + 1; + this.message = + `[WARN] (${filename}:${this.line}:${this.column}) | Tree-sitter error`; + } +} + +export class UnnamedDatatypeWarning implements ManifestDiagnostics { + filename: string; + line: number; + column: number; + datatype: string; + message: string; + + constructor( + filename: string, + capture: Parser.QueryCapture, + ) { + this.filename = filename; + this.line = capture.node.startPosition.row + 1; + this.column = capture.node.startPosition.column + 1; + this.datatype = capture.name; + this.message = + `[WARN] (${filename}:${this.line}:${this.column}) | Unnamed ${this.datatype}`; + } +} diff --git a/packages/cli/src/languagePlugins/python/dependencyResolver/index.test.ts b/packages/cli/src/languagePlugins/python/dependencyResolver/index.test.ts index fdab0b83..d344979e 100644 --- a/packages/cli/src/languagePlugins/python/dependencyResolver/index.test.ts +++ b/packages/cli/src/languagePlugins/python/dependencyResolver/index.test.ts @@ -148,7 +148,7 @@ from original import original_func # First definition with dependency on original def multi_func(): return original_func() - + from another import another_func # Second definition with dependency on another diff --git a/packages/cli/src/languagePlugins/python/exportExtractor/index.test.ts b/packages/cli/src/languagePlugins/python/exportExtractor/index.test.ts index 987a7f17..8fabb023 100644 --- a/packages/cli/src/languagePlugins/python/exportExtractor/index.test.ts +++ b/packages/cli/src/languagePlugins/python/exportExtractor/index.test.ts @@ -185,9 +185,9 @@ describe("PythonExportExtractor", () => { const code = ` def my_function(): pass - + # Some comment in between - + def my_function(a, b): return a + b `; @@ -218,9 +218,9 @@ describe("PythonExportExtractor", () => { class MyClass: def method1(self): pass - + # Some comment in between - + class MyClass: def method2(self): pass @@ -316,17 +316,17 @@ describe("PythonExportExtractor", () => { test("should handle mixed symbol types and multiple modifications", () => { const code = ` x = 10 - + def func(): pass - + x += 5 - + class MyClass: pass - + x = 20 - + def func(a): return a `; @@ -452,7 +452,7 @@ describe("PythonExportExtractor", () => { const code = ` class Person: pass - + person = Person() person.name = "John" person.age = 30 @@ -497,21 +497,21 @@ describe("PythonExportExtractor", () => { const code = ` # Module-level variable counter = 0 - + def increment(): # Local variable with same name as module-level one counter = 0 counter += 1 return counter - + # Module-level modification counter += 10 - + def modify_global(): global counter # This modifies the module-level variable counter += 100 - + # Another module-level variable total = 50 `; @@ -568,30 +568,30 @@ describe("PythonExportExtractor", () => { const code = ` # Module-level variable data = [] - + class DataProcessor: # Class variable with same name data = {} - + def __init__(self): # Instance variable self.data = [] - + def process(self): # Local variable data = [] data.append("processed") return data - + @classmethod def reset(cls): # Modifying class variable cls.data = {} - + # Module-level modification data.append("module") data = data + ["level"] - + # Another module-level variable config = {} `; @@ -642,26 +642,26 @@ describe("PythonExportExtractor", () => { const code = ` # Module-level variable count = 0 - + class Counter: # Class variable count = 0 - + def __init__(self, initial=0): # Instance variable self.count = initial - + def increment(self): # Local method operation count = 0 # Local variable shadowing count += 1 self.count += 1 - + @classmethod def increment_class(cls): # Class variable operation cls.count += 1 - + @staticmethod def process(): # Local function operation @@ -671,28 +671,28 @@ describe("PythonExportExtractor", () => { count *= 2 return count return nested() - + # Another module-level function def operate(): # Local variable count = 5 - + # Nested function def nested_modifier(): nonlocal count count += 5 - + # Double-nested function def deep_nested(): # Local to deep_nested count = 1000 return count - + return deep_nested() - + nested_modifier() return count - + # Module-level modifications count += 1 count *= 2 @@ -759,16 +759,16 @@ describe("PythonExportExtractor", () => { const code = ` import os from celery import Celery - + os.environ.setdefault("DJANGO_SETTINGS_MODULE", "Project.settings") app = Celery("Project") app.config_from_object("django.conf:settings", namespace="CELERY") app.conf.update(app.conf.get("CELERY_CONFIG")) app.autodiscover_tasks() - + app.conf.task_queue_max_priority = 10 app.conf.task_default_priority = 5 - + app.conf.beat_schedule = { "task1": { "task": "module.tasks.task1", @@ -823,7 +823,7 @@ describe("PythonExportExtractor", () => { from flask import Flask from flask_cors import CORS from config import Config - + server = Flask(__name__) server.config.from_object(Config) server.config.update({ @@ -831,14 +831,14 @@ describe("PythonExportExtractor", () => { "SECRET_KEY": "dev-key", }) server.register_blueprint(api_bp, url_prefix="/api") - + CORS(server) - + server.logger.setLevel("INFO") server.config["MAX_CONTENT_LENGTH"] = 16 * 1024 * 1024 - + server.session_interface = CustomSessionInterface() - + server.route_map = { "home": "/", "dashboard": "/dashboard", @@ -849,11 +849,11 @@ describe("PythonExportExtractor", () => { "notifications": "/settings/notifications", } } - + @server.before_request def before_request(): pass - + if __name__ == "__main__": server.run(host="0.0.0.0", port=5000) `; @@ -907,21 +907,21 @@ describe("PythonExportExtractor", () => { data = {} data["key1"] = "value1" data["key2"] = "value2" - + # Nested subscript operations config = {"db": {}} config["db"]["host"] = "localhost" config["db"]["port"] = 5432 - + # Attribute + subscript operations class Storage: def __init__(self): self.items = {} - + storage = Storage() storage.items["item1"] = {"name": "Item 1", "price": 10} storage.items["item2"] = {"name": "Item 2", "price": 20} - + # Mixed operations options = {} options["debug"] = True diff --git a/packages/cli/src/languagePlugins/python/itemResolver/index.test.ts b/packages/cli/src/languagePlugins/python/itemResolver/index.test.ts index bd7b7956..f1a9531f 100644 --- a/packages/cli/src/languagePlugins/python/itemResolver/index.test.ts +++ b/packages/cli/src/languagePlugins/python/itemResolver/index.test.ts @@ -103,7 +103,7 @@ describe("PythonItemResolver", () => { rootNode: pythonParser.parse(` from .level1 import * from .direct import direct_func - + __all__ = ['direct_func', 'level1_func', 'level2_deep_func'] `).rootNode, }, @@ -124,10 +124,10 @@ describe("PythonItemResolver", () => { path: "nested_all_pkg/level1.py", rootNode: pythonParser.parse(` from .level2 import * - + def level1_func(): pass def level1_hidden(): pass - + __all__ = ['level1_func', 'level2_deep_func'] `).rootNode, }, @@ -140,7 +140,7 @@ describe("PythonItemResolver", () => { def level2_func(): pass def level2_deep_func(): pass def level2_hidden(): pass - + __all__ = ['level2_func', 'level2_deep_func'] `).rootNode, }, @@ -238,12 +238,12 @@ describe("PythonItemResolver", () => { path: "precedence.py", rootNode: pythonParser.parse(` # Define a local symbol - def duplicate(): + def duplicate(): return "local" - + # Import a symbol with same name - this should be shadowed from moduleA import CONSTANT as duplicate - + # Wildcard import - should not override existing names from moduleB import * `).rootNode, @@ -256,10 +256,10 @@ describe("PythonItemResolver", () => { rootNode: pythonParser.parse(` # First wildcard import from moduleA import * - + # Second wildcard import from moduleB import * - + # Third explicit import - should override any previous wildcards from moduleA import foo as bar `).rootNode, @@ -323,7 +323,7 @@ describe("PythonItemResolver", () => { path: "usePackage.py", rootNode: pythonParser.parse(` from package import * - + # This should be available from the wildcard import deep_nested_func() `).rootNode, @@ -338,7 +338,7 @@ describe("PythonItemResolver", () => { def public_func(): pass def another_func(): pass def _private_func(): pass - + __all__ = ['public_func'] `).rootNode, }, @@ -380,7 +380,7 @@ describe("PythonItemResolver", () => { path: "importPrivate.py", rootNode: pythonParser.parse(` from privateSymbols import * - + # Should only import public symbols through wildcard `).rootNode, }, @@ -391,7 +391,7 @@ describe("PythonItemResolver", () => { path: "explicitPrivate.py", rootNode: pythonParser.parse(` from privateSymbols import _private_function, PUBLIC_CONSTANT - + # Explicit imports can include private symbols `).rootNode, }, @@ -404,7 +404,7 @@ describe("PythonItemResolver", () => { rootNode: pythonParser.parse(` def regular_func(): pass def _private_func(): pass - + # Explicitly export a private symbol through __all__ __all__ = ['regular_func', '_private_func'] `).rootNode, @@ -416,7 +416,7 @@ describe("PythonItemResolver", () => { path: "importAllOverride.py", rootNode: pythonParser.parse(` from allOverride import * - + # Should import both regular_func and _private_func because they're in __all__ `).rootNode, }, @@ -500,7 +500,7 @@ describe("PythonItemResolver", () => { rootNode: pythonParser.parse(` from multi_wildcard1 import * from multi_wildcard2 import * - + # Should have unique1, unique2, and common (from multi_wildcard1 since it comes first) `).rootNode, }, diff --git a/packages/cli/src/languagePlugins/python/metricAnalyzer/index.test.ts b/packages/cli/src/languagePlugins/python/metricAnalyzer/index.test.ts index 46e20998..9ba8782a 100644 --- a/packages/cli/src/languagePlugins/python/metricAnalyzer/index.test.ts +++ b/packages/cli/src/languagePlugins/python/metricAnalyzer/index.test.ts @@ -131,10 +131,10 @@ def logical_function(a, b, c): class TestClass: def __init__(self, value): self.value = value - + def simple_method(self): return self.value - + def complex_method(self, factor): if factor > 0: return self.value * factor @@ -164,10 +164,10 @@ class TestClass: const code = ` def commented_function(): # This is a comment - + # Another comment value = 42 # Inline comment - + # Final comment return value `; diff --git a/packages/cli/src/languagePlugins/python/symbolExtractor/index.test.ts b/packages/cli/src/languagePlugins/python/symbolExtractor/index.test.ts index 88edb99b..0608f7b7 100644 --- a/packages/cli/src/languagePlugins/python/symbolExtractor/index.test.ts +++ b/packages/cli/src/languagePlugins/python/symbolExtractor/index.test.ts @@ -111,7 +111,7 @@ from utils import Helper class MyClass: def __init__(self): self.helper = Helper() - + def do_something(self): return self.helper.help() @@ -157,7 +157,7 @@ from utils import Helper class MyClass: def __init__(self): self.helper = Helper() - + def do_something(self): return self.helper.help() `.trim(), diff --git a/packages/cli/src/manifest/dependencyManifest/c/index.ts b/packages/cli/src/manifest/dependencyManifest/c/index.ts new file mode 100644 index 00000000..523db48f --- /dev/null +++ b/packages/cli/src/manifest/dependencyManifest/c/index.ts @@ -0,0 +1,125 @@ +import { + type DependencyManifest, + metricCharacterCount, + metricCodeCharacterCount, + metricCodeLineCount, + metricCyclomaticComplexity, + metricDependencyCount, + metricDependentCount, + metricLinesCount, + type SymbolDependencyManifest, + type SymbolType, +} from "@napi/shared"; +import { CDependencyFormatter } from "../../../languagePlugins/c/dependencyFormatting/index.ts"; +import { CMetricsAnalyzer } from "../../../languagePlugins/c/metrics/index.ts"; +import type Parser from "npm:tree-sitter"; +import { cLanguage, cParser } from "../../../helpers/treeSitter/parsers.ts"; +import { CWarningManager } from "../../../languagePlugins/c/warnings/index.ts"; +import type { localConfigSchema } from "../../../config/localConfig.ts"; +import type z from "npm:zod"; + +export function generateCDependencyManifest( + files: Map, + napiConfig: z.infer, +): DependencyManifest { + console.time("generateCDependencyManifest"); + console.info("Processing project..."); + const parsedFiles = new Map< + string, + { path: string; rootNode: Parser.SyntaxNode } + >(); + for (const [filePath, { content: fileContent }] of files) { + try { + const rootNode = cParser.parse(fileContent, undefined, { + bufferSize: fileContent.length + 10, + }).rootNode; + parsedFiles.set(filePath, { path: filePath, rootNode }); + } catch (e) { + console.error(`Failed to parse ${filePath}, skipping`); + console.error(e); + } + } + + const warningManager = new CWarningManager(parsedFiles); + if (warningManager.diagnostics.length > 0) { + console.warn(`⚠️ ${warningManager.diagnostics.length} warnings found:`); + for (const warning of warningManager.diagnostics) { + console.warn(warning.message); + } + } + const includeDirs = napiConfig[cLanguage]?.includedirs ?? []; + const formatter = new CDependencyFormatter(parsedFiles, includeDirs); + const metricsAnalyzer = new CMetricsAnalyzer(); + const manifest: DependencyManifest = {}; + const filecount = parsedFiles.size; + let i = 0; + for (const [, { path }] of parsedFiles) { + console.info(`Processing ${path} (${++i}/${filecount})`); + const fm = formatter.formatFile(path); + const cSyms = fm.symbols; + const symbols: Record = {}; + for (const [symName, symbol] of Object.entries(cSyms)) { + const symType = symbol.type; + const dependencies = symbol.dependencies; + const metrics = metricsAnalyzer.analyzeNode(symbol.node); + symbols[symName] = { + id: symName, + type: symType as SymbolType, + metrics: { + [metricCharacterCount]: metrics.characterCount, + [metricCodeCharacterCount]: metrics.codeCharacterCount, + [metricLinesCount]: metrics.linesCount, + [metricCodeLineCount]: metrics.codeLinesCount, + [metricDependencyCount]: Object.keys(dependencies).length, + [metricDependentCount]: 0, + [metricCyclomaticComplexity]: metrics.cyclomaticComplexity, + }, + dependencies: dependencies, + dependents: {}, + }; + } + const metrics = metricsAnalyzer.analyzeNode(fm.rootNode); + manifest[path] = { + id: fm.id, + filePath: fm.filePath, + language: cLanguage, + metrics: { + [metricCharacterCount]: metrics.characterCount, + [metricCodeCharacterCount]: metrics.codeCharacterCount, + [metricLinesCount]: metrics.linesCount, + [metricCodeLineCount]: metrics.codeLinesCount, + [metricDependencyCount]: Object.keys(fm.dependencies).length, + [metricDependentCount]: 0, + [metricCyclomaticComplexity]: metrics.cyclomaticComplexity, + }, + dependencies: fm.dependencies, + symbols: symbols, + dependents: {}, + }; + } + console.info("Populating dependents..."); + i = 0; + for (const fm of Object.values(manifest)) { + const path = fm.filePath; + console.info(`Populating dependents for ${path} (${++i}/${filecount})`); + for (const symbol of Object.values(fm.symbols)) { + for (const dpncy of Object.values(symbol.dependencies)) { + for (const depsymbol of Object.values(dpncy.symbols)) { + const otherFile = manifest[dpncy.id]; + const otherSymbol = otherFile.symbols[depsymbol]; + if (!otherSymbol.dependents[fm.id]) { + otherSymbol.dependents[fm.id] = { + id: fm.id, + symbols: {}, + }; + } + otherSymbol.dependents[fm.id].symbols[symbol.id] = symbol.id; + fm.metrics[metricDependentCount]++; + symbol.metrics[metricDependentCount]++; + } + } + } + } + console.timeEnd("generateCDependencyManifest"); + return manifest; +} diff --git a/packages/cli/src/manifest/dependencyManifest/index.ts b/packages/cli/src/manifest/dependencyManifest/index.ts index f4781e0a..8bffd11b 100644 --- a/packages/cli/src/manifest/dependencyManifest/index.ts +++ b/packages/cli/src/manifest/dependencyManifest/index.ts @@ -1,5 +1,6 @@ import { generatePythonDependencyManifest } from "./python/index.ts"; import { generateCSharpDependencyManifest } from "./csharp/index.ts"; +import { generateCDependencyManifest } from "./c/index.ts"; import type { localConfigSchema } from "../../config/localConfig.ts"; import type z from "npm:zod"; import type { @@ -7,6 +8,7 @@ import type { SymbolDependencyManifest, } from "@napi/shared"; import { + cLanguage, csharpLanguage, pythonLanguage, } from "../../helpers/treeSitter/parsers.ts"; @@ -20,6 +22,7 @@ const handlerMap: Record< > = { [pythonLanguage]: generatePythonDependencyManifest, [csharpLanguage]: generateCSharpDependencyManifest, + [cLanguage]: generateCDependencyManifest, }; export class UnsupportedLanguageError extends Error { diff --git a/packages/cli/src/symbolExtractor/c/index.ts b/packages/cli/src/symbolExtractor/c/index.ts new file mode 100644 index 00000000..922e8b04 --- /dev/null +++ b/packages/cli/src/symbolExtractor/c/index.ts @@ -0,0 +1,18 @@ +import type { ExtractedFilesMap } from "../types.ts"; +import { CExtractor } from "../../languagePlugins/c/extractor/index.ts"; +import type { DependencyManifest } from "@napi/shared"; +import type { z } from "npm:zod"; +import type { localConfigSchema } from "../../config/localConfig.ts"; + +export function extractCSymbols( + files: Map, + dependencyManifest: DependencyManifest, + symbolsToExtract: Map }>, + _napiConfig: z.infer, +): ExtractedFilesMap { + console.time(`Extracted ${symbolsToExtract.size} symbol(s)`); + const extractor = new CExtractor(files, dependencyManifest, _napiConfig); + const extractedFiles = extractor.extractSymbols(symbolsToExtract); + console.timeEnd(`Extracted ${symbolsToExtract.size} symbol(s)`); + return extractedFiles; +} diff --git a/packages/cli/src/symbolExtractor/index.ts b/packages/cli/src/symbolExtractor/index.ts index 2b1b0d95..a0c401e3 100644 --- a/packages/cli/src/symbolExtractor/index.ts +++ b/packages/cli/src/symbolExtractor/index.ts @@ -1,6 +1,7 @@ import type { ExtractedFilesMap } from "./types.ts"; import { extractPythonSymbols } from "./python/index.ts"; import { extractCSharpSymbols } from "./csharp/index.ts"; +import { extractCSymbols } from "./c/index.ts"; import type { localConfigSchema } from "../config/localConfig.ts"; import type z from "npm:zod"; import type { DependencyManifest } from "@napi/shared"; @@ -16,6 +17,7 @@ const handlerMap: Record< > = { python: extractPythonSymbols, "c-sharp": extractCSharpSymbols, + c: extractCSymbols, }; export class UnsupportedLanguageError extends Error { diff --git a/packages/shared/src/manifest/dependencyManifest/types.ts b/packages/shared/src/manifest/dependencyManifest/types.ts index 8501a69d..c5e15f10 100644 --- a/packages/shared/src/manifest/dependencyManifest/types.ts +++ b/packages/shared/src/manifest/dependencyManifest/types.ts @@ -7,6 +7,12 @@ export const structSymbolType = "struct"; /** Identifies the "enum" instance type. */ export const enumSymbolType = "enum"; +/** Identifies the "union" instance type. */ +export const unionSymbolType = "union"; + +/** Identifies the "typedef" instance type. */ +export const typedefSymbolType = "typedef"; + /** Identifies the "interface" instance type. */ export const interfaceSymbolType = "interface"; @@ -37,6 +43,8 @@ export type SymbolType = | typeof variableSymbolType | typeof structSymbolType | typeof enumSymbolType + | typeof unionSymbolType + | typeof typedefSymbolType | typeof interfaceSymbolType | typeof recordSymbolType | typeof delegateSymbolType; diff --git a/scripts/bump_version.ts b/scripts/bump_version.ts new file mode 100644 index 00000000..4d6064fd --- /dev/null +++ b/scripts/bump_version.ts @@ -0,0 +1,41 @@ +import path from "node:path"; + +const releaseType: string = Deno.args[0]; +if ( + !releaseType || + releaseType !== "patch" && releaseType !== "minor" && releaseType !== "major" +) { + console.error("Invalid release type"); + Deno.exit(1); +} + +const denoJsonPath = path.resolve( + import.meta.dirname as string, + "..", + "deno.json", +); +const denoJson = JSON.parse(await Deno.readTextFile(denoJsonPath)) as { + version: string; +}; + +const [major, minor, patch] = denoJson.version.split(".").map(Number); + +let newVersion: string; + +if (releaseType === "patch") { + newVersion = `${major}.${minor}.${patch + 1}`; +} else if (releaseType === "minor") { + newVersion = `${major}.${minor + 1}.0`; +} else { + newVersion = `${major + 1}.0.0`; +} + +denoJson.version = newVersion; + +await Deno.writeTextFile( + denoJsonPath, + JSON.stringify(denoJson, null, 2) + "\n", +); + +console.log(newVersion); +Deno.exit(0);