diff --git a/cypress/e2e/02-analysis.cy.ts b/cypress/e2e/02-analysis.cy.ts
index af498b3..10e724d 100644
--- a/cypress/e2e/02-analysis.cy.ts
+++ b/cypress/e2e/02-analysis.cy.ts
@@ -5,7 +5,7 @@ describe("analysis", () => {
cy.visit("/");
// submit button disabled by default
- cy.get(`[data-cy="snippet-submit"]`).should("be.disabled");
+ cy.get(`[data-cy="submit"]`).should("be.disabled");
submitSnippet();
@@ -15,4 +15,3 @@ describe("analysis", () => {
});
});
});
-
diff --git a/cypress/e2e/utils/submit-snippet.ts b/cypress/e2e/utils/submit-snippet.ts
index 5e1f96b..a7c0ad0 100644
--- a/cypress/e2e/utils/submit-snippet.ts
+++ b/cypress/e2e/utils/submit-snippet.ts
@@ -3,10 +3,10 @@ export const submitSnippet = () => {
cy.get(`[data-cy="snippet-input"]`).type(snippetCode, { delay: 1 });
// assert the button is enabled
- cy.get(`[data-cy="snippet-submit"]`).should("be.visible");
+ cy.get(`[data-cy="submit"]`).should("be.visible");
// submit the snippet
- cy.get(`[data-cy="snippet-submit"]`).click();
+ cy.get(`[data-cy="submit"]`).click();
};
const snippetCode = `
diff --git a/package-lock.json b/package-lock.json
index 6ceddb2..c309f56 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -4230,6 +4230,21 @@
"integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
"dev": true
},
+ "node_modules/@types/lodash": {
+ "version": "4.14.195",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.14.195.tgz",
+ "integrity": "sha512-Hwx9EUgdwf2GLarOjQp5ZH8ZmblzcbTBC2wtQWNKARBSxM9ezRIAUpeDTgoQRAFB0+8CNWXVA9+MaSOzOF3nPg==",
+ "dev": true
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.8",
+ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.8.tgz",
+ "integrity": "sha512-euY3XQcZmIzSy7YH5+Unb3b2X12Wtk54YWINBvvGQ5SmMvwb11JQskGsfkH/5HXK77Kr8GF0wkVDIxzAisWtog==",
+ "dev": true,
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
"node_modules/@types/md5": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/@types/md5/-/md5-2.3.2.tgz",
@@ -4295,6 +4310,15 @@
"csstype": "^3.0.2"
}
},
+ "node_modules/@types/react-csv": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/@types/react-csv/-/react-csv-1.1.3.tgz",
+ "integrity": "sha512-dkEdyRvRpygSnNg4cyzYWSUjukIQ5lAtXJwc7BqyUfzww/Cv2dcAFGYd+sWTFpGiDNZMVPp6vVPLcAPvJID8Kg==",
+ "dev": true,
+ "dependencies": {
+ "@types/react": "*"
+ }
+ },
"node_modules/@types/react-dom": {
"version": "18.2.6",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.6.tgz",
@@ -4629,6 +4653,14 @@
"vite": "^4.2.0"
}
},
+ "node_modules/@vscode/vscode-languagedetection": {
+ "version": "1.0.22",
+ "resolved": "https://registry.npmjs.org/@vscode/vscode-languagedetection/-/vscode-languagedetection-1.0.22.tgz",
+ "integrity": "sha512-rQ/BgMyLuIXSmbA0MSkIPHtcOw14QkeDbAq19sjvaS9LTRr905yij0S8lsyqN5JgOsbtIx7pAcyOxFMzPmqhZQ==",
+ "bin": {
+ "vscode-languagedetection": "cli/index.js"
+ }
+ },
"node_modules/@webassemblyjs/ast": {
"version": "1.11.6",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz",
@@ -6652,6 +6684,11 @@
"node": ">=8"
}
},
+ "node_modules/devicon": {
+ "version": "2.15.1",
+ "resolved": "https://registry.npmjs.org/devicon/-/devicon-2.15.1.tgz",
+ "integrity": "sha512-iyapbDVaq1PgMSMZ4IekgjH230aSUU4kHmZ142lQ2TJ+9HB4P4V78eNDkyaQEEoi0KJhxIGPcTSFqxHaZ4OMyw=="
+ },
"node_modules/diff": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz",
@@ -11068,6 +11105,11 @@
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"dev": true
},
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw=="
+ },
"node_modules/lodash.debounce": {
"version": "4.0.8",
"resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz",
@@ -11462,6 +11504,11 @@
"mkdirp": "bin/cmd.js"
}
},
+ "node_modules/monaco-editor": {
+ "version": "0.40.0",
+ "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.40.0.tgz",
+ "integrity": "sha512-1wymccLEuFSMBvCk/jT1YDW/GuxMLYwnFwF9CDyYCxoTw2Pt379J3FUhwy9c43j51JdcxVPjwk0jm0EVDsBS2g=="
+ },
"node_modules/ms": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
@@ -12649,6 +12696,11 @@
"react": ">= 16.3.0"
}
},
+ "node_modules/react-csv": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/react-csv/-/react-csv-2.2.2.tgz",
+ "integrity": "sha512-RG5hOcZKZFigIGE8LxIEV/OgS1vigFQT4EkaHeKgyuCbUAu9Nbd/1RYq++bJcJJ9VOqO/n9TZRADsXNDR4VEpw=="
+ },
"node_modules/react-dom": {
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
@@ -15164,9 +15216,12 @@
"@mui/material": "^5.13.5",
"axios": "^1.4.0",
"filesize": "^10.0.7",
+ "lodash-es": "^4.17.21",
"md5": "^2.3.0",
+ "monaco-editor": "^0.40.0",
"react": "^18.2.0",
"react-countup": "^6.4.2",
+ "react-csv": "^2.2.2",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-icons": "^4.10.1",
@@ -15177,8 +15232,10 @@
},
"devDependencies": {
"@types/express": "^4.17.17",
+ "@types/lodash-es": "^4.17.8",
"@types/md5": "^2.3.2",
"@types/react": "^18.0.37",
+ "@types/react-csv": "^1.1.3",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
@@ -15198,8 +15255,10 @@
"version": "1.0.0",
"license": "ISC",
"dependencies": {
+ "@vscode/vscode-languagedetection": "^1.0.22",
"axios": "^1.4.0",
"cors": "^2.8.5",
+ "devicon": "^2.15.1",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"md5": "^2.3.0",
diff --git a/packages/app/index.html b/packages/app/index.html
index 9a94c5a..829f285 100644
--- a/packages/app/index.html
+++ b/packages/app/index.html
@@ -7,6 +7,10 @@
content="CodeTotal by OX Security, a megalinter UI"
/>
+
Code Total By OX Security
diff --git a/packages/app/package.json b/packages/app/package.json
index 8851054..057a608 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -16,9 +16,12 @@
"@mui/material": "^5.13.5",
"axios": "^1.4.0",
"filesize": "^10.0.7",
+ "lodash-es": "^4.17.21",
"md5": "^2.3.0",
+ "monaco-editor": "^0.40.0",
"react": "^18.2.0",
"react-countup": "^6.4.2",
+ "react-csv": "^2.2.2",
"react-dom": "^18.2.0",
"react-dropzone": "^14.2.3",
"react-icons": "^4.10.1",
@@ -29,8 +32,10 @@
},
"devDependencies": {
"@types/express": "^4.17.17",
+ "@types/lodash-es": "^4.17.8",
"@types/md5": "^2.3.2",
"@types/react": "^18.0.37",
+ "@types/react-csv": "^1.1.3",
"@types/react-dom": "^18.0.11",
"@typescript-eslint/eslint-plugin": "^5.59.0",
"@typescript-eslint/parser": "^5.59.0",
diff --git a/packages/app/src/analysis/actions/analysis-actions.ts b/packages/app/src/analysis/actions/analysis-actions.ts
index 10c241e..490e125 100644
--- a/packages/app/src/analysis/actions/analysis-actions.ts
+++ b/packages/app/src/analysis/actions/analysis-actions.ts
@@ -43,7 +43,8 @@ export const startAnalysis = async (navigate: NavigateFunction) => {
};
const createRequestData = (): Analysis | undefined => {
- const { inputType, repositoryURL, file, snippet } = AnalysisStore.getState();
+ const { inputType, repositoryURL, file, snippet, language } =
+ AnalysisStore.getState();
const sharedVariables = {
name: "Lint",
inputType,
@@ -55,6 +56,10 @@ const createRequestData = (): Analysis | undefined => {
case AnalysisType.File:
return { ...sharedVariables, file } as FileAnalysis;
case AnalysisType.Snippet:
- return { ...sharedVariables, snippet } as SnippetAnalysis;
+ return {
+ ...sharedVariables,
+ snippet,
+ language,
+ } as SnippetAnalysis;
}
};
diff --git a/packages/app/src/analysis/components/CodeSnippetForm.tsx b/packages/app/src/analysis/components/CodeSnippetForm.tsx
index 6963172..bfa637e 100644
--- a/packages/app/src/analysis/components/CodeSnippetForm.tsx
+++ b/packages/app/src/analysis/components/CodeSnippetForm.tsx
@@ -1,17 +1,22 @@
-import { Button, TextField, Theme } from "@mui/material";
+import { TextField, Theme, Typography } from "@mui/material";
import { FC, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { makeStyles } from "tss-react/mui";
+import { LanguageIcon } from "../../common/LanguageIcon";
+import { fetchDetect } from "../../common/utils/detect-lanauge-utils";
import { startAnalysis } from "../actions/analysis-actions";
import { AnalysisStore, useAnalysisStore } from "../stores/analysis-store";
+import { SubmitButton } from "./SubmitButton";
export const CodeSnippetForm: FC = () => {
const { classes } = useStyles();
- const { snippet, snippetEnabled } = useAnalysisStore();
+ const { snippet, snippetEnabled, sending, language } = useAnalysisStore();
const navigate = useNavigate();
const handleChange = useCallback((e: React.ChangeEvent) => {
- AnalysisStore.setState({ snippet: e.target.value });
+ const snippet = e.target.value;
+ AnalysisStore.setState({ snippet });
+ fetchDetect(snippet);
}, []);
const handleSubmit = () => {
@@ -33,15 +38,31 @@ export const CodeSnippetForm: FC = () => {
"data-cy": "snippet-input",
}}
/>
-
+
+
+ {language && language.name && (
+
+ {language.icon ? (
+
+ ) : (
+
+ {language.name}
+
+ )}
+
+ )}
+
+
+ Send Snippet
+
+
);
};
@@ -54,4 +75,15 @@ const useStyles = makeStyles()((theme: Theme) => ({
gap: theme.spacing(2),
padding: theme.spacing(1),
},
+ language: {
+ display: "flex",
+ gap: theme.spacing(1),
+ alignSelf: "flex-start",
+ },
+ footer: {
+ display: "flex",
+ gap: theme.spacing(1),
+ alignSelf: "stretch",
+ justifyContent: "space-between",
+ },
}));
diff --git a/packages/app/src/analysis/components/FileUploadForm.tsx b/packages/app/src/analysis/components/FileUploadForm.tsx
index 516f984..83a4d59 100644
--- a/packages/app/src/analysis/components/FileUploadForm.tsx
+++ b/packages/app/src/analysis/components/FileUploadForm.tsx
@@ -1,4 +1,10 @@
-import { Theme, Typography, alpha, darken } from "@mui/material";
+import {
+ CircularProgress,
+ Theme,
+ Typography,
+ alpha,
+ darken,
+} from "@mui/material";
import { filesize } from "filesize";
import { FC, useCallback, useState } from "react";
import { FileRejection, useDropzone } from "react-dropzone";
@@ -7,13 +13,18 @@ import { useNavigate } from "react-router-dom";
import { makeStyles } from "tss-react/mui";
import config from "../../config";
import { startAnalysis } from "../actions/analysis-actions";
-import { AnalysisStore } from "../stores/analysis-store";
+import {
+ AnalysisStore,
+ AsyncState,
+ useAnalysisStore,
+} from "../stores/analysis-store";
const MAX_SIZE = parseInt(config.CODETOTAL_UPLOAD_FILE_LIMIT_BYTES);
export const FileUploadForm: FC = () => {
const { classes } = useStyles();
const navigate = useNavigate();
+ const { sending } = useAnalysisStore();
const [error, setError] = useState("");
const onDrop = useCallback(
@@ -47,28 +58,46 @@ export const FileUploadForm: FC = () => {
maxSize: MAX_SIZE,
});
+ const uploading = sending === AsyncState.Loading;
+
return (
-
+
- {isDragActive ? (
-
Drop the files here ...
- ) : (
- <>
+ {uploading && (
+
+
- Drag and drop a file here
-
- or
-
- Click to browse
+ Uploading file
- {error && (
-
- {error}
-
- )}
+
+ )}
+ {!uploading && (
+ <>
+
+ {isDragActive ? (
+
+ Release to upload...
+
+ ) : (
+ <>
+
+ Drag and drop a file here
+
+ or
+
+ Click to browse
+
+ {error && (
+
+ {error}
+
+ )}
+ >
+ )}
+
>
)}
@@ -81,16 +110,31 @@ const useStyles = makeStyles()((theme: Theme) => ({
fontSize: "5rem",
color: theme.palette.primary.main,
},
- dropZone: {
- border: `2px dashed ${theme.palette.divider}`,
- padding: theme.spacing(2),
- cursor: "pointer",
- transition: theme.transitions.create("background-color"),
+ container: {
+ outline: `2px dashed ${theme.palette.divider}`,
+ transition: theme.transitions.create("all", {
+ duration: theme.transitions.duration.short,
+ }),
"&:hover,&:focus-within": {
backgroundColor:
theme.palette.mode === "dark"
? alpha(theme.palette.background.paper, 0.2)
: darken(theme.palette.background.paper, 0.05),
+ outline: "2px dashed",
+ outlineColor:
+ theme.palette.mode === "dark"
+ ? darken(theme.palette.divider, 0.3)
+ : darken(theme.palette.divider, 0.3),
},
},
+ uploading: {
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ gap: theme.spacing(1),
+ },
+ dropZone: {
+ padding: theme.spacing(2),
+ cursor: "pointer",
+ },
}));
diff --git a/packages/app/src/analysis/components/InputForm.tsx b/packages/app/src/analysis/components/InputForm.tsx
index 84d6d86..0add499 100644
--- a/packages/app/src/analysis/components/InputForm.tsx
+++ b/packages/app/src/analysis/components/InputForm.tsx
@@ -1,11 +1,4 @@
-import {
- Alert,
- CircularProgress,
- Paper,
- Tab,
- Tabs,
- Theme,
-} from "@mui/material";
+import { Alert, Paper, Tab, Tabs, Theme } from "@mui/material";
import { FC } from "react";
import { AnalysisType, OneOfValues } from "shared-types";
import { makeStyles } from "tss-react/mui";
@@ -37,6 +30,7 @@ export const InputForm: FC = () => {
variant="fullWidth"
>
{
- {sending === "loading" && (
-
-
-
- )}
-
{sending === "error" && (
@@ -111,13 +99,4 @@ const useStyles = makeStyles()((theme: Theme) => ({
flexGrow: 1,
position: "relative",
},
- loader: {
- width: "100%",
- height: "100%",
- display: "grid",
- placeItems: "center",
- position: "absolute",
- top: 0,
- left: 0,
- },
}));
diff --git a/packages/app/src/analysis/components/RepositoryForm.tsx b/packages/app/src/analysis/components/RepositoryForm.tsx
index 2dcd8dc..5dadd44 100644
--- a/packages/app/src/analysis/components/RepositoryForm.tsx
+++ b/packages/app/src/analysis/components/RepositoryForm.tsx
@@ -1,14 +1,15 @@
-import { Button, FormControl, TextField, Theme } from "@mui/material";
+import { FormControl, TextField, Theme } from "@mui/material";
import { FC, useCallback } from "react";
import { useNavigate } from "react-router-dom";
import { makeStyles } from "tss-react/mui";
import { startAnalysis } from "../actions/analysis-actions";
import { AnalysisStore, useAnalysisStore } from "../stores/analysis-store";
+import { SubmitButton } from "./SubmitButton";
export const RepositoryForm: FC = () => {
const { classes } = useStyles();
const navigate = useNavigate();
- const { repositoryURL, repoEnabled } = useAnalysisStore();
+ const { repositoryURL, repoEnabled, sending } = useAnalysisStore();
const handleChange = useCallback((e: React.ChangeEvent) => {
AnalysisStore.setState({ repositoryURL: e.target.value });
@@ -28,14 +29,15 @@ export const RepositoryForm: FC = () => {
/>
-
+
);
};
diff --git a/packages/app/src/analysis/components/SubmitButton.tsx b/packages/app/src/analysis/components/SubmitButton.tsx
new file mode 100644
index 0000000..6536660
--- /dev/null
+++ b/packages/app/src/analysis/components/SubmitButton.tsx
@@ -0,0 +1,25 @@
+import { Button, ButtonProps, CircularProgress } from "@mui/material";
+import { FC } from "react";
+import { BiSend } from "react-icons/bi";
+
+export const SubmitButton: FC
= ({ loading, ...props }) => {
+ return (
+ }
+ startIcon={
+ loading && (
+
+ )
+ }
+ />
+ );
+};
+
+interface SubmitButtonProps extends Omit {
+ loading: boolean;
+}
diff --git a/packages/app/src/analysis/stores/analysis-store.ts b/packages/app/src/analysis/stores/analysis-store.ts
index 31f19bd..31c8bd0 100644
--- a/packages/app/src/analysis/stores/analysis-store.ts
+++ b/packages/app/src/analysis/stores/analysis-store.ts
@@ -1,4 +1,4 @@
-import { AnalysisType, OneOfValues } from "shared-types";
+import { AnalysisType, OneOfValues, ProgrammingLanguage } from "shared-types";
import { useStore } from "zustand";
import { createStore } from "zustand/vanilla";
@@ -8,6 +8,7 @@ const initialState: AnalysisStoreState = {
file: undefined,
sending: "idle",
inputType: AnalysisType.Snippet,
+ language: undefined,
};
export const AnalysisStore = createStore<
@@ -24,6 +25,7 @@ interface AnalysisStoreState {
file?: File;
sending: OneOfValues;
inputType: OneOfValues;
+ language?: ProgrammingLanguage;
}
interface AnalysisStoreFunctions {
diff --git a/packages/app/src/app/components/AppRouteProvider.tsx b/packages/app/src/app/components/AppRouteProvider.tsx
index a3ecf83..c1b768c 100644
--- a/packages/app/src/app/components/AppRouteProvider.tsx
+++ b/packages/app/src/app/components/AppRouteProvider.tsx
@@ -1,5 +1,6 @@
-import { FC, lazy } from "react";
+import { FC, Suspense, lazy } from "react";
import { RouterProvider, createBrowserRouter } from "react-router-dom";
+import { Loader } from "../../report/components/Loader";
import { ErrorPage } from "./ErrorPage";
const LazyAnalysisPage = lazy(() => import("../../analysis/AnalysisPage"));
@@ -18,5 +19,9 @@ const router = createBrowserRouter([
]);
export const AppRouteProvider: FC = () => {
- return ;
+ return (
+ }>
+
+
+ );
};
diff --git a/packages/app/src/common/AnalysisErrorDialog.tsx b/packages/app/src/common/AnalysisErrorDialog.tsx
new file mode 100644
index 0000000..e15184c
--- /dev/null
+++ b/packages/app/src/common/AnalysisErrorDialog.tsx
@@ -0,0 +1,89 @@
+import {
+ Button,
+ Dialog,
+ DialogActions,
+ DialogContent,
+ DialogTitle,
+ Divider,
+ SvgIcon,
+ Theme,
+ Typography,
+} from "@mui/material";
+import { FC, useCallback, useEffect, useState } from "react";
+import { MdErrorOutline } from "react-icons/md";
+import { useNavigate } from "react-router-dom";
+import { makeStyles } from "tss-react/mui";
+import { resetReport } from "../report/actions/error-report-action";
+import { useReportStore } from "../report/stores/fe-report-store";
+
+export const AnalysisErrorDialog: FC = () => {
+ const { classes } = useStyles();
+ const { analysisError, reset } = useReportStore();
+ const [open, setOpen] = useState(!!analysisError);
+ const navigate = useNavigate();
+
+ const handleClose = useCallback(() => {
+ setOpen(false);
+ resetReport();
+ navigate("/");
+ }, [navigate]);
+
+ useEffect(() => {
+ !!analysisError && setOpen(true);
+ }, [analysisError]);
+
+ return (
+
+ );
+};
+
+const useStyles = makeStyles()((theme: Theme) => ({
+ dialogContent: {
+ display: "flex",
+ flexDirection: "column",
+ gap: theme.spacing(2),
+ },
+}));
diff --git a/packages/app/src/common/LanguageIcon.tsx b/packages/app/src/common/LanguageIcon.tsx
new file mode 100644
index 0000000..c7df3ff
--- /dev/null
+++ b/packages/app/src/common/LanguageIcon.tsx
@@ -0,0 +1,57 @@
+import { Theme, Tooltip, useTheme } from "@mui/material";
+import { FC } from "react";
+import { ProgrammingLanguage } from "shared-types";
+import { makeStyles } from "tss-react/mui";
+
+export const LanguageIcon: FC = ({ language }) => {
+ const { classes, cx } = useStyles();
+ const theme = useTheme();
+ const isDarkmode = theme.palette.mode === "dark";
+ if (!language) {
+ return null;
+ }
+
+ // only is dark mode and only for "plain" type icons
+ // they require a background since when used with an img tag
+ // colors can't be applied through css and we end up with a black colored icon
+ const requiresBackground = isDarkmode && language.icon === "plain";
+
+ return (
+
+
+
+
+
+ );
+};
+const useStyles = makeStyles()((theme: Theme) => ({
+ languageIcon: {
+ display: "inline-flex",
+ height: "2em",
+ width: "2em",
+ },
+ img: {
+ width: "100%",
+ height: "100%",
+ },
+ darkmode: {
+ backgroundColor: theme.palette.text.secondary,
+ borderRadius: "50%",
+ display: "inline-flex",
+ },
+}));
+
+interface LanguageIconProps {
+ language?: ProgrammingLanguage;
+}
diff --git a/packages/app/src/common/utils/detect-lanauge-utils.ts b/packages/app/src/common/utils/detect-lanauge-utils.ts
new file mode 100644
index 0000000..7ab4b12
--- /dev/null
+++ b/packages/app/src/common/utils/detect-lanauge-utils.ts
@@ -0,0 +1,15 @@
+import axios from "axios";
+import debounce from "lodash-es/debounce";
+import { ProgrammingLanguage } from "shared-types";
+import { AnalysisStore } from "../../analysis/stores/analysis-store";
+import config from "../../config";
+
+export const detect = async (snippet: string) => {
+ const res = await axios.post(
+ `http://${config.CODETOTAL_HTTP_HOST}:${config.CODETOTAL_HTTP_PORT}/detect`,
+ { snippet }
+ );
+ AnalysisStore.setState({ language: res.data });
+};
+
+export const fetchDetect = debounce(detect, 1000);
diff --git a/packages/app/src/report/ReportPage.tsx b/packages/app/src/report/ReportPage.tsx
index 18eac81..0322086 100644
--- a/packages/app/src/report/ReportPage.tsx
+++ b/packages/app/src/report/ReportPage.tsx
@@ -1,5 +1,6 @@
import { FC, useEffect, useRef } from "react";
import { useNavigate, useParams } from "react-router-dom";
+import { AnalysisErrorDialog } from "../common/AnalysisErrorDialog";
import { initProgress } from "./actions/init-report-action";
import { IssuesTable } from "./components/IssuesTable";
import { ReportDrawer } from "./components/ReportDrawer";
@@ -26,6 +27,7 @@ const ReportPage: FC = () => {
+
);
};
diff --git a/packages/app/src/report/actions/error-report-action.ts b/packages/app/src/report/actions/error-report-action.ts
new file mode 100644
index 0000000..026edda
--- /dev/null
+++ b/packages/app/src/report/actions/error-report-action.ts
@@ -0,0 +1,8 @@
+import { ReportStore } from "../stores/fe-report-store";
+
+export const resetReport = () => {
+ const { unsubscribe, reset } = ReportStore.getState();
+
+ unsubscribe && unsubscribe();
+ reset();
+};
diff --git a/packages/app/src/report/actions/init-report-action.ts b/packages/app/src/report/actions/init-report-action.ts
index 9ce442a..e56ec30 100644
--- a/packages/app/src/report/actions/init-report-action.ts
+++ b/packages/app/src/report/actions/init-report-action.ts
@@ -3,7 +3,7 @@ import { NavigateFunction } from "react-router-dom";
import { AnalysisStatus } from "shared-types";
import config from "../../config";
import { ReportStore } from "../stores/fe-report-store";
-import { subscribeToLintProgress } from "./subscribe-report-action";
+import { subscribeToReportProgress } from "./subscribe-report-action";
export const initProgress = async (
requestId: string,
@@ -17,9 +17,7 @@ export const initProgress = async (
switch (status) {
case AnalysisStatus.Created:
- subscribeToLintProgress(requestId);
- break;
- case AnalysisStatus.Completed:
+ subscribeToReportProgress(requestId);
break;
case AnalysisStatus.NotFound:
navigate("/");
diff --git a/packages/app/src/report/actions/subscribe-report-action.ts b/packages/app/src/report/actions/subscribe-report-action.ts
index 0cda637..8418a7b 100644
--- a/packages/app/src/report/actions/subscribe-report-action.ts
+++ b/packages/app/src/report/actions/subscribe-report-action.ts
@@ -1,32 +1,35 @@
import { AnalysisStatus, ReportState } from "shared-types";
-import { subscribe } from "../../ws-client";
import { ReportStore } from "../stores/fe-report-store";
+import { subscribe } from "../utils/ws-client";
-export const subscribeToLintProgress = (requestId: string) => {
+export const subscribeToReportProgress = (requestId: string) => {
console.log("subscribing to WS...");
// clear previous error
- ReportStore.setState({ subscriptionError: undefined });
+ ReportStore.setState({ wsError: undefined });
- const unsubscribe = subscribe(
+ const unsubscribe = subscribe({
requestId,
- (msg: Partial
) => {
- if (msg.status && msg.status === AnalysisStatus.Completed) {
+ onMessage: (msg: Partial) => {
+ const completed = msg.status && msg.status === AnalysisStatus.Completed;
+ const error = !!msg.analysisError;
+ if (completed || error) {
+ // close ws connection on completed/error
unsubscribe();
}
ReportStore.setState({ ...msg });
},
- () => {
+ onError: () => {
ReportStore.setState({
- subscriptionError: "Web socket connection error",
+ wsError: "Web socket connection error",
});
},
- () => {
+ onClose: () => {
const { inProgress } = ReportStore.getState();
inProgress &&
ReportStore.setState({
- subscriptionError: "Web socket connection closed while scanning",
+ wsError: "Web socket connection closed while scanning",
});
- }
- );
+ },
+ });
ReportStore.setState({ unsubscribe });
};
diff --git a/packages/app/src/report/components/Detection.tsx b/packages/app/src/report/components/Detection.tsx
index 36ac66b..59b3843 100644
--- a/packages/app/src/report/components/Detection.tsx
+++ b/packages/app/src/report/components/Detection.tsx
@@ -21,6 +21,5 @@ const useStyles = makeStyles()(() => ({
detection: {
height: "100%",
position: "relative",
- display: "grid",
},
}));
diff --git a/packages/app/src/report/components/ExportToCSV.tsx b/packages/app/src/report/components/ExportToCSV.tsx
new file mode 100644
index 0000000..5a97d4e
--- /dev/null
+++ b/packages/app/src/report/components/ExportToCSV.tsx
@@ -0,0 +1,47 @@
+import { IconButton, Theme, Tooltip, Zoom } from "@mui/material";
+import { FC } from "react";
+import { CSVLink } from "react-csv";
+import { MdCloudDownload } from "react-icons/md";
+import { AnalysisStatus } from "shared-types";
+import { makeStyles } from "tss-react/mui";
+import { useReportStore } from "../stores/fe-report-store";
+
+export const ExportToCSV: FC = () => {
+ const { classes } = useStyles();
+ const { status, allIssues, requestId } = useReportStore();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+const useStyles = makeStyles()((theme: Theme) => ({
+ exportToCSV: {
+ display: "inline-flex",
+ alignItems: "center",
+ gap: theme.spacing(1),
+ color: theme.palette.primary.main,
+ fontWeight: 400,
+ textDecoration: "none",
+ "&:hover": {
+ textDecoration: "underline",
+ },
+ },
+}));
diff --git a/packages/app/src/report/components/ReportHeader.tsx b/packages/app/src/report/components/ReportHeader.tsx
index 0e32157..2db0713 100644
--- a/packages/app/src/report/components/ReportHeader.tsx
+++ b/packages/app/src/report/components/ReportHeader.tsx
@@ -1,13 +1,20 @@
import {
+ Divider,
+ IconButton,
Paper,
Theme,
+ Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { FC } from "react";
+import { IoMdArrowBack } from "react-icons/io";
+import { NavLink } from "react-router-dom";
import { makeStyles } from "tss-react/mui";
+import { LanguageIcon } from "../../common/LanguageIcon";
import { useReportStore } from "../stores/fe-report-store";
+import { ExportToCSV } from "./ExportToCSV";
import { ReportHeaderSection } from "./ReportHeaderSection";
import { Score } from "./Score";
@@ -23,6 +30,7 @@ export const ReportHeader: FC = ({ ready }) => {
linters = [],
lintersWithIssues,
progress,
+ language,
} = useReportStore();
if (!lintersWithIssues) {
return null;
@@ -64,6 +72,18 @@ export const ReportHeader: FC = ({ ready }) => {
value={resourceValue || "-"}
dataCy="resource-value"
/>
+
+
+
+
+
+
+
+
+
+
+
+
({
fontWeight: 700,
[theme.breakpoints.up("md")]: {
fontSize: "1.562rem",
- marginBlockEnd: theme.spacing(2),
+ marginBlockStart: theme.spacing(2),
textAlign: "start",
},
},
@@ -121,6 +141,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
maxWidth: 600,
[theme.breakpoints.up("md")]: {
flexDirection: "row",
+ alignItems: "flex-end",
},
},
score: {
@@ -134,6 +155,13 @@ const useStyles = makeStyles()((theme: Theme) => ({
nowrap: {
wordBreak: "keep-all",
},
+ footer: {
+ display: "flex",
+ alignItems: "center",
+ gap: theme.spacing(2),
+ paddingBlockStart: theme.spacing(1),
+ justifyContent: "space-between"
+ },
}));
interface ReportBannerProps {
diff --git a/packages/app/src/report/components/ReportHeaderSection.tsx b/packages/app/src/report/components/ReportHeaderSection.tsx
index 0a6b27a..0dc6d48 100644
--- a/packages/app/src/report/components/ReportHeaderSection.tsx
+++ b/packages/app/src/report/components/ReportHeaderSection.tsx
@@ -33,6 +33,7 @@ const useStyles = makeStyles()((theme: Theme) => ({
color: theme.palette.text.secondary,
},
"& > div": {
+ marginBlockStart: theme.spacing(0.5),
color: theme.palette.text.primary,
fontWeight: "bold",
textOverflow: "ellipsis",
diff --git a/packages/app/src/report/stores/fe-report-store.ts b/packages/app/src/report/stores/fe-report-store.ts
index 819823f..6af6e91 100644
--- a/packages/app/src/report/stores/fe-report-store.ts
+++ b/packages/app/src/report/stores/fe-report-store.ts
@@ -1,7 +1,7 @@
-import { AnalysisStatus, ReportState } from "shared-types";
+import { AnalysisStatus, Issue, ReportState } from "shared-types";
import { createStore, useStore } from "zustand";
import { ScoreColorKey } from "../fe-report-types";
-import { resolveScoreColor } from "../utils/report-utils";
+import { resolveScoreColor } from "../utils/score-utils";
const initialState: InitialState = {
status: AnalysisStatus.Created,
@@ -15,7 +15,8 @@ const initialState: InitialState = {
unsubscribe: undefined,
repoDetails: undefined,
fileDetails: undefined,
- subscriptionError: undefined,
+ wsError: undefined,
+ analysisError: undefined,
};
export const ReportStore = createStore((set, get) => ({
@@ -53,6 +54,17 @@ export const ReportStore = createStore((set, get) => ({
reset: () => {
set({ ...initialState });
},
+ allIssues: () => {
+ const { linters } = get();
+ return (linters || [])
+ .map((linter) =>
+ (linter.issues || []).map((issue) => ({
+ ...issue,
+ linter: linter.name,
+ }))
+ )
+ .flat();
+ },
}));
export const useReportStore = () => useStore(ReportStore);
@@ -65,12 +77,13 @@ type InitialState = Omit<
| "reset"
| "lintersCompleted"
| "progress"
+ | "allIssues"
>;
interface FeReportStoreState extends ReportState {
selectedLinterName?: string;
inProgress: boolean;
- subscriptionError?: string;
+ wsError?: string;
issuesCount(toolName: string): number;
unsubscribe?: () => void;
scoreColor(): ScoreColorKey;
@@ -78,4 +91,5 @@ interface FeReportStoreState extends ReportState {
lintersCompleted(): number;
progress(): number;
reset(): void;
+ allIssues(): Issue[];
}
diff --git a/packages/app/src/report/utils/report-utils.ts b/packages/app/src/report/utils/score-utils.ts
similarity index 100%
rename from packages/app/src/report/utils/report-utils.ts
rename to packages/app/src/report/utils/score-utils.ts
diff --git a/packages/app/src/ws-client.ts b/packages/app/src/report/utils/ws-client.ts
similarity index 59%
rename from packages/app/src/ws-client.ts
rename to packages/app/src/report/utils/ws-client.ts
index b17c882..5171a84 100644
--- a/packages/app/src/ws-client.ts
+++ b/packages/app/src/report/utils/ws-client.ts
@@ -1,12 +1,12 @@
import { ReportState } from "shared-types";
-import config from "./config";
+import config from "../../config";
-export const subscribe = (
- requestId: string,
- onMessage: (data: ReportState) => void,
- onError: () => void,
- onClose: () => void
-) => {
+export const subscribe = ({
+ requestId,
+ onMessage,
+ onError,
+ onClose,
+}: WSClientSubsciptionOptions) => {
const ws = new WebSocket(
`ws://${config.CODETOTAL_WS_HOST}:${config.CODETOTAL_WS_PORT}?requestId=${requestId}`
);
@@ -24,3 +24,10 @@ export const subscribe = (
};
return unsubscribe;
};
+
+interface WSClientSubsciptionOptions {
+ requestId: string;
+ onMessage: (data: ReportState) => void;
+ onError: () => void;
+ onClose: () => void;
+}
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 9890256..0d41398 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -16,8 +16,10 @@
"author": "Itay",
"license": "ISC",
"dependencies": {
+ "@vscode/vscode-languagedetection": "^1.0.22",
"axios": "^1.4.0",
"cors": "^2.8.5",
+ "devicon": "^2.15.1",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"md5": "^2.3.0",
diff --git a/packages/backend/src/actions/create-analysis-request.ts b/packages/backend/src/actions/create-analysis-request.ts
index 78c22cd..cd9e01e 100644
--- a/packages/backend/src/actions/create-analysis-request.ts
+++ b/packages/backend/src/actions/create-analysis-request.ts
@@ -5,6 +5,7 @@ import {
Analysis,
AnalysisType,
FileAnalysis,
+ ProgrammingLanguage,
RepoAnalysis,
SnippetAnalysis,
} from "shared-types";
@@ -13,7 +14,7 @@ import { logger } from "../utils/logger";
export const createAnalysisRequestData = async (
action: Analysis
-): Promise<[unknown, string]> => {
+): Promise<[unknown, string, ProgrammingLanguage?]> => {
switch (action.inputType) {
case AnalysisType.Repo: {
return [
@@ -23,7 +24,17 @@ export const createAnalysisRequestData = async (
}
case AnalysisType.Snippet: {
const snippetMd5 = md5((action).snippet);
- return [{ snippet: (action).snippet }, snippetMd5];
+ const snippetAction = action as SnippetAnalysis;
+ const languageId = snippetAction.language?.id;
+ const snippetExtension = languageId ? `.${languageId}` : undefined;
+ return [
+ {
+ snippet: snippetAction.snippet,
+ snippetExtension,
+ },
+ snippetMd5,
+ snippetAction.language,
+ ];
}
case AnalysisType.File: {
const fileAction = action as FileAnalysis;
diff --git a/packages/backend/src/actions/create-analysis.ts b/packages/backend/src/actions/create-analysis.ts
index a59d086..d0b44e1 100644
--- a/packages/backend/src/actions/create-analysis.ts
+++ b/packages/backend/src/actions/create-analysis.ts
@@ -10,7 +10,7 @@ export const createAnalysis = async (
action: Analysis
): Promise<{ requestId: string }> => {
try {
- const [data, resourceValue] = await createAnalysisRequestData(action);
+ const [data, resourceValue, language] = await createAnalysisRequestData(action);
const response = await axios.post<{ request_id: string }>(
config.MEGALINTER_ANALYSIS_URL,
@@ -22,6 +22,7 @@ export const createAnalysis = async (
reportStore.set({
resourceType: action.inputType,
resourceValue,
+ language
});
subscribeToMegaLinter(requestId, reportStore);
return { requestId };
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 28e2dab..59fa832 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -1,6 +1,6 @@
import { subscribeToReport } from "./actions/subscribe-to-report";
import config from "./config";
-import { startHttpServer } from "./transport/http-server";
+import { startHttpServer } from "./transport/http/http-server";
import { startRedisClient } from "./transport/redis-client";
import { createWSServer, listenToWSConnection } from "./transport/ws-server";
diff --git a/packages/backend/src/language-detection/language-detect.test.ts b/packages/backend/src/language-detection/language-detect.test.ts
new file mode 100644
index 0000000..7e8e3bb
--- /dev/null
+++ b/packages/backend/src/language-detection/language-detect.test.ts
@@ -0,0 +1,23 @@
+import { detectLanguage } from "./language-detect";
+import { tsCodeSnippet } from "./snippets-mocks";
+
+describe("language-detect", () => {
+ test("resolve language", async () => {
+ const language = await detectLanguage(tsCodeSnippet);
+ expect(language.id).toBeDefined();
+ expect(language.name).toBeDefined();
+ expect(language.icon).toBeDefined();
+ });
+
+ test("resolve language without an icon", async () => {
+ const language = await detectLanguage("MKDIR mydir Set X=123");
+ expect(language.id).toBeDefined();
+ expect(language.name).toBeDefined();
+ expect(language.icon).toBeUndefined();
+ });
+
+ test("resolveId return undefined when language not detected", async () => {
+ const language = await detectLanguage("");
+ expect(language).toBeUndefined();
+ });
+});
diff --git a/packages/backend/src/language-detection/language-detect.ts b/packages/backend/src/language-detection/language-detect.ts
new file mode 100644
index 0000000..dff7168
--- /dev/null
+++ b/packages/backend/src/language-detection/language-detect.ts
@@ -0,0 +1,22 @@
+import { ProgrammingLanguage } from "shared-types";
+import { resolveIcon } from "./language-resolve-icon";
+import { resolveName } from "./language-resolve-name";
+import { resolveId } from "./language-resolve-id";
+
+export const detectLanguage = async (
+ snippet: string
+): Promise => {
+ const languageId = await resolveId(snippet);
+
+ if (languageId) {
+ const name = resolveName(languageId);
+ const icon = resolveIcon(name);
+ return {
+ id: languageId,
+ name,
+ icon,
+ };
+ }
+
+ return undefined;
+};
diff --git a/packages/backend/src/language-detection/language-resolve-icon.test.ts b/packages/backend/src/language-detection/language-resolve-icon.test.ts
new file mode 100644
index 0000000..e662861
--- /dev/null
+++ b/packages/backend/src/language-detection/language-resolve-icon.test.ts
@@ -0,0 +1,23 @@
+import { resolveIcon } from "./language-resolve-icon";
+
+describe("language-resolve-icon", () => {
+ test("resolve TypeScript Icon", () => {
+ const icon = resolveIcon("TypeScript");
+ expect(icon).toBe("original");
+ });
+
+ test("resolve Rust Icon", () => {
+ const icon = resolveIcon("Rust");
+ expect(icon).toBe("plain");
+ });
+
+ test("resolve Python Icon", () => {
+ const icon = resolveIcon("Python");
+ expect(icon).toBe("original");
+ });
+
+ test("resolveIcon return undefined when icon not found", () => {
+ const name = resolveIcon("123");
+ expect(name).toBeUndefined();
+ });
+});
diff --git a/packages/backend/src/language-detection/language-resolve-icon.ts b/packages/backend/src/language-detection/language-resolve-icon.ts
new file mode 100644
index 0000000..897300f
--- /dev/null
+++ b/packages/backend/src/language-detection/language-resolve-icon.ts
@@ -0,0 +1,12 @@
+import iconsMap from "devicon/devicon.json";
+
+export const resolveIcon = (languageName: string): string | undefined => {
+ const lcName = languageName.toLowerCase();
+ const devIconInfo = iconsMap.find((l) => l.name === lcName);
+
+ if (devIconInfo) {
+ return devIconInfo.versions.svg[0];
+ }
+
+ return undefined;
+};
diff --git a/packages/backend/src/language-detection/language-resolve-id.test.ts b/packages/backend/src/language-detection/language-resolve-id.test.ts
new file mode 100644
index 0000000..1196714
--- /dev/null
+++ b/packages/backend/src/language-detection/language-resolve-id.test.ts
@@ -0,0 +1,19 @@
+import { resolveId } from "./language-resolve-id";
+import { pyCodeSnippet, tsCodeSnippet } from "./snippets-mocks";
+
+describe("language-resolve-id", () => {
+ test("resolveId language id from snippet", async () => {
+ const id = await resolveId(pyCodeSnippet);
+ expect(id).toBe("py");
+ });
+
+ test("resolveId language id from snippet", async () => {
+ const id = await resolveId(tsCodeSnippet);
+ expect(id).toBe("ts");
+ });
+
+ test("resolveId return undefined when language not detected", async () => {
+ const id = await resolveId("123");
+ expect(id).toBeUndefined();
+ });
+});
diff --git a/packages/backend/src/language-detection/language-resolve-id.ts b/packages/backend/src/language-detection/language-resolve-id.ts
new file mode 100644
index 0000000..bded142
--- /dev/null
+++ b/packages/backend/src/language-detection/language-resolve-id.ts
@@ -0,0 +1,17 @@
+import { ModelOperations } from "@vscode/vscode-languagedetection";
+
+// Language detection of code snippet using @vscode/vscode-languagedetection (guesslang)
+// Name resolution using highlight.js (https://github.com/highlightjs/highlight.js/blob/main/SUPPORTED_LANGUAGES.md)
+// Icon resolution using "devicon" (https://devicon.dev/)
+
+const modulOperations = new ModelOperations();
+
+export const resolveId = async (snippet: string): Promise => {
+ const detection = await modulOperations.runModel(snippet);
+
+ if (detection.length > 0) {
+ return detection[0].languageId;
+ }
+
+ return undefined;
+};
diff --git a/packages/backend/src/language-detection/language-resolve-name.test.ts b/packages/backend/src/language-detection/language-resolve-name.test.ts
new file mode 100644
index 0000000..42660a1
--- /dev/null
+++ b/packages/backend/src/language-detection/language-resolve-name.test.ts
@@ -0,0 +1,13 @@
+import { resolveName } from "./language-resolve-name";
+
+describe("language-resolve-name", () => {
+ test("resolveName", () => {
+ const name = resolveName("ts");
+ expect(name).toBe("TypeScript");
+ });
+
+ test("resolveName return undefined when language not found", () => {
+ const name = resolveName("123");
+ expect(name).toBeUndefined();
+ });
+});
diff --git a/packages/backend/src/language-detection/language-resolve-name.ts b/packages/backend/src/language-detection/language-resolve-name.ts
new file mode 100644
index 0000000..92391cf
--- /dev/null
+++ b/packages/backend/src/language-detection/language-resolve-name.ts
@@ -0,0 +1,9 @@
+import languagesMap from "./languages-id-to-name-map.json";
+
+export const resolveName = (languageId: string): string | undefined => {
+ if (languagesMap[languageId]) {
+ return languagesMap[languageId];
+ }
+
+ return undefined;
+};
diff --git a/packages/backend/src/language-detection/languages-id-to-name-map.json b/packages/backend/src/language-detection/languages-id-to-name-map.json
new file mode 100644
index 0000000..dfe86f8
--- /dev/null
+++ b/packages/backend/src/language-detection/languages-id-to-name-map.json
@@ -0,0 +1,420 @@
+{
+ "1c": "1C",
+ "4d": "4D",
+ "sap-abap": "ABAP",
+ "abap": "ABAP",
+ "abnf": "ABNF",
+ "accesslog": "Access logs",
+ "ada": "Ada",
+ "apex": "Apex",
+ "arduino": "Arduino (C++ w/Arduino libs)",
+ "ino": "Arduino (C++ w/Arduino libs)",
+ "armasm": "ARM assembler",
+ "arm": "ARM assembler",
+ "avrasm": "AVR assembler",
+ "actionscript": "ActionScript",
+ "as": "ActionScript",
+ "alan": "Alan IF",
+ "i": "Alan IF",
+ "ln": "Alan",
+ "angelscript": "AngelScript",
+ "asc": "AngelScript",
+ "apache": "Apache",
+ "apacheconf": "Apache",
+ "applescript": "AppleScript",
+ "osascript": "AppleScript",
+ "arcade": "Arcade",
+ "asciidoc": "AsciiDoc",
+ "adoc": "AsciiDoc",
+ "aspectj": "AspectJ",
+ "autohotkey": "AutoHotkey",
+ "autoit": "AutoIt",
+ "awk": "Awk",
+ "mawk": "Awk",
+ "nawk": "Awk",
+ "gawk": "Awk",
+ "bash": "Bash",
+ "sh": "Bash",
+ "zsh": "Bash",
+ "basic": "Basic",
+ "bbcode": "BBCode",
+ "blade": "Blade (Laravel)",
+ "bnf": "BNF",
+ "bqn": "BQN",
+ "brainfuck": "Brainfuck",
+ "bf": "Brainfuck",
+ "csharp": "C#",
+ "cs": "C#",
+ "c": "C",
+ "h": "C",
+ "cpp": "C++",
+ "hpp": "C++",
+ "cc": "C++",
+ "hh": "C++",
+ "c++": "C++",
+ "h++": "C++",
+ "cxx": "C++",
+ "hxx": "C++",
+ "cal": "C/AL",
+ "c3": "C3",
+ "cos": "Cache Object Script",
+ "cls": "Cache Object Script",
+ "candid": "Candid",
+ "did": "Candid",
+ "cmake": "CMake",
+ "cmake.in": "CMake",
+ "cobol": "COBOL",
+ "standard-cobol": "COBOL",
+ "coq": "Coq",
+ "csp": "CSP",
+ "css": "CSS",
+ "capnproto": "Cap’n Proto",
+ "capnp": "Cap’n Proto",
+ "chaos": "Chaos",
+ "kaos": "Chaos",
+ "chapel": "Chapel",
+ "chpl": "Chapel",
+ "cisco": "Cisco CLI",
+ "clojure": "Clojure",
+ "clj": "Clojure",
+ "coffeescript": "CoffeeScript",
+ "coffee": "CoffeeScript",
+ "cson": "CoffeeScript",
+ "iced": "CoffeeScript",
+ "cpc": "CpcdosC+",
+ "crmsh": "Crmsh",
+ "crm": "Crmsh",
+ "pcmk": "Crmsh",
+ "crystal": "Crystal",
+ "cr": "Crystal",
+ "curl": "cURL",
+ "cypher": "Cypher (Neo4j)",
+ "d": "D",
+ "dafny": "Dafny",
+ "dart": "Dart",
+ "dpr": "Delphi",
+ "dfm": "Delphi",
+ "pas": "Delphi",
+ "pascal": "Delphi",
+ "diff": "Diff",
+ "patch": "Diff",
+ "django": "Django",
+ "jinja": "Django",
+ "dns": "DNS Zone file",
+ "zone": "DNS Zone file",
+ "bind": "DNS Zone file",
+ "dockerfile": "Dockerfile",
+ "docker": "Dockerfile",
+ "dos": "DOS",
+ "bat": "DOS",
+ "cmd": "DOS",
+ "dsconfig": "dsconfig",
+ "dts": "DTS (Device Tree)",
+ "dust": "Dust",
+ "dst": "Dust",
+ "dylan": "Dylan",
+ "ebnf": "EBNF",
+ "elixir": "Elixir",
+ "elm": "Elm",
+ "erlang": "Erlang",
+ "erl": "Erlang",
+ "excel": "Excel",
+ "xls": "Excel",
+ "xlsx": "Excel",
+ "extempore": "Extempore",
+ "xtlang": "Extempore",
+ "xtm": "Extempore",
+ "fsharp": "F#",
+ "fs": "F#",
+ "fix": "FIX",
+ "flix": "Flix",
+ "fortran": "Fortran",
+ "f90": "Fortran",
+ "f95": "Fortran",
+ "func": "FunC",
+ "gcode": "G-Code",
+ "nc": "G-Code",
+ "gams": "Gams",
+ "gms": "Gams",
+ "gauss": "GAUSS",
+ "gss": "GAUSS",
+ "godot": "GDScript",
+ "gdscript": "GDScript",
+ "gherkin": "Gherkin",
+ "hbs": "Handlebars",
+ "glimmer": "Glimmer and EmberJS",
+ "html.hbs": "Handlebars",
+ "html.handlebars": "Handlebars",
+ "htmlbars": "Glimmer and EmberJS",
+ "gn": "GN for Ninja",
+ "gni": "GN for Ninja",
+ "go": "Go",
+ "golang": "Go",
+ "gf": "Grammatical Framework",
+ "golo": "Golo",
+ "gololang": "Golo",
+ "gradle": "Gradle",
+ "graphql": "GraphQL",
+ "groovy": "Groovy",
+ "gsql": "GSQL",
+ "xml": "HTML, XML",
+ "html": "HTML, XML",
+ "xhtml": "HTML, XML",
+ "rss": "HTML, XML",
+ "atom": "HTML, XML",
+ "xjb": "HTML, XML",
+ "xsd": "HTML, XML",
+ "xsl": "HTML, XML",
+ "plist": "HTML, XML",
+ "svg": "HTML, XML",
+ "http": "HTTP",
+ "https": "HTTP",
+ "haml": "Haml",
+ "handlebars": "Handlebars",
+ "haskell": "Haskell",
+ "hs": "Haskell",
+ "haxe": "Haxe",
+ "hx": "Haxe",
+ "hlsl": "High-level shader language",
+ "hy": "Hy",
+ "hylang": "Hy",
+ "ini": "Ini, TOML",
+ "toml": "Ini, TOML",
+ "inform7": "Inform7",
+ "i7": "Inform7",
+ "irpf90": "IRPF90",
+ "iptables": "Iptables",
+ "json": "JSON",
+ "java": "Java",
+ "jsp": "Java",
+ "javascript": "JavaScript",
+ "js": "JavaScript",
+ "jsx": "JavaScript",
+ "jolie": "Jolie",
+ "iol": "Jolie",
+ "ol": "Jolie",
+ "julia": "Julia",
+ "julia-repl": "Julia",
+ "kotlin": "Kotlin",
+ "kt": "Kotlin",
+ "": "Lang",
+ "tex": "LaTeX",
+ "leaf": "Leaf",
+ "lean": "Lean",
+ "lasso": "Lasso",
+ "ls": "LiveScript",
+ "lassoscript": "Lasso",
+ "less": "Less",
+ "ldif": "LDIF",
+ "lisp": "Lisp",
+ "livecodeserver": "LiveCode Server",
+ "livescript": "LiveScript",
+ "lookml": "LookML",
+ "lua": "Lua",
+ "macaulay2": "Macaulay2",
+ "makefile": "Makefile",
+ "mk": "Makefile",
+ "mak": "Makefile",
+ "make": "Makefile",
+ "markdown": "Markdown",
+ "md": "Markdown",
+ "mkdown": "Markdown",
+ "mkd": "Markdown",
+ "mathematica": "Mathematica",
+ "mma": "Mathematica",
+ "wl": "Mathematica",
+ "matlab": "Matlab",
+ "maxima": "Maxima",
+ "mel": "Maya Embedded Language",
+ "mercury": "Mercury",
+ "mips": "MIPS Assembler",
+ "mipsasm": "MIPS Assembler",
+ "mint": "Mint",
+ "mirc": "mIRC Scripting Language",
+ "mrc": "mIRC Scripting Language",
+ "mizar": "Mizar",
+ "mkb": "MKB",
+ "mlir": "MLIR",
+ "mojolicious": "Mojolicious",
+ "monkey": "Monkey",
+ "moonscript": "Moonscript",
+ "moon": "Moonscript",
+ "motoko": "Motoko",
+ "mo": "Motoko",
+ "n1ql": "N1QL",
+ "nsis": "NSIS",
+ "never": "Never",
+ "nginx": "Nginx",
+ "nginxconf": "Nginx",
+ "nim": "Nim",
+ "nimrod": "Nim",
+ "nix": "Nix",
+ "oak": "Oak",
+ "ocl": "Object Constraint Language",
+ "ocaml": "OCaml",
+ "ml": "OCaml",
+ "objectivec": "Objective C",
+ "mm": "Objective C",
+ "objc": "Objective C",
+ "obj-c": "Objective C",
+ "obj-c++": "Objective C",
+ "objective-c++": "Objective C",
+ "glsl": "OpenGL Shading Language",
+ "openscad": "OpenSCAD",
+ "scad": "OpenSCAD",
+ "ruleslanguage": "Oracle Rules Language",
+ "oxygene": "Oxygene",
+ "pf": "PF",
+ "pf.conf": "PF",
+ "php": "PHP",
+ "papyrus": "Papyrus",
+ "psc": "Papyrus",
+ "parser3": "Parser3",
+ "perl": "Perl",
+ "pl": "Perl",
+ "pm": "Perl",
+ "pine": "Pine Script",
+ "pinescript": "Pine Script",
+ "plaintext": "Plaintext",
+ "txt": "Plaintext",
+ "text": "Plaintext",
+ "pony": "Pony",
+ "pgsql": "PostgreSQL & PL/pgSQL",
+ "postgres": "PostgreSQL & PL/pgSQL",
+ "postgresql": "PostgreSQL & PL/pgSQL",
+ "powershell": "PowerShell",
+ "ps": "PowerShell",
+ "ps1": "PowerShell",
+ "processing": "Processing",
+ "prolog": "Prolog",
+ "properties": "Properties",
+ "proto": "Protocol Buffers",
+ "protobuf": "Protocol Buffers",
+ "puppet": "Puppet",
+ "pp": "Puppet",
+ "python": "Python",
+ "py": "Python",
+ "gyp": "Python",
+ "profile": "Python profiler results",
+ "python-repl": "Python REPL",
+ "pycon": "Python REPL",
+ "qsharp": "Q#",
+ "k": "Q",
+ "kdb": "Q",
+ "qml": "QML",
+ "r": "R",
+ "cshtml": "Razor CSHTML",
+ "razor": "Razor CSHTML",
+ "razor-cshtml": "Razor CSHTML",
+ "reasonml": "ReasonML",
+ "re": "ReasonML",
+ "redbol": "Rebol & Red",
+ "rebol": "Rebol & Red",
+ "red": "Rebol & Red",
+ "red-system": "Rebol & Red",
+ "rib": "RenderMan RIB",
+ "rsl": "RenderMan RSL",
+ "risc": "RiScript",
+ "riscript": "RiScript",
+ "graph": "Roboconf",
+ "instances": "Roboconf",
+ "robot": "Robot Framework",
+ "rf": "Robot Framework",
+ "rpm-specfile": "RPM spec files",
+ "rpm": "RPM spec files",
+ "spec": "RPM spec files",
+ "rpm-spec": "RPM spec files",
+ "specfile": "RPM spec files",
+ "ruby": "Ruby",
+ "rb": "Ruby",
+ "gemspec": "Ruby",
+ "podspec": "Ruby",
+ "thor": "Ruby",
+ "irb": "Ruby",
+ "rust": "Rust",
+ "rs": "Rust",
+ "rvt": "RVT Script",
+ "rvt-script": "RVT Script",
+ "SAS": "SAS",
+ "sas": "SAS",
+ "scss": "SCSS",
+ "sql": "SQL",
+ "p21": "STEP Part 21",
+ "step": "STEP Part 21",
+ "stp": "STEP Part 21",
+ "scala": "Scala",
+ "scheme": "Scheme",
+ "scilab": "Scilab",
+ "sci": "Scilab",
+ "sfz": "SFZ",
+ "shexc": "Shape Expressions",
+ "shell": "Shell",
+ "console": "Shell",
+ "smali": "Smali",
+ "smalltalk": "Smalltalk",
+ "st": "Smalltalk",
+ "sml": "SML",
+ "solidity": "Solidity",
+ "sol": "Solidity",
+ "spl": "Splunk SPL",
+ "stan": "Stan",
+ "stanfuncs": "Stan",
+ "stata": "Stata",
+ "iecst": "Structured Text",
+ "scl": "Structured Text",
+ "stl": "Structured Text",
+ "structured-text": "Structured Text",
+ "stylus": "Stylus",
+ "styl": "Stylus",
+ "subunit": "SubUnit",
+ "supercollider": "Supercollider",
+ "sc": "Supercollider",
+ "svelte": "Svelte",
+ "swift": "Swift",
+ "tcl": "Tcl",
+ "tk": "Tcl",
+ "terraform": "Terraform (HCL)",
+ "tf": "Terraform (HCL)",
+ "hcl": "Terraform (HCL)",
+ "tap": "Test Anything Protocol",
+ "thrift": "Thrift",
+ "toit": "Toit",
+ "tp": "TP",
+ "tsql": "Transact-SQL",
+ "twig": "Twig",
+ "craftcms": "Twig",
+ "typescript": "TypeScript",
+ "ts": "TypeScript",
+ "tsx": "TypeScript",
+ "mts": "TypeScript",
+ "cts": "TypeScript",
+ "unicorn-rails-log": "Unicorn Rails log",
+ "vbnet": "VB.Net",
+ "vb": "VB.Net",
+ "vba": "VBA",
+ "vbscript": "VBScript",
+ "vbs": "VBScript",
+ "vhdl": "VHDL",
+ "vala": "Vala",
+ "verilog": "Verilog",
+ "v": "Verilog",
+ "vim": "Vim Script",
+ "xsharp": "X#",
+ "xs": "X#",
+ "prg": "X#",
+ "axapta": "X++",
+ "x++": "X++",
+ "x86asm": "x86 Assembly",
+ "xl": "XL",
+ "tao": "XL",
+ "xquery": "XQuery",
+ "xpath": "XQuery",
+ "xq": "XQuery",
+ "xqm": "XQuery",
+ "yml": "YAML",
+ "yaml": "YAML",
+ "zenscript": "ZenScript",
+ "zs": "ZenScript",
+ "zephir": "Zephir",
+ "zep": "Zephir"
+}
diff --git a/packages/backend/src/language-detection/snippets-mocks.ts b/packages/backend/src/language-detection/snippets-mocks.ts
new file mode 100644
index 0000000..0645265
--- /dev/null
+++ b/packages/backend/src/language-detection/snippets-mocks.ts
@@ -0,0 +1,28 @@
+export const pyCodeSnippet = `
+ import pandas as pd
+ import requests
+ from requests import post
+ print(pd.read_csv('1.csv'))
+ HEADERS = {
+ 'Accept': '*/*',
+ 'Accept-Language': 'en-US,en;q=0.9,ru;q=0.8',
+ 'authorization': 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGllbnROYW1lIjoid2ViYXBwLXYzMSIsInNjb3BlIjoic3RhdGljLWNvbnRlbnQtYXBpLGN1cmF0aW9uLWFwaSxuZXh4LWNvbnRlbnQtYXBpLXYzMSx3ZWJhcHAtYXBpIn0.mbuG9wS9Yf5q6PqgR4fiaRFIagiHk9JhwoKES7ksVX4',
+ }
+ res1 = requests.get('http://pbom.dev/', headers=HEADERS)
+ res2 = requests.get('http://ox.security/', allow_redirects=True)
+ res3 = requests.get('https://megalinter.io/', verify=False)
+`;
+
+export const tsCodeSnippet = `
+import iconsMap from "devicon/devicon.json";
+export const resolveIcon = (languageName: string): string | undefined => {
+ const lcName = languageName.toLowerCase();
+ const devIconInfo = iconsMap.find((l) => l.name === lcName);
+
+ if (devIconInfo) {
+ return devIconInfo.versions.svg[0];
+ }
+
+ return undefined;
+};
+`;
diff --git a/packages/backend/src/megalinter/megalinter-types.ts b/packages/backend/src/megalinter/megalinter-types.ts
index dd5b181..38f359c 100644
--- a/packages/backend/src/megalinter/megalinter-types.ts
+++ b/packages/backend/src/megalinter/megalinter-types.ts
@@ -95,13 +95,21 @@ export interface LinterCompleteMessage extends BaseMessage {
export interface MegalinterErrorMessage extends BaseMessage {
messageType: typeof MessageType.ServerError;
message: string;
- errorCode: string;
+ errorCode: MegalinterErrorCode;
errorDetails: {
error: string;
};
requestId: string;
}
+export enum MegalinterErrorCode {
+ MissingAnalysisType = "missingAnalysisType",
+ GitClone = "gitCloneError",
+ UploadFileNotFound = "uploadedFileNotFound",
+ SnippetGuessError = "snippetGuessError",
+ SnippetBuildError = "snippetBuildError",
+}
+
export interface RawLinter {
descriptorId: string;
linterId: string;
diff --git a/packages/backend/src/megalinter/parsers/parse-errors.test.ts b/packages/backend/src/megalinter/parsers/parse-errors.test.ts
new file mode 100644
index 0000000..90f1742
--- /dev/null
+++ b/packages/backend/src/megalinter/parsers/parse-errors.test.ts
@@ -0,0 +1,47 @@
+import { MegalinterErrorMessage, MessageType } from "../megalinter-types";
+import { parseMegalinterError } from "./parse-errors";
+
+const storeMock = {
+ set: jest.fn(),
+ get: jest.fn().mockImplementation(() => ({ linters: [{ name: "devskim" }] })),
+ subscribe: jest.fn(),
+};
+
+describe("parse-errors", () => {
+ test("parseMegalinterError", () => {
+ const linterErrorMessage = {
+ messageType: MessageType.ServerError,
+ message: "Some error message",
+ errorCode: "gitCloneError",
+ errorDetails: {
+ error: "Some error details",
+ },
+ requestId: "123",
+ };
+ parseMegalinterError(
+ linterErrorMessage as MegalinterErrorMessage,
+ storeMock
+ );
+
+ expect(storeMock.set).toBeCalledWith({
+ analysisError: {
+ errorCode: "gitCloneError",
+ errorMessage: "Some error message",
+ errorDetails: "Some error details",
+ },
+ });
+ });
+
+ test("parseMegalinterError should fail gracefully", () => {
+ const linterErrorMessage = {};
+
+ try {
+ parseMegalinterError(
+ linterErrorMessage as MegalinterErrorMessage,
+ storeMock
+ );
+ } catch (err) {
+ expect(err.message).toBe("Unable to parse megalinter error message");
+ }
+ });
+});
diff --git a/packages/backend/src/megalinter/parsers/parse-errors.ts b/packages/backend/src/megalinter/parsers/parse-errors.ts
new file mode 100644
index 0000000..d2a2c2e
--- /dev/null
+++ b/packages/backend/src/megalinter/parsers/parse-errors.ts
@@ -0,0 +1,23 @@
+import { ReportStore } from "../../stores/be-report-store";
+import { logger } from "../../utils/logger";
+import { MegalinterErrorMessage } from "../megalinter-types";
+
+export const parseMegalinterError = (
+ msg: MegalinterErrorMessage,
+ reportStore: ReportStore
+) => {
+ try {
+ const { errorCode, message, errorDetails } = msg;
+ const analysisError = {
+ errorCode,
+ errorMessage: message,
+ errorDetails: errorDetails.error,
+ };
+ reportStore.set({
+ analysisError,
+ });
+ } catch (err) {
+ logger.megalinter.error(err);
+ throw new Error("Unable to parse megalinter error message");
+ }
+};
diff --git a/packages/backend/src/megalinter/parsers/parse-linter-status.test.ts b/packages/backend/src/megalinter/parsers/parse-linter-status.test.ts
index 9ed0cff..16f9ada 100644
--- a/packages/backend/src/megalinter/parsers/parse-linter-status.test.ts
+++ b/packages/backend/src/megalinter/parsers/parse-linter-status.test.ts
@@ -9,7 +9,7 @@ const storeMock = {
};
describe("parse-linter-status", () => {
- test("createWSServer", () => {
+ test("parseLinterStatus", () => {
const linterCompleteMessage = {
linterStatus: LinterStatus.Success,
linterId: "devskim",
diff --git a/packages/backend/src/megalinter/parsers/parser.ts b/packages/backend/src/megalinter/parsers/parser.ts
index 76e642a..f514055 100644
--- a/packages/backend/src/megalinter/parsers/parser.ts
+++ b/packages/backend/src/megalinter/parsers/parser.ts
@@ -3,10 +3,12 @@ import {
BaseMessage,
LinterCompleteMessage,
MegalinterCompleteMessage,
+ MegalinterErrorMessage,
MegalinterStartMessage,
MessageType,
} from "../megalinter-types";
import { parseDetails } from "./parse-details";
+import { parseMegalinterError } from "./parse-errors";
import { parseLinterStatus } from "./parse-linter-status";
import { parseMegalinterComplete } from "./parse-megalinter-complete";
import { parseMegalinterStart } from "./parse-megalinter-start";
@@ -25,6 +27,9 @@ export const parseMessage = (msg: BaseMessage, reportStore: ReportStore) => {
parseLinterStatus(msg as LinterCompleteMessage, reportStore);
runSarif(msg as LinterCompleteMessage, reportStore);
break;
+ case MessageType.ServerError:
+ parseMegalinterError(msg as MegalinterErrorMessage, reportStore);
+ break;
}
parsePackages(msg, reportStore);
diff --git a/packages/backend/src/stores/be-report-store.test.ts b/packages/backend/src/stores/be-report-store.test.ts
index a364c6c..e2df2ac 100644
--- a/packages/backend/src/stores/be-report-store.test.ts
+++ b/packages/backend/src/stores/be-report-store.test.ts
@@ -8,6 +8,7 @@ describe("report-store", () => {
expect(getStore("123").get().requestId).toEqual("123");
expect(getStore("123").get().status).toEqual(AnalysisStatus.Created);
expect(getStore("123").get().score).toEqual(0);
+ expect(getStore("123").get().analysisError).toEqual(undefined);
expect(getStore("123")).toEqual(reportStore);
});
});
diff --git a/packages/backend/src/stores/be-report-store.ts b/packages/backend/src/stores/be-report-store.ts
index 8926afa..7773b24 100644
--- a/packages/backend/src/stores/be-report-store.ts
+++ b/packages/backend/src/stores/be-report-store.ts
@@ -5,7 +5,7 @@ import { addStore } from "./stores-map";
export const createReportStore = (requestId: string) => {
logger.stores.log(`Creating report store for requestId: "${requestId}"`);
-
+
const reportStore = createStore({
requestId,
resourceType: undefined,
@@ -16,8 +16,12 @@ export const createReportStore = (requestId: string) => {
repoDetails: undefined,
fileDetails: undefined,
score: 0,
+ analysisError: undefined,
});
+
+ // save the store instance for later use
addStore(requestId, reportStore);
+
return reportStore;
};
@@ -25,4 +29,5 @@ export type InitialReportStoreState = Pick<
ReportState,
"requestId" | "status" | "score"
>;
+
export type ReportStore = Store;
diff --git a/packages/backend/src/transport/http/http-file-upload-handler.test.ts b/packages/backend/src/transport/http/http-file-upload-handler.test.ts
new file mode 100644
index 0000000..8355365
--- /dev/null
+++ b/packages/backend/src/transport/http/http-file-upload-handler.test.ts
@@ -0,0 +1,22 @@
+import {
+ FileUploader,
+ createFileUploadHandler,
+} from "./http-file-upload-handler";
+
+const memStorageMock = jest.fn(() => ({}));
+const singleFuncMock = jest.fn(() => jest.fn());
+const uploader = jest.fn(() => ({ single: singleFuncMock }));
+uploader["memoryStorage"] = memStorageMock;
+
+describe("http-file-upload-handler", () => {
+ test("createFileUploadHandler", () => {
+ const handler = createFileUploadHandler(
+ uploader as unknown as FileUploader
+ );
+
+ expect(memStorageMock).toBeCalled();
+ expect(uploader).toBeCalledWith({ storage: {} });
+ expect(singleFuncMock).toBeCalledWith("file");
+ expect(typeof handler).toBe("function");
+ });
+});
diff --git a/packages/backend/src/transport/http/http-file-upload-handler.ts b/packages/backend/src/transport/http/http-file-upload-handler.ts
new file mode 100644
index 0000000..8647c0e
--- /dev/null
+++ b/packages/backend/src/transport/http/http-file-upload-handler.ts
@@ -0,0 +1,12 @@
+import { RequestHandler } from "express";
+
+export const createFileUploadHandler = (uploader: FileUploader): RequestHandler => {
+ const storage = uploader.memoryStorage();
+ const upload = uploader({ storage });
+ return upload.single("file");
+};
+
+export type FileUploader = {
+ memoryStorage(): object;
+ (options: { storage: object });
+};
diff --git a/packages/backend/src/transport/http-server.ts b/packages/backend/src/transport/http/http-server.ts
similarity index 69%
rename from packages/backend/src/transport/http-server.ts
rename to packages/backend/src/transport/http/http-server.ts
index 9c61842..003a8eb 100644
--- a/packages/backend/src/transport/http-server.ts
+++ b/packages/backend/src/transport/http/http-server.ts
@@ -4,9 +4,14 @@ import express, { Request, Response } from "express";
import multer from "multer";
import path from "node:path";
import { Analysis, AnalysisStatus, FileAnalysis } from "shared-types";
-import { createAnalysis } from "../actions/create-analysis";
-import { getStore } from "../stores/stores-map";
-import { logger } from "../utils/logger";
+import { createAnalysis } from "../../actions/create-analysis";
+import { detectLanguage } from "../../language-detection/language-detect";
+import { getStore } from "../../stores/stores-map";
+import { logger } from "../../utils/logger";
+import {
+ FileUploader,
+ createFileUploadHandler,
+} from "./http-file-upload-handler";
export const startHttpServer = ({ host, port }: HttpServerOptions) => {
const app = express();
@@ -28,9 +33,9 @@ export const startHttpServer = ({ host, port }: HttpServerOptions) => {
// add analysis route
app.post(
"/analysis",
- createFileUploadHandler(),
+ createFileUploadHandler(multer as FileUploader),
async (req: Request, res: Response) => {
- logger.transport.log("Receiver new analysis request");
+ logger.transport.log("Received new analysis request");
const file = req.file;
let action = req.body as Analysis;
if (file) {
@@ -60,6 +65,20 @@ export const startHttpServer = ({ host, port }: HttpServerOptions) => {
}
});
+ app.post("/detect", async (req: Request, res: Response) => {
+ const { snippet } = req.body;
+
+ try {
+ const language = await detectLanguage(snippet);
+ language && res.json(language);
+ } catch (err) {
+ logger.transport.error(err.message);
+ res.status(500).send();
+ }
+
+ res.status(200).send();
+ });
+
app.listen(port, host, () => {
logger.transport.log(`HTTP server is running on ${host}:${port}`);
});
@@ -69,9 +88,3 @@ interface HttpServerOptions {
host: string;
port: number;
}
-
-const createFileUploadHandler = () => {
- const storage = multer.memoryStorage();
- const upload = multer({ storage: storage });
- return upload.single("file");
-};
diff --git a/packages/shared-types/src/analysis-types.ts b/packages/shared-types/src/analysis-types.ts
index 322ac4a..d65548b 100644
--- a/packages/shared-types/src/analysis-types.ts
+++ b/packages/shared-types/src/analysis-types.ts
@@ -1,3 +1,4 @@
+import { ProgrammingLanguage } from "./language-types";
import { OneOfValues } from "./typescript-types";
export interface Analysis {
@@ -18,6 +19,7 @@ export interface RepoAnalysis extends Analysis {
export interface SnippetAnalysis extends Analysis {
inputType: typeof AnalysisType.Snippet;
snippet: string;
+ language?: ProgrammingLanguage;
}
export interface FileAnalysis extends Analysis {
diff --git a/packages/shared-types/src/index.ts b/packages/shared-types/src/index.ts
index 6ced1db..0a34791 100644
--- a/packages/shared-types/src/index.ts
+++ b/packages/shared-types/src/index.ts
@@ -1,3 +1,4 @@
export * from "./analysis-types";
+export * from "./language-types";
export * from "./report-types";
export * from "./typescript-types";
diff --git a/packages/shared-types/src/language-types.ts b/packages/shared-types/src/language-types.ts
new file mode 100644
index 0000000..3d6daf8
--- /dev/null
+++ b/packages/shared-types/src/language-types.ts
@@ -0,0 +1,5 @@
+export interface ProgrammingLanguage {
+ id: string;
+ name?: string;
+ icon?: string;
+}
diff --git a/packages/shared-types/src/report-types.ts b/packages/shared-types/src/report-types.ts
index fc53d57..c5ec357 100644
--- a/packages/shared-types/src/report-types.ts
+++ b/packages/shared-types/src/report-types.ts
@@ -1,3 +1,4 @@
+import { ProgrammingLanguage } from ".";
import { AnalysisStatus } from "./analysis-types";
import { OneOfValues } from "./typescript-types";
@@ -11,10 +12,14 @@ export interface ReportState {
repoDetails?: RepoDetails;
fileDetails?: FileDetails;
score: number;
+ analysisError?: {
+ errorCode?: string;
+ errorMessage?: string;
+ errorDetails?: string;
+ };
+ language?: ProgrammingLanguage;
}
-
-
export interface RepoDetails {
languages: ReportLanguage[];
readmeUrl: string;