Skip to content

Instantly share code, notes, and snippets.

@drslump
Created October 28, 2013 02:53
Show Gist options
  • Save drslump/7190767 to your computer and use it in GitHub Desktop.
Save drslump/7190767 to your computer and use it in GitHub Desktop.
Proposal for a diagnostics system in the Boo compiler
"""
Diagnostics system based on the one from clang
http://clang.llvm.org/diagnostics.html
http://clang.llvm.org/docs/InternalsManual.html#the-diagnostics-subsystem
http://clang.llvm.org/doxygen/Diagnostic_8h_source.html
A DiagsEngine will be associated with the the CompilerContext and made
available to all the compiler components, including meta programming
extension points (macros, ast attributes, ..). The idea is that an
extensible compiler like Boo should be very user friendly when reporting
errors.
Best practices for creating diagnostics:
- Every diagnostic must have a unique numeric identifier.
- Keep the string short. It should ideally be less than 80 chars. This
avoids the diagnostic wrapping when printed, and forces you to think
about the important point you are conveying with the diagnostic.
- Take advantage of location information. The user will be able to see
the line and location of the caret, so you don’t need to tell them that
the problem is with the 4th argument to the function: just point to it.
- Do not capitalize the diagnostic string, and do not end it with a period.
- If you need to quote something in the diagnostic string, use single quotes.
- Produce Hints only when actually sure they will work as intended.
The message will be processed by String.Format so it can contain advanced
placeholder logic besides these custom formatters:
- Plurals: `{2:plural=none|one|some|||many||||a lot}` 1 = one, 3 = some, 1000 = a lot
- Select: `{1:sel=unary|binary}` -> 0 = unary, 1 = binary
- Ordinal: `{1:ord}` -> 2 = 2nd
- Or: `{1:or}` -> (this, that, another) = this, that or another
- And: `{1:and}` -> (this, that, another) = this, that and another
- Type: `{1:type}` -> node = FooBar
- Fully Qualified Name: `{1:fqn}` -> node = System.Console.WriteLine
"""
namespace Boo.Lang.Compiler.Diagnostics
import Boo.Lang.Compiler(CompilerContext, ICompilerInput)
import Boo.Lang.Compiler.Ast(LexicalInfo, SourceLocation, Node)
# The level of a diagnostic
enum DiagLevel:
Ignored
Note
Warning
Error
Fatal
# Delegate for the notification of new diagnostics
callable DiagnosticHandler(DiagLevel, Diagnostic)
class DiagsEngine:
"""
Main interface for the diagnostics system
"""
# Notify a new diagnostics session
event OnStartContext as callable(CompilerContext)
event OnStopContext as callable(CompilerContext)
# Notify a new file being analyzed
event OnStartFile as callable(ICompilerInput)
event OnStopFile as callable(ICompilerInput)
# Notify successfully consumed diagnostics
event Handler as DiagnosticHandler
property IgnoreAllWarnings as bool
property WarningsAsErrors as bool
property ErrorsAsFatal as bool
property SuppressAllDiagnostics as bool
property ErrorLimit as uint
property IgnoredCodes as (int)
[getter(FatalOcurred)]
_fatalOcurred = false
[getter(NoteCount)]
_noteCount = 0
[getter(WarningCount)]
_warningCount = 0
[getter(ErrorCount)]
_errorCount = 0
Count:
get: return ErrorCount + WarningCount + NoteCount + (1 if FatalOcurred else 0)
virtual protected def Map(diag as Diagnostic) as DiagLevel:
""" Maps a diagnostic to a normalized severity level based on the configuration
"""
if FatalOcurred or SuppressAllDiagnostics:
return DiagLevel.Ignored
level = diag.Level
if diag.Code in IgnoredCodes:
level = DiagLevel.Ignored
if WarningsAsErrors and level == DiagLevel.Warning:
level = DiagLevel.Error
if IgnoreAllWarnings and level == DiagLevel.Warning:
level = DiagLevel.Ignored
if ErrorsAsFatal and level == DiagLevel.Error:
level = DiagLevel.Fatal
if ErrorLimit != 0 and _errorCount >= ErrorLimit:
level = DiagLevel.Ignored
return level
virtual def Consume(diag as Diagnostic):
""" Consume a diagnostic produced by the compiler to notify
the configured handlers if needed.
"""
level = Map(diag)
if level == DiagLevel.Ignored:
return
if level == DiagLevel.Fatal:
_fatalOcurred = true
elif level == DiagLevel.Error:
_errorCount += 1
elif level == DiagLevel.Warning:
_warningCount += 1
elif level == DiagLevel.Note:
_noteCount += 1
if Handler:
Handler(level, diag)
class Hint:
"""
Represents a code modification:
- Insert: Count = 0, Insert = 'foo'
- Replace: Count = 5, Insert = 'foo'
- Remove: Count = 5, Insert = ''
"""
# Original position
property Caret as SourceLocation
# Number of characters to remove
property Count as int
# Text to insert at the caret
property Insert as string
class Range:
"""
Represents a segment of the source code
"""
property From as SourceLocation
property Until as SourceLocation
class NodeRange(Range):
def constructor(node as Node):
From = SourceLocation(node.LexicalInfo.Line, node.LexicalInfo.Column)
Until = SourceLocation(
node.LexicalInfo.Line,
node.LexicalInfo.Column + node.ToCodeString().Length
)
class Diagnostic:
"""
Represents a diagnostic. It can either be subclassed for common cases
or generated via factories to reduce repeated code.
"""
# Severity of the diagnostic
property Level as DiagLevel
# Unique code identifier for the diagnostic
property Code as int
# Reported message. It can use placeholders for Arguments
property Message as string
# Exact position where the diagnostic found the problem
property Caret as LexicalInfo
# Values to fill the placeholders in the message
property Arguments as List[of object]
# Signals important aspects of the original code (from, until)
property Ranges as List[of Range]
# Additional information about how to solve this specific problem
property Hints as List[of Hint]
class NodeDiagnostic(Diagnostic):
def constructor(node as Node):
Caret = node.LexicalInfo
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment