source: trunk/base/src/port1.0/portlint.tcl @ 41511

Last change on this file since 41511 was 41511, checked in by blb@…, 12 years ago

lint command - add --nitpick option; ticket #14799

  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 14.6 KB
Line 
1# et:ts=4
2# portlint.tcl
3# $Id: portlint.tcl 41511 2008-11-05 02:50:05Z blb@macports.org $
4
5package provide portlint 1.0
6package require portutil 1.0
7
8set org.macports.lint [target_new org.macports.lint lint_main]
9target_runtype ${org.macports.lint} always
10target_state ${org.macports.lint} no
11target_provides ${org.macports.lint} lint
12target_requires ${org.macports.lint} main
13target_prerun ${org.macports.lint} lint_start
14
15set_ui_prefix
16
17set lint_portsystem \
18        "1.0"
19
20set lint_platforms [list \
21        "macosx" \
22        "darwin" \
23        "freebsd" \
24        "openbsd" \
25        "netbsd" \
26        "linux" \
27        "sunos" \
28        ]
29
30set lint_categories [list \
31        "aqua" \
32        "archivers" \
33        "audio" \
34        "benchmarks" \
35        "cad" \
36        "comms" \
37        "cross" \
38        "databases" \
39        "devel" \
40        "editors" \
41        "emulators" \
42        "erlang" \
43        "finance" \
44        "fuse" \
45        "games" \
46        "genealogy" \
47        "gis" \
48        "gnome" \
49        "gnustep" \
50        "graphics" \
51        "iphone" \
52        "irc" \
53        "java" \
54        "kde" \
55        "lang" \
56        "mail" \
57        "math" \
58        "multimedia" \
59        "net" \
60        "news" \
61        "office" \
62        "palm" \
63        "perl" \
64        "print" \
65        "python" \
66        "ruby" \
67        "science" \
68        "security" \
69        "shells" \
70        "sysutils" \
71        "tex" \
72        "textproc" \
73        "www" \
74        "x11" \
75        "xfce" \
76        "zope" \
77        ]
78
79set lint_required [list \
80        "name" \
81        "version" \
82        "description" \
83        "long_description" \
84        "categories" \
85        "maintainers" \
86        "platforms" \
87        "homepage" \
88        "master_sites" \
89        "checksums" \
90        ]
91
92set lint_optional [list \
93        "epoch" \
94        "revision" \
95        "worksrcdir" \
96        "distname" \
97        "use_automake" \
98        "use_autoconf" \
99        "use_configure" \
100        ]
101
102set lint_variants [list \
103        "universal" \
104        "docs" \
105        "aqua" \
106        "x11" \
107        ]
108
109
110proc seems_utf8 {str} {
111    set len [string length $str]
112    for {set i 0} {$i<$len} {incr i} {
113        set c [scan [string index $str $i] %c]
114        if {$c < 0x80} {
115            # ASCII
116            continue
117        } elseif {($c & 0xE0) == 0xC0} {
118            set n 1
119        } elseif {($c & 0xF0) == 0xE0} {
120            set n 2
121        } elseif {($c & 0xF8) == 0xF0} {
122            set n 3
123        } elseif {($c & 0xFC) == 0xF8} {
124            set n 4
125        } elseif {($c & 0xFE) == 0xFC} {
126            set n 5
127        } else {
128            return false
129        }
130        for {set j 0} {$j<$n} {incr j} {
131            incr i
132            if {$i == $len} {
133                return false
134            } elseif {([scan [string index $str $i] %c] & 0xC0) != 0x80} {
135                return false
136            }
137        }
138    }
139    return true
140}
141
142
143proc lint_start {args} {
144    global UI_PREFIX portname
145    ui_msg "$UI_PREFIX [format [msgcat::mc "Verifying Portfile for %s"] ${portname}]"
146}
147
148proc lint_main {args} {
149        global UI_PREFIX portname portpath portresourcepath ports_lint_nitpick
150        set portfile ${portpath}/Portfile
151        set portdirs [split ${portpath} /]
152        set last [llength $portdirs]
153        incr last -1
154        set portdir [lindex $portdirs $last]
155        incr last -1
156        set portcatdir [lindex $portdirs $last]
157        set groupdir ${portresourcepath}/group
158
159        set warnings 0
160        set errors 0
161
162    ###################################################################
163    ui_debug "$portfile"
164   
165    if {[info exists ports_lint_nitpick] && $ports_lint_nitpick eq "yes"} {
166        set nitpick true
167    } else {
168        set nitpick false
169    }
170
171    set topline_number 1
172    set require_blank false
173    set require_after ""
174    set seen_portsystem false
175    set seen_portgroup false
176    set in_description false
177
178    set local_variants [list]
179
180    set f [open $portfile RDONLY]
181    # read binary (to check UTF-8)
182    fconfigure $f -encoding binary
183    set lineno 1
184    while {1} {
185        set line [gets $f]
186        if {[eof $f]} {
187            if {$nitpick} {
188                seek $f -1 end
189                set last [read $f 1]
190                if {![string match "\n" $last]} {
191                    ui_warn "Line $lineno has missing newline (at end of file)"
192                    incr warnings
193                }
194            }
195            close $f
196            break
197        }
198        ui_debug "$lineno: $line"
199
200        if {![seems_utf8 $line]} {
201            ui_error "Line $lineno seems to contain an invalid UTF-8 sequence"
202            incr errors
203        }
204
205        if {[string equal "PortSystem" $require_after] && \
206            [string match "PortGroup*" $line]} {
207            set require_blank false
208        }
209
210        if {$nitpick && $require_blank && ($line != "")} {
211            ui_warn "Line $lineno should be a newline (after $require_after)"
212            incr warnings
213        }
214        set require_blank false
215
216        if {$nitpick && [regexp {\S[ \t]+$} $line]} {
217            # allow indented blank lines between blocks of code and such
218            ui_warn "Line $lineno has trailing whitespace before newline"
219            incr warnings
220        }
221
222        if {($lineno == $topline_number) && [string match "*-\*- *" $line]} {
223            ui_info "OK: Line $lineno has emacs/vim Mode"
224            incr topline_number
225        }
226        if {($lineno == $topline_number) && ![string match "*\$Id*\$" $line]} {
227            ui_warn "Line $lineno is missing RCS tag (\$Id\$)"
228            incr warnings
229        } elseif {($lineno == $topline_number)} {
230            ui_info "OK: Line $lineno has RCS tag (\$Id\$)"
231            set require_blank true
232            set require_after "RCS tag"
233        }
234
235        if {[string match "PortSystem*" $line]} {
236            if {$seen_portsystem} {
237                 ui_error "Line $lineno repeats PortSystem information"
238                 incr errors
239            }
240            regexp {PortSystem\s+([0-9.]+)} $line -> portsystem
241            if {![info exists portsystem]} {
242                 ui_error "Line $lineno has unrecognized PortSystem"
243                 incr errors
244            }
245            set seen_portsystem true
246            set require_blank true
247            set require_after "PortSystem"
248        }
249        if {[string match "PortGroup*" $line]} {
250            if {$seen_portgroup} {
251                 ui_error "Line $lineno repeats PortGroup information"
252                 incr errors
253            }
254            regexp {PortGroup\s+([a-z0-9]+)\s+([0-9.]+)} $line -> portgroup portgroupversion
255            if {![info exists portgroup]} {
256                 ui_error "Line $lineno has unrecognized PortGroup"
257                 incr errors
258            }
259            set seen_portgroup true
260            set require_blank true
261            set require_after "PortGroup"
262        }
263
264        # TODO: check for repeated variable definitions
265        # TODO: check the definition order of variables
266        # TODO: check length of description against max
267
268        if {[string match "long_description*" $line]} {
269            set in_description true
270        }
271        if {$in_description && ([string range $line end end] != "\\")} {
272            set in_description false
273            #set require_blank true
274            #set require_after "long_description"
275        } elseif {$in_description} {
276            set require_blank false
277        }
278
279        if {[string match "variant*" $line]} {
280            regexp {variant\s+(\w+)} $line -> variantname
281            if {[info exists variantname]} {
282                 lappend local_variants $variantname
283            }
284        }
285
286        if {[regexp {(^|\s)configure\s+\{\s*\}} $line]} {
287            ui_warn "Line $lineno should say \"use_configure no\" instead of declaring an empty configure phase"
288            incr warnings
289        }
290
291        ### TODO: more checks to Portfile syntax
292
293        incr lineno
294    }
295
296    ###################################################################
297
298    global os.platform os.arch os.version
299    global portversion portrevision portepoch
300    # hoping for "noarch" :
301    set portarch ${os.arch}
302    global description long_description platforms categories all_variants
303    global maintainers homepage master_sites checksums patchfiles
304    global depends_lib depends_build depends_run fetch.type
305   
306    global lint_portsystem lint_platforms lint_categories
307    global lint_required lint_optional lint_variants
308
309    if (!$seen_portsystem) {
310        ui_error "Didn't find PortSystem specification"
311        incr errors
312    }  elseif {$portsystem != $lint_portsystem} {
313        ui_error "Unknown PortSystem: $portsystem"
314        incr errors
315    } else {
316        ui_info "OK: Found PortSystem $portsystem"
317    }
318    if (!$seen_portgroup) {
319        # PortGroup is optional, so missing is OK
320    }  elseif {![file exists $groupdir/$portgroup-$portgroupversion.tcl]} {
321        ui_error "Unknown PortGroup: $portgroup-$portgroupversion"
322        incr errors
323    } else {
324        ui_info "OK: Found PortGroup $portgroup-$portgroupversion"
325    }
326
327    foreach req_var $lint_required {
328        if {$req_var == "name"} {
329            set var "portname"
330        } elseif {$req_var == "version"} {
331            set var "portversion"
332        } else {
333            set var $req_var
334        }
335
336       if {$var == "master_sites" && ${fetch.type} != "standard"} {
337             ui_info "OK: $var not required for fetch.type ${fetch.type}"
338             continue
339       }
340       
341       if {![info exists $var]} {
342            ui_error "Missing required variable: $req_var"
343            incr errors
344        } else {
345            ui_info "OK: Found required variable: $req_var"
346        }
347    }
348
349    foreach opt_var $lint_optional {
350       if {$opt_var == "epoch"} {
351            set var "portepoch"
352        } elseif {$opt_var == "revision"} {
353            set var "portrevision"
354        } else {
355            set var $opt_var
356       }
357       if {[info exists $var]} {
358            # TODO: check whether it was seen (or default)
359            ui_info "OK: Found optional variable: $opt_var"
360       }
361    }
362
363    if {[info exists platforms]} {
364        foreach platform $platforms {
365           if {[lsearch -exact $lint_platforms $platform] == -1} {
366                ui_error "Unknown platform: $platform"
367                incr errors
368            } else {
369                ui_info "OK: Found platform: $platform"
370            }
371        }
372    }
373
374    if {[info exists categories]} {
375        set category [lindex $categories 0]
376        if {[lsearch -exact $lint_categories $category] == -1} {
377            ui_error "Unknown category: $category"
378            incr errors
379        } else {
380            ui_info "OK: Found category: $category"
381        }
382        foreach secondary $categories {
383            if {[string match $secondary $category]} {
384                continue
385            }
386            ui_info "OK: Found category: $secondary"
387        }
388    }
389
390    if {![string is integer -strict $portepoch]} {
391        ui_error "Port epoch is not numeric:  $portepoch"
392        incr errors
393    }
394    if {![string is integer -strict $portrevision]} {
395        ui_error "Port revision is not numeric: $portrevision"
396        incr errors
397    }
398
399    set variantnumber 1
400    foreach variant $all_variants {
401        set variantname [ditem_key $variant name] 
402        set variantdesc [lindex [ditem_key $variant description] 0]
403        if {![info exists variantname] || $variantname == ""} {
404            ui_error "Variant number $variantnumber does not have a name"
405            incr errors
406        } else {
407            set name_ok true
408            set desc_ok true
409
410            if {![regexp {^[A-Za-z0-9_]+$} $variantname]} {
411                ui_error "Variant name $variantname is not valid; use \[A-Za-z0-9_\]+ only"
412                incr errors
413                set name_ok false
414            }
415
416            if {![info exists variantdesc] || $variantdesc == ""} {
417                # don't warn about missing descriptions for global variants
418                if {[lsearch -exact $local_variants $variantname] != -1 &&
419                    [lsearch -exact $lint_variants $variantname] == -1} {
420                    ui_warn "Variant $variantname does not have a description"
421                    incr warnings
422                    set desc_ok false
423                } elseif {$variantdesc == ""} {
424                    set variantdesc "(pre-defined variant)"
425                }
426            }
427
428            if {$name_ok} {
429                if {$desc_ok} {
430                    ui_info "OK: Found variant $variantname: $variantdesc"
431                } else {
432                    ui_info "OK: Found variant: $variantname"
433                }
434            }
435        }
436        incr variantnumber
437    }
438
439    set all_depends {}
440    if {[info exists depends_lib]} { eval "lappend all_depends $depends_lib" }
441    if {[info exists depends_build]} { eval "lappend all_depends $depends_build" }
442    if {[info exists depends_run]} { eval "lappend all_depends $depends_run" }
443    foreach depspec $all_depends {
444        set dep [lindex [split $depspec :] end]
445        if {[catch {set res [mport_search "^$dep\$"]} error]} {
446            global errorInfo
447            ui_debug "$errorInfo"
448            continue
449        }
450        if {$res == ""} {
451            ui_error "Unknown dependency: $dep"
452            incr errors
453        } else {
454            ui_info "OK: Found dependency: $dep"
455        }
456    }
457
458    if {[regexp "^(.+)nomaintainer(@macports.org)?(.+)$" $maintainers] } {
459        ui_error "Using nomaintainer together with other maintainer"
460        incr errors
461    }
462
463    if {[regexp "^openmaintainer(@macports.org)?$" $maintainers] } {
464        ui_error "Using openmaintainer without any other maintainer"
465        incr errors
466    }
467
468    if {[string match "*darwinports@opendarwin.org*" $maintainers]} {
469        ui_warn "Using legacy email address for no/open maintainer"
470        incr warnings
471    }
472
473    if {[string match "*nomaintainer@macports.org*" $maintainers] ||
474        [string match "*openmaintainer@macports.org*" $maintainers]} {
475        ui_warn "Using full email address for no/open maintainer"
476        incr warnings
477    }
478
479    # these checks are only valid for ports stored in the regular tree directories
480    if {$portcatdir != $category} {
481        ui_error "Portfile parent directory $portcatdir does not match primary category $category"
482        incr errors
483    } else {
484        ui_info "OK: Portfile parent directory matches primary category"
485    }
486    if {$portdir != $portname} {
487        ui_error "Portfile directory $portdir does not match port name $portname"
488        incr errors
489    } else {
490        ui_info "OK: Portfile directory matches port name"
491    }
492
493    if {$nitpick && [info exists patchfiles]} {
494        foreach patchfile $patchfiles {
495            if {![string match "patch-*.diff" $patchfile] && [file exists "$portpath/files/$patchfile"]} {
496                ui_warn "Patchfile $patchfile does not follow the source patch naming policy \"patch-*.diff\""
497                incr warnings
498            }
499        }
500    }
501
502    ### TODO: more checks to Tcl variables/sections
503
504    ui_debug "Name: $portname"
505    ui_debug "Epoch: $portepoch"
506    ui_debug "Version: $portversion"
507    ui_debug "Revision: $portrevision"
508    ui_debug "Arch: $portarch"
509    ###################################################################
510
511        ui_msg "$UI_PREFIX [format [msgcat::mc "%d errors and %d warnings found."] $errors $warnings]"
512
513        return {$errors > 0}
514}
Note: See TracBrowser for help on using the repository browser.