Plugins¶
Introduction¶
Nagelfar allows you to set up a list of plugins that can hook up and affect the checks in different stages.
A plugin can be used for things like:
Enforce local rules
Handle checks for custom commands that cannot be handled within Nagelfar’s syntax tokens
Annotate or ignore constructs in e.g. legacy code that cannot be changed
Collect statistics, e.g. a call graph
See further Examples below.
Generic rules¶
A plugin is a Tcl script file that must start with the verbatim sequence “##Nagelfar Plugin :”. A plugin is sourced and used in its own safe interpreter and thus have free access to its own global space. Hookup points are defined by declaring specifically named procedures as specified below, and apart from those, a plugin can define and do whatever within the limits of a safe interpreter.
In addition to the standard safe interpreter environment, a plugin has access to stdout as well.
Note that backslash-newline is always removed at an early stage in Nagelfar, so when hooks receive “unparsed” data, those have been removed.
Usage¶
To activate a plugin, use -plugin <plugin>
on the command line. Repeat
to load additional plugins in the specified order.
Nagelfar searches for plugins (looking for names <plugin> and <plugin>.tcl) in the following places:
. (Current directory)
./plugins
<nagelfar source>/../..
<nagelfar source>/../../plugins
<nagelfar source>/../plugins
-pluginpath <dir>
Option
Currently the plugin cannot be selected from the GUI.
Result of plugin procedures¶
Each hookup procedure returns a list with an even number of elements. These are interpreted as keyword-value pairs, with the following keywords allowed.
- replaceThe value is used to replace the incoming value for further processing.
If multiple plugins use replace only the last value is effective.
comment : The value is fed through inline comment parsing to affect surroundings.
error : The value produces an error message.
warning : The value produces a warning message.
note : The value produces a note message.
To do nothing, return an empty list.
Messages will be reported on the first line of whatever the hook gets to look at.
Information dictionary¶
Each hook procedure receives an information dictionary as one argument. It currently has at least these elements:
namespace : Current namespace
caller : Current procedure
file : Current file
firstpass : True during first scan of code. False during full check.
vars : Dictionary where each key is a known variable in current scope.
cmds : List of command names saved in the syntax database.
Hooks¶
Finalizing checking¶
proc finalizePlugin {} { }
If this procedure is declared, it is called at the end of checking.
The return value from finalizePlugin may only contain messages.
Raw Body Hook¶
proc bodyRaw {stmt info} { }
If declared, this receives each script body unparsed (backslash-newline removed). The info dictionary includes the element “subst” to indicate if the body is in a command substitution.
Raw Statement Hook¶
proc statementRaw {stmt info} { }
If declared, this receives each statement unparsed (backslash-newline removed).
Statement Words Hook¶
proc statementWords {words info} { }
If declared, this receives each statement split into a list of words but otherwise unprocessed/unsubstituted. Things like quotes and braces are left in the words.
Many checks can be done in a simple way here since you have direct access to the command word and the number of arguments.
Raw Expression Hook¶
proc earlyExpr {exp info} { }
If declared, this receives any expression unparsed.
Late Expression Hook¶
proc lateExpr {exp info} { }
If declared, this receives any expression after all variable or command substitutions have been replaced by “${_____}”. It is still basically the same expression and this allows a handler that knows fewer syntax rules.
Variable Write Hook¶
proc varWrite {var info} { }
If declared, this receives any variable written to.
Variable Read Hook¶
proc varRead {var info} { }
If declared, this receives any variable read from.
Raw Line Hook¶
proc lineRaw {line info} { }
If declared, this receives each line unaltered while a file is initially read.
In the info dictionary, namespace, caller, and firstpass are not relevant.
Unknown Command Hook¶
proc unknownCommand {cmd info} { }
If declared, this receives any encountered unknown command.
Write Header Hook¶
proc writeHeader {} {}
If declared, called when writing a file with -header. Every Inline-comment returned is appended to the file.
Read Inline Comments¶
proc syntaxComment {type opts} {}
If declared, receives any “##nagelfar <type> <opts…>” in the input. May return true to disable default action, e.g. if the type is plugin specific.
Examples¶
Call Graph¶
##Nagelfar Plugin : Create a call graph
proc statementWords {words info} {
set caller [dict get $info caller]
set callee [lindex $words 0]
if {$caller ne "" && $callee ne ""} {
array set ::callGraph [list "$caller -> $callee" 1]
}
return
}
proc finalizePlugin {} {
foreach item [lsort -dictionary [array names ::callGraph]] {
puts "Call: $item"
}
return
}
Ignore a command¶
##Nagelfar Plugin : Ignore mugg command
proc statementRaw {stmt info} {
set res {}
if {[string match "mugg *" $stmt]} {
lappend res replace {}
}
return $res
}
Handle known side effect¶
##Nagelfar Plugin : Handle known side effect
proc statementWords {words info} {
set res {}
# The command "mugg" sets a variable in the caller
if {[lindex $words 0] eq "mugg"} {
lappend res comment
lappend res "##nagelfar variable gurka"
}
return $res
}
Forbid operator¶
##Nagelfar Plugin : Forbid operator
proc lateExpr {exp info} {
if {[string match "* eq *" $exp]} {
return [list error "Operator \"eq\" is forbidden here"]
}
return {}
}
Check coding standard¶
##Nagelfar Plugin : Check coding rule
proc statementWords {words info} {
set res {}
# We require space around ! in if { ! []}
if {[lindex $words 0] eq "if"} {
set e [lindex $words 1]
if {[regexp {\{(\s*)!(\s*)\[} $e -> pre post]} {
if {$pre ne " " || $post ne " "} {
lappend res warning
lappend res "Not (!) should be surrounded by one space"
}
}
}
return $res
}
Allow custom operator¶
##Nagelfar Plugin : Allow custom operator
proc lateExpr {exp info} {
# Just replace it with something further processing recognizes
set exp [string map {{ my_cool_bin_op } { eq }} $exp]
return [list replace $exp]
}
Look for operator usage¶
##Nagelfar Plugin : Operator with string literal
# In the wake of TIP#461, help looking for things that can become a problem.
proc lateExpr {exp info} {
# Any comparison operator vs literal string give a note
# The regexp could be more precise of course.
if {[regexp {(!=|==|<|<=|>|>=)\s*\"} $exp -> op]} {
return [list note "Operator \"$op\" used with string literal"]
}
if {[regexp {\"\s*(!=|==|<|<=|>|>=)} $exp -> op]} {
return [list note "Operator \"$op\" used with string literal"]
}
return ""
}
Handle special syntax¶
##Nagelfar Plugin : Handle special syntax
proc statementWords {words info} {
set res {}
# We are only interested in calls to "mugg"
if {[lindex $words 0] ne "mugg"} {
return $res
}
# If a command has varying syntax depending on contents it can be handled,
# compare e.g. with a complex command like "if".
# In this example, only 1 or 5 arguments are allowed, which could
# also be expressed directly with the syntax string "1: x : 5"
lappend res comment
if {[llength $words] == 6} {
lappend res "##nagelfar syntax mugg x x x x x"
} else {
lappend res "##nagelfar syntax mugg x"
}
return $res
}
Check for unused globals¶
##Nagelfar Plugin : Check for unused globals
set ::data {}
proc statementWords {words info} {
if {[lindex $words 0] ne "global"} return
set caller [dict get $info caller]
foreach var [lrange $words 1 end] {
dict set ::data $caller $var 1
}
return
}
proc varWrite {var info} {
set caller [dict get $info caller]
dict unset ::data $caller $var
return
}
proc varRead {var info} {
set caller [dict get $info caller]
dict unset ::data $caller $var
return
}
proc finalizePlugin {} {
set res {}
foreach caller [dict keys $::data] {
foreach var [dict keys [dict get $::data $caller]] {
lappend res warning "Unused global '$var' in proc '$caller'"
}
}
lappend res note "Globals checked by plugin"
return $res
}
Sqlite code¶
In code like this, using the sqlite3 package:
db eval { SELECT rowid,name,start FROM SQLYSTUFF } {
list $rowid $name
}
db eval {UPDATE tasks SET user = $u, initial = 320 WHERE rowid = $g}
Nagelfar cannot know that rowid and name are existing variables and will give an error. A plugin can parse the SQL and provide this info.
Similarly Nagelfar does not know that the SQL code can contain variable references. Checking those can also be done.
##Nagelfar Plugin : Sqlite handler
proc statementWords {words info} {
# We are only interested in calls to "db eval <sql> ?<code>?"
if {[lindex $words 0] ne "db"} return
if {[lindex $words 1] ne "eval"} return
if {[llength $words] < 3} return
set sql [lindex $words 2]
set res {}
# Looking for variable reads
foreach {_ var} [regexp -all -inline {[$:](\w+)} $sql] {
if {![dict exists $info vars $var]} {
lappend res warning
lappend res "Unknown variable '$var'"
}
}
# Simple "parser" assuming a certain format to detect variables set
if {[llength $words] == 4} {
if {[regexp {SELECT (.*) FROM} [lindex $words 2] -> vars]} {
foreach var [regexp -all -inline {\w+} $vars] {
lappend res comment
lappend res "##nagelfar variable $var"
}
}
}
return $res
}
Namespace eval check¶
Detect creative writing in namespace eval code.
##Nagelfar Plugin : Namespace eval check
proc statementWords {words info} {
set caller [dict get $info caller]
# Code in proc is not interesting
if {$caller ne ""} return
set ns [dict get $info namespace]
# Global is not interesting
if {$ns eq "" || $ns eq "::"} return
set cmd [lindex $words 0]
if {$cmd eq "variable"} {
foreach {var _} [lindex $words 1 end] {
set ::known(${ns}::$var) 1
}
}
return
}
proc varWrite {var info} {
set caller [dict get $info caller]
# Code in proc is not interesting
if {$caller ne ""} return
set ns [dict get $info namespace]
# Global is not interesting
if {$ns eq "" || $ns eq "::"} return
if {![info exists ::known(${ns}::$var)]} {
return [list warning "Writing $var without variable call"]
}
}
Deprecation Notice¶
Add deprecation warning to proc, and save info to Header.
##Nagelfar Plugin : Deprecation Notice
set ::deprecated {}
proc syntaxComment {type opts} {
if {$type eq "deprecated"} {
lappend ::deprecated [lindex $opts 0]
return true
}
return false
}
proc statementWords {words info} {
if {[lindex $words 0] in $::deprecated} {
return [list warning "[lindex $words 0] is deprecated"]
}
return {}
}
proc writeHeader {} {
set res {}
foreach cmd [lsort -unique $::deprecated] {
lappend res "##nagelfar deprecated $cmd"
}
return $res
}
Detect proc overwrite¶
Warn if a procedure is defined twice.
##Nagelfar Plugin : Detect proc redefinition
proc statementWords {words info} {
set res {}
# Skip the first pass
if {[dict get $info firstpass]} {
return $res
}
# We are only interested in calls to "proc"
if {[lindex $words 0] ne "proc"} {
return $res
}
# Quick and dirty namespace resolve. Might need work.
set ns [dict get $info namespace]
set name [lindex $words 1]
if {[string match ::* $name]} {
set fullName $name
} else {
set fullName ${ns}::$name
}
if {[info exists ::seen($fullName)]} {
lappend res warning
lappend res "Redefined proc \"$name\""
} else {
set ::seen($fullName) [dict get $info file]
}
return $res
}
Handle unknown commands¶
If a command is unknown, suggest a similar command from the list of known commands.
##Nagelfar Plugin : Suggest possible fixes
proc unknownCommand {cmd info} {
set res {}
set cmds [dict get $info cmds]
set candidates [lsearch -glob -all -inline $cmds "*::$cmd"]
set candidates [lsort -dictionary $candidates]
if {[llength $candidates] == 0} {
return $res
}
set fixSuggestion \"[join $candidates "\", \""]\"
if {[llength $candidates] == 1} {
set msg "Perhaps you meant: $fixSuggestion"
} else { # [llength $candidates] > 1
set msg "Perhaps you meant one of: $fixSuggestion"
}
lappend res "warning"
lappend res "$msg"
return $res
}