Explorar o código

Merge pull request #27819 from overleaf/mj-human-readable-logs-typescript

[web] Convert logs parsing to typescript

GitOrigin-RevId: 7a338740db50c8a3a0b70dd2212083f17348d4f1
Mathias Jakobsen hai 1 ano
pai
achega
44f4c835c8

+ 15 - 10
services/web/frontend/js/ide/human-readable-logs/HumanReadableLogs.js → services/web/frontend/js/ide/human-readable-logs/HumanReadableLogs.ts

@@ -1,14 +1,17 @@
-import LatexLogParser from '../log-parser/latex-log-parser'
+import LatexLogParser, {
+  LatexParserOptions,
+  ParseResult,
+} from '../log-parser/latex-log-parser'
 import ruleset from './HumanReadableLogsRules'
 import ruleset from './HumanReadableLogsRules'
 
 
 export default {
 export default {
-  parse(rawLog, options) {
+  parse(rawLog: string | ParseResult, options: LatexParserOptions) {
     const parsedLogEntries =
     const parsedLogEntries =
       typeof rawLog === 'string'
       typeof rawLog === 'string'
         ? new LatexLogParser(rawLog, options).parse()
         ? new LatexLogParser(rawLog, options).parse()
         : rawLog
         : rawLog
 
 
-    const seenErrorTypes = {} // keep track of types of errors seen
+    const seenErrorTypes: Record<string, boolean> = {} // keep track of types of errors seen
 
 
     for (const entry of parsedLogEntries.all) {
     for (const entry of parsedLogEntries.all) {
       const ruleDetails = ruleset.find(rule =>
       const ruleDetails = ruleset.find(rule =>
@@ -28,9 +31,11 @@ export default {
         }
         }
 
 
         if (ruleDetails.contentRegex) {
         if (ruleDetails.contentRegex) {
-          const match = entry.content.match(ruleDetails.contentRegex)
-          if (match) {
-            entry.contentDetails = match.slice(1)
+          if (entry.content != null) {
+            const match = entry.content.match(ruleDetails.contentRegex)
+            if (match) {
+              entry.contentDetails = match.slice(1)
+            }
           }
           }
         }
         }
 
 
@@ -74,14 +79,14 @@ export default {
     }
     }
 
 
     // filter out the suppressed errors (from the array entries in parsedLogEntries)
     // filter out the suppressed errors (from the array entries in parsedLogEntries)
-    for (const [key, errors] of Object.entries(parsedLogEntries)) {
-      if (typeof errors === 'object' && errors.length > 0) {
-        parsedLogEntries[key] = Array.from(errors).filter(
+    for (const type of ['errors', 'warnings', 'typesetting'] as const) {
+      const errors = parsedLogEntries[type]
+      if (Array.isArray(errors) && errors.length > 0) {
+        parsedLogEntries[type] = Array.from(errors).filter(
           err => !err.suppressed
           err => !err.suppressed
         )
         )
       }
       }
     }
     }
-
     return parsedLogEntries
     return parsedLogEntries
   },
   },
 }
 }

+ 40 - 39
services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsHints.tsx

@@ -172,26 +172,28 @@ const hints: { [ruleId: string]: LogHint } = {
   },
   },
   hint_undefined_control_sequence: {
   hint_undefined_control_sequence: {
     formattedContent: details => {
     formattedContent: details => {
-      if (details?.length && packageSuggestionsForCommands.has(details[0])) {
+      if (details?.length) {
         const command = details[0]
         const command = details[0]
         const suggestion = packageSuggestionsForCommands.get(command)
         const suggestion = packageSuggestionsForCommands.get(command)
-        return (
-          <>
-            <p>
-              We think you’ve got a missing package! The <code>{command}</code>{' '}
-              command won't work unless you include
-              <code>{suggestion.command}</code> in your{' '}
-              <WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
-                document preamble
-              </WikiLink>
-              .{' '}
-              <WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
-                Learn more about packages
-              </WikiLink>
-              .
-            </p>
-          </>
-        )
+        if (suggestion) {
+          return (
+            <>
+              <p>
+                We think you’ve got a missing package! The{' '}
+                <code>{command}</code> command won't work unless you include
+                <code>{suggestion.command}</code> in your{' '}
+                <WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
+                  document preamble
+                </WikiLink>
+                .{' '}
+                <WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
+                  Learn more about packages
+                </WikiLink>
+                .
+              </p>
+            </>
+          )
+        }
       }
       }
       return (
       return (
         <>
         <>
@@ -215,29 +217,28 @@ const hints: { [ruleId: string]: LogHint } = {
   },
   },
   hint_undefined_environment: {
   hint_undefined_environment: {
     formattedContent: details => {
     formattedContent: details => {
-      if (
-        details?.length &&
-        packageSuggestionsForEnvironments.has(details[0])
-      ) {
+      if (details?.length) {
         const environment = details[0]
         const environment = details[0]
         const suggestion = packageSuggestionsForEnvironments.get(environment)
         const suggestion = packageSuggestionsForEnvironments.get(environment)
-        return (
-          <>
-            <p>
-              We think you’ve got a missing package! The{' '}
-              <code>{environment}</code> environment won't work unless you
-              include <code>{suggestion.command}</code> in your{' '}
-              <WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
-                document preamble
-              </WikiLink>
-              .{' '}
-              <WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
-                Learn more about packages
-              </WikiLink>
-              .
-            </p>
-          </>
-        )
+        if (suggestion) {
+          return (
+            <>
+              <p>
+                We think you’ve got a missing package! The{' '}
+                <code>{environment}</code> environment won't work unless you
+                include <code>{suggestion.command}</code> in your{' '}
+                <WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#The_preamble_of_a_document">
+                  document preamble
+                </WikiLink>
+                .{' '}
+                <WikiLink url="https://www.overleaf.com/learn/latex/Learn_LaTeX_in_30_minutes#Finding_and_using_LaTeX_packages">
+                  Learn more about packages
+                </WikiLink>
+                .
+              </p>
+            </>
+          )
+        }
       }
       }
       return (
       return (
         <>
         <>

+ 4 - 2
services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsPackageSuggestions.js → services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsPackageSuggestions.ts

@@ -1,4 +1,6 @@
-const commandSuggestions = [
+type Suggestion = [string, { name: string; command: string }]
+
+const commandSuggestions: Suggestion[] = [
   [
   [
     '\\includegraphics',
     '\\includegraphics',
     { name: 'graphicx', command: '\\usepackage{graphicx}' },
     { name: 'graphicx', command: '\\usepackage{graphicx}' },
@@ -30,7 +32,7 @@ const commandSuggestions = [
   ['\\arraybackslash', { name: 'array', command: '\\usepackage{array}' }],
   ['\\arraybackslash', { name: 'array', command: '\\usepackage{array}' }],
 ]
 ]
 
 
-const environmentSuggestions = [
+const environmentSuggestions: Suggestion[] = [
   ['justify', { name: 'ragged2e', command: '\\usepackage{ragged2e}' }],
   ['justify', { name: 'ragged2e', command: '\\usepackage{ragged2e}' }],
   ['align', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
   ['align', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
   ['align*', { name: 'amsmath', command: '\\usepackage{amsmath}' }],
   ['align*', { name: 'amsmath', command: '\\usepackage{amsmath}' }],

+ 15 - 12
services/web/frontend/js/ide/human-readable-logs/HumanReadableLogsRules.tsx

@@ -13,7 +13,7 @@ interface Rule {
   contentRegex?: RegExp
   contentRegex?: RegExp
   improvedTitle?: (
   improvedTitle?: (
     currentTitle: string,
     currentTitle: string,
-    details?: [string]
+    details?: string[]
   ) => string | [string, JSX.Element]
   ) => string | [string, JSX.Element]
   package?: string
   package?: string
   highlightCommand?: (contentDetails: string[]) => string | undefined
   highlightCommand?: (contentDetails: string[]) => string | undefined
@@ -65,10 +65,13 @@ const rules: Rule[] = [
     regexToMatch: /Undefined control sequence/,
     regexToMatch: /Undefined control sequence/,
     // Match the last control sequence in the line
     // Match the last control sequence in the line
     contentRegex: /^[^\n]*(\\\S+)\s*[\n]/,
     contentRegex: /^[^\n]*(\\\S+)\s*[\n]/,
-    improvedTitle: (currentTitle: string, details?: [string]) => {
-      if (details?.length && packageSuggestionsForCommands.has(details[0])) {
-        const command = details[0]
-        const suggestion = packageSuggestionsForCommands.get(command)
+    improvedTitle: (currentTitle: string, details?: string[]) => {
+      if (!details?.length) {
+        return currentTitle
+      }
+      const command = details[0]
+      const suggestion = packageSuggestionsForCommands.get(command)
+      if (suggestion) {
         return [
         return [
           `Is ${suggestion.command} missing?`,
           `Is ${suggestion.command} missing?`,
           // eslint-disable-next-line react/jsx-key
           // eslint-disable-next-line react/jsx-key
@@ -87,13 +90,13 @@ const rules: Rule[] = [
     ruleId: 'hint_undefined_environment',
     ruleId: 'hint_undefined_environment',
     regexToMatch: /LaTeX Error: Environment .+ undefined/,
     regexToMatch: /LaTeX Error: Environment .+ undefined/,
     contentRegex: /\\begin\{(\S+)\}/,
     contentRegex: /\\begin\{(\S+)\}/,
-    improvedTitle: (currentTitle: string, details?: [string]) => {
-      if (
-        details?.length &&
-        packageSuggestionsForEnvironments.has(details[0])
-      ) {
-        const environment = details[0]
-        const suggestion = packageSuggestionsForEnvironments.get(environment)
+    improvedTitle: (currentTitle: string, details?: string[]) => {
+      if (!details?.length) {
+        return currentTitle
+      }
+      const environment = details[0]
+      const suggestion = packageSuggestionsForEnvironments.get(environment)
+      if (suggestion) {
         return [
         return [
           `Is ${suggestion.command} missing?`,
           `Is ${suggestion.command} missing?`,
           // eslint-disable-next-line react/jsx-key
           // eslint-disable-next-line react/jsx-key

+ 37 - 9
services/web/frontend/js/ide/log-parser/bib-log-parser.js → services/web/frontend/js/ide/log-parser/bib-log-parser.ts

@@ -18,12 +18,21 @@ const MESSAGE_LEVELS = {
   ERROR: 'error',
   ERROR: 'error',
 }
 }
 
 
-const parserReducer = function (maxErrors) {
-  return function (accumulator, parser) {
-    const consume = function (logText, regex, process) {
+type Parser<T> = [RegExp, (match: RegExpExecArray) => T]
+
+const parserReducer = function (maxErrors: number | null) {
+  return function <T>(
+    accumulator: [T[], string],
+    parser: Parser<T>
+  ): [T[], string] {
+    const consume = function (
+      logText: string,
+      regex: RegExp,
+      process: (match: RegExpExecArray) => T
+    ): [T[], string] {
       let match
       let match
       let text = logText
       let text = logText
-      const result = []
+      const result: T[] = []
       let iterationCount = 0
       let iterationCount = 0
 
 
       while ((match = regex.exec(text))) {
       while ((match = regex.exec(text))) {
@@ -55,8 +64,22 @@ const parserReducer = function (maxErrors) {
   }
   }
 }
 }
 
 
+type BibLogEntry = {
+  file: string
+  level: string
+  message: string
+  line: string
+  raw: string
+}
+
 export default class BibLogParser {
 export default class BibLogParser {
-  constructor(text, options = {}) {
+  text: string
+  options: Record<string, any>
+  lines: string[]
+  warningParsers: Parser<BibLogEntry>[]
+  errorParsers: Parser<BibLogEntry>[]
+
+  constructor(text: string, options = {}) {
     if (typeof text !== 'string') {
     if (typeof text !== 'string') {
       throw new Error('BibLogParser Error: text parameter must be a string')
       throw new Error('BibLogParser Error: text parameter must be a string')
     }
     }
@@ -156,11 +179,11 @@ export default class BibLogParser {
     // reduce over the parsers, starting with the log text,
     // reduce over the parsers, starting with the log text,
     const [allWarnings, remainingText] = this.warningParsers.reduce(
     const [allWarnings, remainingText] = this.warningParsers.reduce(
       parserReducer(this.options.maxErrors),
       parserReducer(this.options.maxErrors),
-      [[], this.text]
+      [[] as BibLogEntry[], this.text]
     )
     )
     const [allErrors] = this.errorParsers.reduce(
     const [allErrors] = this.errorParsers.reduce(
       parserReducer(this.options.maxErrors),
       parserReducer(this.options.maxErrors),
-      [[], remainingText]
+      [[] as BibLogEntry[], remainingText]
     )
     )
 
 
     return {
     return {
@@ -173,7 +196,10 @@ export default class BibLogParser {
   }
   }
 
 
   parseBiber() {
   parseBiber() {
-    const result = {
+    const result: Record<
+      'all' | 'errors' | 'warnings' | 'files' | 'typesetting',
+      BibLogEntry[]
+    > = {
       all: [],
       all: [],
       errors: [],
       errors: [],
       warnings: [],
       warnings: [],
@@ -186,7 +212,9 @@ export default class BibLogParser {
         const [fullLine, , messageType, message] = match
         const [fullLine, , messageType, message] = match
         const newEntry = {
         const newEntry = {
           file: '',
           file: '',
-          level: MESSAGE_LEVELS[messageType] || 'INFO',
+          level:
+            MESSAGE_LEVELS[messageType as keyof typeof MESSAGE_LEVELS] ||
+            'INFO',
           message,
           message,
           line: '',
           line: '',
           raw: fullLine,
           raw: fullLine,

+ 85 - 13
services/web/frontend/js/ide/log-parser/latex-log-parser.js → services/web/frontend/js/ide/log-parser/latex-log-parser.ts

@@ -14,8 +14,54 @@ const STATE = {
   ERROR: 1,
   ERROR: 1,
 }
 }
 
 
+export type LatexParserOptions = {
+  fileBaseNames?: RegExp[]
+  ignoreDuplicates?: boolean
+}
+
+type LatexLogEntry = {
+  line: string | number | null
+  file: string | undefined
+  level: 'error' | 'warning' | 'typesetting'
+  message: string
+  content?: string
+  raw: string
+  ruleId?: string
+  contentDetails?: string[]
+  command?: string
+  suppressed?: boolean
+}
+
+type File = {
+  path: string
+  files: File[]
+}
+
+export type ParseResult = {
+  all: LatexLogEntry[]
+  errors: LatexLogEntry[]
+  warnings: LatexLogEntry[]
+  typesetting: LatexLogEntry[]
+  files: File[]
+}
 export default class LatexParser {
 export default class LatexParser {
-  constructor(text, options = {}) {
+  state: number
+  fileBaseNames: RegExp[]
+  ignoreDuplicates?: boolean
+  data: LatexLogEntry[]
+  fileStack: File[]
+  rootFileList: File[]
+  currentFileList: File[]
+  openParens: number
+  latexWarningRegex: RegExp
+  packageWarningRegex: RegExp
+  packageRegex: RegExp
+  log: LogText
+  currentLine: string
+  currentFilePath: string | undefined
+  currentError: LatexLogEntry | undefined
+
+  constructor(text: string, options: LatexParserOptions = {}) {
     this.state = STATE.NORMAL
     this.state = STATE.NORMAL
     this.fileBaseNames = options.fileBaseNames || [/compiles/, /\/usr\/local/]
     this.fileBaseNames = options.fileBaseNames || [/compiles/, /\/usr\/local/]
     this.ignoreDuplicates = options.ignoreDuplicates
     this.ignoreDuplicates = options.ignoreDuplicates
@@ -26,11 +72,14 @@ export default class LatexParser {
     this.latexWarningRegex = LATEX_WARNING_REGEX
     this.latexWarningRegex = LATEX_WARNING_REGEX
     this.packageWarningRegex = PACKAGE_WARNING_REGEX
     this.packageWarningRegex = PACKAGE_WARNING_REGEX
     this.packageRegex = PACKAGE_REGEX
     this.packageRegex = PACKAGE_REGEX
+    this.currentLine = ''
     this.log = new LogText(text)
     this.log = new LogText(text)
   }
   }
 
 
-  parse() {
-    while ((this.currentLine = this.log.nextLine()) !== false) {
+  parse(): ParseResult {
+    let nextLine: string | false
+    while ((nextLine = this.log.nextLine()) !== false) {
+      this.currentLine = nextLine
       if (this.state === STATE.NORMAL) {
       if (this.state === STATE.NORMAL) {
         if (this.currentLineIsError()) {
         if (this.currentLineIsError()) {
           this.state = STATE.ERROR
           this.state = STATE.ERROR
@@ -58,6 +107,9 @@ export default class LatexParser {
         }
         }
       }
       }
       if (this.state === STATE.ERROR) {
       if (this.state === STATE.ERROR) {
+        if (!this.currentError) {
+          throw new Error('LatexParser Error: currentError is undefined')
+        }
         this.currentError.content += this.log
         this.currentError.content += this.log
           .linesUpToNextMatchingLine(/^l\.[0-9]+/)
           .linesUpToNextMatchingLine(/^l\.[0-9]+/)
           .join('\n')
           .join('\n')
@@ -111,6 +163,9 @@ export default class LatexParser {
 
 
   parseFileLineError() {
   parseFileLineError() {
     const result = this.currentLine.match(FILE_LINE_ERROR_REGEX)
     const result = this.currentLine.match(FILE_LINE_ERROR_REGEX)
+    if (!result) {
+      throw new Error('LatexParser Error: Unable to extract error from line.')
+    }
     this.currentError = {
     this.currentError = {
       line: result[2],
       line: result[2],
       file: result[1],
       file: result[1],
@@ -145,7 +200,7 @@ export default class LatexParser {
     return this.data.push(this.currentError)
     return this.data.push(this.currentError)
   }
   }
 
 
-  parseSingleWarningLine(prefixRegex) {
+  parseSingleWarningLine(prefixRegex: RegExp) {
     const warningMatch = this.currentLine.match(prefixRegex)
     const warningMatch = this.currentLine.match(prefixRegex)
     if (!warningMatch) {
     if (!warningMatch) {
       return
       return
@@ -173,6 +228,11 @@ export default class LatexParser {
     let lineMatch = this.currentLine.match(LINES_REGEX)
     let lineMatch = this.currentLine.match(LINES_REGEX)
     let line = lineMatch ? parseInt(lineMatch[1], 10) : null
     let line = lineMatch ? parseInt(lineMatch[1], 10) : null
     const packageMatch = this.currentLine.match(this.packageRegex)
     const packageMatch = this.currentLine.match(this.packageRegex)
+    if (!packageMatch) {
+      throw new Error(
+        'LatexParser Error: Unable to extract package name from warning.'
+      )
+    }
     const packageName = packageMatch[1]
     const packageName = packageMatch[1]
     // Regex to get rid of the unnecesary (packagename) prefix in most multi-line warnings
     // Regex to get rid of the unnecesary (packagename) prefix in most multi-line warnings
     const prefixRegex = new RegExp(
     const prefixRegex = new RegExp(
@@ -180,10 +240,15 @@ export default class LatexParser {
       'i'
       'i'
     )
     )
     // After every warning message there's a blank line, let's use it
     // After every warning message there's a blank line, let's use it
-    while ((this.currentLine = this.log.nextLine())) {
+    let currentLine: string | false
+    while ((currentLine = this.log.nextLine())) {
+      this.currentLine = currentLine
       lineMatch = this.currentLine.match(LINES_REGEX)
       lineMatch = this.currentLine.match(LINES_REGEX)
       line = lineMatch ? parseInt(lineMatch[1], 10) : line
       line = lineMatch ? parseInt(lineMatch[1], 10) : line
       warningMatch = this.currentLine.match(prefixRegex)
       warningMatch = this.currentLine.match(prefixRegex)
+      if (!warningMatch) {
+        throw new Error('LatexParser Error: Unable to extract warning message.')
+      }
       warningLines.push(warningMatch[1])
       warningLines.push(warningMatch[1])
     }
     }
     const rawMessage = warningLines.join(' ')
     const rawMessage = warningLines.join(' ')
@@ -291,16 +356,19 @@ export default class LatexParser {
     return path
     return path
   }
   }
 
 
-  postProcess(data) {
-    const all = []
-    const errorsByLevel = {
+  postProcess(data: LatexLogEntry[]) {
+    const all: LatexLogEntry[] = []
+    const errorsByLevel: Record<
+      'error' | 'warning' | 'typesetting',
+      LatexLogEntry[]
+    > = {
       error: [],
       error: [],
       warning: [],
       warning: [],
       typesetting: [],
       typesetting: [],
     }
     }
     const hashes = new Set()
     const hashes = new Set()
 
 
-    const hashEntry = entry => entry.raw
+    const hashEntry = (entry: LatexLogEntry) => entry.raw
 
 
     data.forEach(item => {
     data.forEach(item => {
       const hash = hashEntry(item)
       const hash = hashEntry(item)
@@ -326,7 +394,11 @@ export default class LatexParser {
 }
 }
 
 
 class LogText {
 class LogText {
-  constructor(text) {
+  text: string
+  lines: string[]
+  row: number
+
+  constructor(text: string) {
     this.text = text.replace(/(\r\n)|\r/g, '\n')
     this.text = text.replace(/(\r\n)|\r/g, '\n')
     // Join any lines which look like they have wrapped.
     // Join any lines which look like they have wrapped.
     const wrappedLines = this.text.split('\n')
     const wrappedLines = this.text.split('\n')
@@ -355,7 +427,7 @@ class LogText {
     this.row = 0
     this.row = 0
   }
   }
 
 
-  nextLine() {
+  nextLine(): string | false {
     this.row++
     this.row++
     if (this.row >= this.lines.length) {
     if (this.row >= this.lines.length) {
       return false
       return false
@@ -368,11 +440,11 @@ class LogText {
     this.row--
     this.row--
   }
   }
 
 
-  linesUpToNextWhitespaceLine(stopAtError) {
+  linesUpToNextWhitespaceLine(stopAtError: boolean = false) {
     return this.linesUpToNextMatchingLine(/^ *$/, stopAtError)
     return this.linesUpToNextMatchingLine(/^ *$/, stopAtError)
   }
   }
 
 
-  linesUpToNextMatchingLine(match, stopAtError) {
+  linesUpToNextMatchingLine(match: RegExp, stopAtError: boolean = false) {
     const lines = []
     const lines = []
 
 
     while (true) {
     while (true) {