;# $Id: lint.pl,v 3.0.1.10 1997/02/28 16:31:53 ram Exp $
;#
;#  Copyright (c) 1991-1993, Raphael Manfredi
;#  
;#  You may redistribute only under the terms of the Artistic Licence,
;#  as specified in the README file that comes with the distribution.
;#  You may reuse parts of this distribution only within the terms of
;#  that same Artistic Licence; a copy of which may be found at the root
;#  of the source tree for dist 3.0.
;#
;# $Log: lint.pl,v $
;# Revision 3.0.1.10  1997/02/28  16:31:53  ram
;# patch61: added support for ?F: lines to monitor file usage
;# patch61: now honours "create" and "empty" lint directives
;#
;# Revision 3.0.1.9  1995/09/25  09:19:15  ram
;# patch59: new ?Y: directive to change unit layout
;#
;# Revision 3.0.1.8  1995/07/25  14:19:47  ram
;# patch56: will now check : comments line for potential danger
;#
;# Revision 3.0.1.7  1994/10/29  16:36:14  ram
;# patch36: now extensively checks created files thanks to new ?F: lines
;#
;# Revision 3.0.1.6  1994/05/13  15:29:09  ram
;# patch27: now understands macro definitions in ?H: lines
;#
;# Revision 3.0.1.5  1994/05/06  15:27:48  ram
;# patch23: now warns for units ending with non-blank line
;# patch23: warn for units where last line is not new-line terminated
;#
;# Revision 3.0.1.4  1994/01/24  14:28:40  ram
;# patch16: now knows about "internal use only" variables on ?MAKE: lines
;# patch16: now suppress "read-only var set" message when change hint
;#
;# Revision 3.0.1.3  1993/11/10  17:39:39  ram
;# patch14: now spots stale ?M: dependencies
;#
;# Revision 3.0.1.2  1993/10/16  13:55:26  ram
;# patch12: now checks ?M: lines also
;#
;# Revision 3.0.1.1  1993/08/25  14:03:40  ram
;# patch6: now correctly signals conditional dependencies with no default
;#
;# Revision 3.0  1993/08/18  12:10:25  ram
;# Baseline for dist 3.0 netwide release.
;#
;# The list of all available units is held in @ARGV. We shall parse them and
;# extract the dependencies. A lot of global data structures are filled in
;# during this phase.
;#
# Initialize the extraction process by setting some variables.
# We return a string to be eval'ed to do more customized initializations.
sub init_extraction {
	$c_symbol = '';				# Current symbol seen in ?C: lines
	$s_symbol = '';				# Current symbol seen in ?S: lines
	$m_symbol = '';				# Current symbol seen in ?M: lines
	$last_interpreted = 0;		# True when last line was an '@' one
	%csym = ();					# C symbols described
	%ssym = ();					# Shell symbols described
	%hcsym = ();				# C symbols used by ?H: lines
	%hssym = ();				# Shell symbols used by ?H: lines
	%msym = ();					# Magic symbols defined by ?M: lines
	%mdep = ();					# C symbol dependencies introduced by ?M:
	%symset = ();				# Records all the shell symbol set
	%symused = ();				# Records all the shell symbol used
	%tempseen = ();				# Temporary shell variable seen
	%fileseen = ();				# Produced files seen
	%fileused = ();				# Files used, by unit (private UU files)
	%filemisused = ();			# Files not used as ./file or ...UU/file
	%filetmp = ();				# Local temporary files in ?F: directives
	%filesetin = ();			# Lists units defining a temporary file
	%filecreated = ();			# Records files created in this unit
	%prodfile = ();				# Unit where a given file is said to be created
	%defseen = ();				# Symbol defintions claimed
	%lintseen = ();				# Symbols declared known by a ?LINT: line
	%lintchange = ();			# Symbols declared changed by a ?LINT: line
	%lintuse = ();				# Symbols declared used by unit
	%lintextern = ();			# Symbols known to be externally defined
	%lintcreated = ();			# Files declared as created by a ?LINT: line
	%condsym = ();				# Records all the conditional symbols
	%condseen = ();				# Records conditional dependencies
	%depseen = ();				# Records full dependencies
	%shvisible = ();			# Records units making a symbol visible
	%shspecial = ();			# Records special units listed as wanted
	%shdepend = ();				# Records units listed in one's dependency list
	%shmaster = ();				# List of units defining a shell symbol
	%cmaster = ();				# List of units defining a C symbol
	%symdep = ();				# Records units where symbol is a dependency
	@make = ();					# Records make dependency lines
	$body = 'p_body';			# Procedure to handle body
	$ending = 'p_end';			# Called at the end of each unit
}

# End the extraction process
sub end_extraction {
}

# Process the ?MAKE: line
sub p_make {
	local($_) = @_;
	local(@ary);					# Locally defined symbols
	local(@dep);					# Dependencies
	local($where) = "\"$file\", line $. (?MAKE:)";
	return unless /^[\w+ ]*:/;		# We only want the main dependency rule
	warn "$where: ignoring duplicate dependency listing line.\n"
		if $makeseen{$unit}++;
	return if $makeseen{$unit} > 1;
	undef %condseen;				# Reset those once for every unit
	undef %depseen;					# (assuming there is only one depend line)
	undef %defseen;
	undef %tempseen;
	undef %symset;
	undef %symused;
	undef %csym;
	undef %ssym;
	undef %hcsym;
	undef %hssym;
	undef %lintseen;
	undef %lintchange;
	undef %lintextern;
	undef %lintcreated;
	undef %fileseen;
	undef %filetmp;
	undef %filecreated;
	s|^\s*||;						# Remove leading spaces
	chop;
	s/:(.*)//;
	@dep = split(' ', $1);			# Dependencies
	@ary = split(' ');				# Locally defined symbols
	local($nowarn);					# True when +Special is seen
	foreach $sym (@ary) {
		# Ignore "internal use only" symbols as far as metalint goes.
		# Actually, we record the presence of a '+' in front of a special
		# unit name and use that as a hint to suppress the presence of that
		# special unit in the defined symbol section.
		$nowarn = ($sym =~ s/^\+//);

		# We record for each shell symbol the list of units which claim to make
		# it, so as to report duplicates.
		if ($sym =~ /^[_a-z]/ || $Except{$sym}) {
			$shmaster{"\$$sym"} .= "$unit ";
			++$defseen{$sym};
		} else {
			warn "$where: special unit '$sym' should not be listed as made.\n"
				unless $sym eq $unit || $nowarn;
		}
	}
	# Record dependencies for later perusal
	push(@make, join(' ', @ary) . ':' . join(' ', @dep));
	foreach $sym (@dep) {
		if ($sym =~ /^\+[_A-Za-z]/) {
			$sym =~ s|^\+||;
			++$condseen{$sym};		# Conditional symbol wanted
			++$condsym{$sym};		# %condsym has a greater lifetime
		} else {
			++$depseen{$sym};		# Full dependency
		}

		# Each 'wanted' special unit (i.e. one starting with a capital letter)
		# is remembered, so as to prevent exported symbols from being reported
		# as "undefined". For instance, Myread exports $dflt, $ans and $rp.
		$shspecial{$unit} .= "$sym " if substr($sym, 0, 1) =~ /^[A-Z]/;

		# Record all known dependencies (special or not) for this unit
		$shdepend{$unit} .= "$sym ";

		# Remember where wanted symbol is defined, so that we can report
		# stale dependencies later on (i.e. dependencies which refer to non-
		# existent symbols).
		$symdep{$sym} .= "$unit ";	# This symbol is wanted here
	}
	# Make sure we do not want a symbol twice, nor do we want it once as a full
	# dependency and once as a conditional dependency.
	foreach $sym (@dep) {
		if ($sym =~ /^\+[_A-Za-z]/) {
			$sym =~ s|^\+||;
			warn "$where: '+$sym' is listed $condseen{$sym} times.\n"
				if $condseen{$sym} > 1;
			$condseen{$sym} = 1 if $condseen{$sym};	# Avoid multiple messages
		} else {
			warn "$where: '$sym' is listed $depseen{$sym} times.\n"
				if $depseen{$sym} > 1;
			$depseen{$sym} = 1 if $depseen{$sym};	# Avoid multiple messages
		}
		warn "$where: '$sym' listed as both conditional and full dependency.\n"
			if $condseen{$sym} && $depseen{$sym};
	}
	# Make sure every unit "inherits" from the symbols exported by 'Init'.
	$shspecial{$unit} .= 'Init ' unless $shspecial{$unit} =~ /Init\b/;
}

# Process the ?O: line
sub p_obsolete {
	local($_) = @_;
	chop;
	$Obsolete{"$unit.U"} = $_;		# Message to print if unit is used
}

# Process the ?S: lines
sub p_shell {
	local($_) = @_;
	local($where) = "\"$file\", line $. (?S:)";
	if (/^(\w+)\s*(\(.*\))*\s*:/) {
		&check_last_declaration;
		$s_symbol = $1;
		print "  ?S: $s_symbol\n" if $opt_d;
		# Make sure we do not define symbol twice and that the symbol is indeed
		# listed in the ?MAKE: line.
		warn "$where: duplicate description for variable '\$$s_symbol'.\n"
			if $ssym{$s_symbol}++;
		warn "$where: variable '\$$s_symbol' is not listed on ?MAKE: line.\n"
			unless $defseen{$s_symbol} || $lintseen{$s_symbol};
		# Deal with obsolete symbol list (enclosed between parenthesis)
		&record_obsolete("\$$_") if /\(/;
	} else {
		unless ($s_symbol) {
			warn "$where: syntax error in ?S: construct.\n";
			return;
		}
	}

	m|^\.\s*$| && ($s_symbol = '');		# End of comment
}

# Process the ?C: lines
sub p_c {
	local($_) = @_;
	local($where) = "\"$file\", line $. (?C:)";
	if (s/^(\w+)\s*~\s*(\S+)\s*(.*):/$1 $3:/) {
		&check_last_declaration;
		$c_symbol = $2;					# Alias for definition in config.h
		# Record symbol definition for further duplicate spotting
		$cmaster{$1} .= "$unit " unless $csym{$1};
		print "  ?C: $1 ~ $c_symbol\n" if $opt_d;
		# Make sure we do not define symbol twice
		warn "$where: duplicate description for symbol '$1'.\n"
			if $csym{$1}++;
		# Deal with obsolete symbol list (enclosed between parenthesis)
		&record_obsolete("$_") if /\(/;
	} elsif (/^(\w+)\s*(\(.*\))*\s*:/) {
		&check_last_declaration;
		$c_symbol = $1;
		# Record symbol definition for further duplicate spotting
		$cmaster{$c_symbol} .= "$unit " unless $csym{$c_symbol};
		print "  ?C: $c_symbol\n" if $opt_d;
		# Make sure we do not define symbol twice
		warn "$where: duplicate description for symbol '$c_symbol'.\n"
			if $csym{$c_symbol}++;
		# Deal with obsolete symbol list (enclosed between parenthesis)
		&record_obsolete("$_") if /\(/;
	} else {
		unless ($c_symbol) {
			warn "$where: syntax error in ?C: construct.\n";
			return;
		}
	}

	s|^(\w+)|?$c_symbol:/* $1| ||							# Start of comment
	(s|^\.\s*$|?$c_symbol: */\n| && ($c_symbol = '', 1)) ||	# End of comment
	s|^(.*)|?$c_symbol: *$1|;								# Middle of comment
	&p_config("$_");					# Add comments to config.h.SH
}

# Process the ?H: lines
sub p_config {
	local($_) = @_;
	local($where) = "\"$file\", line $. (?H)" unless $where;
	s/^\?(\w+)://;						# Remove leading '?var:'
	return unless /^#/;					# Look only for cpp lines
	if (m|^#\$(\w+)\s+(\w+).*\$(\w+)|) {
		# Case: #$d_var VAR "$var"
		warn "$where: symbol '$2' was already defined.\n" if $hcsym{$2}++;
		&check_definition("$1");
		&check_definition("$3");
	} elsif (m|^#define\s+(\w+)\((.*)\)\s+\$(\w+)|) {
		# Case: #define VAR(x) $var
		warn "$where: symbol '$1' was already defined.\n" if $hcsym{$1}++;
		&check_definition("$3");
	} elsif (m|^#\$define\s+(\w+)|) {
		# Case: #$define VAR
		warn "$where: symbol '$1' was already defined.\n" if $hcsym{$1}++;
	} elsif (m|^#\$(\w+)\s+(\w+)|) {
		# Case: #$d_var VAR
		warn "$where: symbol '$2' was already defined.\n" if $hcsym{$2}++;
		&check_definition("$1");
	} elsif (m|^#define\s+(\w+).*\$(\w+)|) {
		# Case: #define VAR "$var"
		warn "$where: symbol '$1' was already defined.\n" if $hcsym{$1}++;
		&check_definition("$2");
	} elsif (m|^#define\s+(\w+)|) {
		# Case: #define VAR
		$hcsym{$1}++;			# Multiple occurrences may be legitimate
	}
}

# Process the ?M: lines
sub p_magic {
	local($_) = @_;
	local($where) = "\"$file\", line $. (?M)";
	if (/^(\w+):\s*([\w\s]*)\n$/) {
		&check_last_declaration;
		$m_symbol = $1;
		$msym{$1} = "$unit";	# p_wanted ensure we do not define symbol twice
		$mdep{$1} = $2;			# Save C symbol dependencies
		&p_wanted("$unit:$m_symbol");
	} else {
		unless ($m_symbol) {
			warn "$where: syntax error in ?M: construct.\n";
			return;
		}
	}
	m|^\.\s*$| && ($m_symbol = '');		# End of comment
}

# Process the ?INIT: lines
sub p_init {
	local($where) = "\"$file\", line $. (?INIT)";
	&p_body;		# Pass it along as a body line (leading ?INIT: removed)
}

# Process the ?D: lines
sub p_default {
	local($_) = @_;
	local($where) = "\"$file\", line $. (?D)";
	local($sym) = /^(\w+)=/;
	$hasdefault{$sym}++;
	&p_body;		# Pass it along as a body line (leading ?D: removed)
}

# Process the ?V: lines
sub p_visible {
	# A visible symbol can freely be manipulated by any unit which includes the
	# current unit in its dependencies. Symbols before ':' may be only used for
	# reading while symbols after ':' may be used for both reading and writing.
	# The array %shvisible records symbols as keys. Read-only symbols have a
	# leading '$' while read-write symbols are recorded as-is.
	local($where) = "\"$file\", line $. (?V)";
	unless (substr($unit, 0, 1) =~ /^[A-Z]/) {
		warn "$where: visible declaration in non-special unit ignored.\n";
		return;
	}
	local($read_only) = $_[0] =~ /^([^:]*):?/;
	local($read_write) = $_[0] =~ /:(.*)/;
	local(@rsym) = split(' ', $read_only);
	local(@rwsym) = split(' ', $read_write);
	local($w);
	foreach (@rsym) {		# Read only symbols
		warn "$where: wanted variable '\$$_' made visible.\n" if &wanted($_);
		warn "$where: defined variable '\$$_' made visible.\n"
			if &defined($_) && !$lintseen{$_};
		$w = $shvisible{"\$$_"};
		warn "$where: variable '\$$_' already made visible by unit $w.\n" if $w;
		$w = $shvisible{$_};
		warn "$where: variable '\$$_' already read-write visible in $w.\n" if $w;
		$shvisible{"\$$_"} = $unit unless $w;
	}
	foreach (@rwsym) {		# Read/write symbols
		warn "$where: wanted variable '\$$_' made visible.\n" if &wanted($_);
		warn "$where: defined variable '\$$_' made visible.\n"
			if &defined($_) && !$lintseen{$_};
		$w = $shvisible{$_};
		warn "$where: variable '\$$_' already made visible by unit $w.\n" if $w;
		$w = $shvisible{"\$$_"};
		warn "$where: variable '\$$_' already read-only visible in $w.\n" if $w;
		$shvisible{$_} = $unit unless $w;
	}
}

# Process the ?W: lines
sub p_wanted {
	# Somehow, we should check that none of the symbols to activate are stale
	# ones, i.e. they all finally resolve to some known target -- FIXME
	local($active) = $_[0] =~ /^([^:]*):/;		# Symbols to activate
	local($look_symbols) = $_[0] =~ /:(.*)/;	# When those are used
	local(@symbols) = split(' ', $look_symbols);
	local($where) = "\"$file\", line $. (?W)" unless $where;
	# A "?W:symbol" line asks metaconfig to define 'symbol' in the wanted file
	# as a C target iff that word is found within the sources. This is mainly
	# intended for the built-in interpreter to check for definedness.
	local($w);
	foreach (@symbols) {
		warn "$where: variable '\$$_' already wanted.\n" if &wanted($_);
		warn "$where: variable '\$$_' also locally defined.\n" if &defined($_);
		$w = $cwanted{$_};
		if ($msym{$_} ne '') {
			warn "$where: symbol '$_' already listed on a ?M: line in '$w'.\n"
				if $w;
		} else {
		warn "$where: variable '\$$_' already listed on a ?W: line in '$w'.\n"
			if $w;
		}
		$cwanted{$_} = $unit unless $w;
	}
}

# Process the ?Y: lines
sub p_layout {
	local($_) = @_;
	chop;
	local($where) = "\"$file\", line $. (?Y)";
	s/^\s+//;
	tr/A-Z/a-z/;			# Layouts are record in lowercase
	warn "$where: unknown layout directive '$_'.\n"
		unless defined $Lcmp{$_};
}

# Process the ?P: lines
sub p_public {
	# FIXME
}

# Process the ?L: lines
sub p_library {
	# There should not be any '-l' in front of the library name
	# FIXME
}

# Process the ?I: lines
sub p_include {
	# FIXME
}

# Process the ?T: lines
sub p_temp {
	local($_) = @_;
	local(@sym) = split(' ', $_);
	local($where) = "\"$file\", line $. (?T:)";
	foreach $sym (@sym) {
		warn "$where: temporary symbol '\$$sym' multiply declared.\n"
			if $tempseen{$sym}++ == 1;
		$tempmaster{$sym} .= "$unit " if $tempseen{$sym} == 1;
	}
}

# Process the ?F: lines
sub p_file {
	local($_) = @_;
	local(@files) = split(' ', $_);
	local($where) = "\"$file\", line $. (?F:)";
	local($uufile);					# Name of file produced in the UU directory
	local($tmpfile);				# Name of a temporary file
	# We care only about UU files, i.e. files produced in the UU directory
	# and which are identified by the convention ./filename. Files !filename
	# are not produced, i.e. they are temporary or externally provided.
	# The %prodfile table records all the files produced, so we may detect
	# inconsistencies between units, while %filemaster records the (first) unit
	# defining a given UU file to make sure that (special) unit is named in the
	# dependency line when that UU file. Duplicates will be caught in the
	# sanity check phase thanks to %prodfile.
	# Temporary files are recorded in %filesetin, so that we may later compare
	# the list with the UU files to detect possible overwrites.
	foreach $file (@files) {
		warn "$where: produced file '$file' multiply declared.\n"
			if $fileseen{$file}++ == 1;
		if (($tmpfile = $file) =~ s/^!//) {
			$filetmp{$tmpfile}++;
			$filesetin{$tmpfile} .= "$unit " if $fileseen{$file} == 1;
			next;					# Is not a UU file for sure, so skip
		}
		$prodfile{$file} .= "$unit " if $fileseen{$file} == 1;
		($uufile = $file) =~ s|^\./(\S+)$|$1|;
		next if $file eq $uufile;	# Don't care about non-UU files
		unless (substr($unit, 0, 1) =~ /^[A-Z]/ || $lintcreated{$uufile}) {
			warn "$where: UU file '$uufile' in non-special unit ignored.\n";
			next;
		}
		$filemaster{$uufile} = $unit unless defined $filemaster{$uufile};
		$filecreated{$uufile} = 'a';	# Will be automagically incremented
	}
}

# Process the ?LINT: lines
sub p_lint {
	local($_) = @_;
	local(@sym);
	s/^\s+//;						# Strip leading spaces
	if (s/^set//) {					# Listed variables are set
		@sym = split(' ', $_);
		foreach (@sym) {
			$symset{$_}++;			# Shell variable set
		}
	} elsif (s/^desc\w+//) {		# Listed shell variables are described
		@sym = split(' ', $_);
		foreach (@sym) {
			$ssym{$_}++;			# Shell variable described
		}
	} elsif (s/^creat\w+//) {		# Listed created files in regular units
		@sym = split(' ', $_);
		foreach (@sym) {
			$lintcreated{$_}++;		# Persistent UU file created
		}
	} elsif (s/^known//) {			# Listed C variables are described
		@sym = split(' ', $_);
		foreach (@sym) {
			$csym{$_}++;			# C symbol described
		}
	} elsif (s/^change//) {			# Shell variable ok to be changed
		@sym = split(' ', $_);
		foreach (@sym) {
			$lintchange{$_}++;		# Do not complain if changed
		}
	} elsif (s/^extern//) {			# Variables known to be externally defined
		@sym = split(' ', $_);
		foreach (@sym) {
			$lintextern{$_}++;		# Do not complain if used in a ?H: line
		}
	} elsif (s/^use//) {			# Variables declared as used by unit
		@sym = split(' ', $_);
		foreach (@sym) {
			$lintuse{$_}++;			# Do not complain if on ?MAKE and not used
		}
	} elsif (s/^def\w+//) {			# Listed variables are defined
		@sym = split(' ', $_);
		foreach (@sym) {
			$lintseen{$_}++;		# Shell variable defined in this unit
		}
	} elsif (m/^empty/) {			# Empty unit file
		$lintempty{$unit}++;
	} else {
		local($where) = "\"$file\", line $." unless $where;
		local($word) = /^(\w+)/;
		warn "$where: unknown LINT request '$word' ignored.\n";
	}
}

# Process the body of the unit
sub p_body {
	return unless $makeseen{$unit};
	local($_) = @_;
	local($where) = "\"$file\", line $." unless $where;
	# Ensure there is no control line in the body of the unit
	local($control) = /^\?([\w\-]+):/;
	local($known) = $control ? $Depend{$control} : "";
	warn "$where: control sequence '?$control:' ignored within body.\n"
		if $known && !/^\?X:|^\?LINT:/;
	if (s/^\?LINT://) {				# ?LINT directives allowed within body
		$_ .= &complete_line(FILE) if s/\\\s*$//;
		&p_lint($_);
	}
	return if $known;
	# Ingnore interpreted lines and their continuations
	if ($last_interpreted) {
		return if /\\$/;			# Still part of the interpreted line
		$last_interpreted = 0;		# End of interpreted lines
		return;						# This line was the last interpreted
	}
	# Look for interpreted lines and ignore them
	if (/^@/) {
		$last_interpreted = /\\$/;	# Set flag if line is continued
		return;						# And skip this line
	}
	s/^\s+//;						# Remove leading spaces
	# Detect shell ':' "comment" lines, and perform sanity checks on them...
	# Also spot any of '<>|&;' since those will have their shell behaviour
	# behaviour
	if (s/^:\s+//) {
		s/<\$?\w+>//g;				# Remove valid <$var> escapes or old <VAR>
		warn "$where: meaningful shell character '$1' in comment line.\n"
			while s/([<>&\|;]+)//g;
		require 'shellwords.pl';
		eval { &shellwords($_) };
		return unless $@;			# Ignore comment, no quoting problem
		local($what) = '';
		$what = 'double' if $@ =~ /\bdouble\b/;
		$what = 'single' if $@ =~ /\bsingle\b/;
		warn "$where: unmatched $what quote in comment line.\n" if $what;
		warn "$where: $@" unless $what;
		return;
	}

	# From now on, do all substitutes with ':' since it would be dangerous
	# to remove things plain and simple. It could yields false matches
	# afterwards...

	# Record any attempt made to set a shell variable
	local($sym);
	while (s/(\w+)=/:/) {
		$sym = $1;
		next if $sym =~ /^\d+/;		# Ignore $1 and friends
		$symset{$sym}++;			# Shell variable set
		# Not part of a $cc -DWHATEVER line and not made nor temporary
		unless ($sym =~ /^D/ || &defined($sym)) {
			if (&wanted($sym)) {
				warn "$where: variable '\$$sym' is changed.\n"
					unless $lintchange{$sym};
			} else {
				# Record that the variable is set but not listed locally.
				$shset{$unit} .= "$sym " unless
					$shset{$unit} =~ /\b$sym\b/ || $lintchange{$sym};
			}
		}
	}
	# Now look at the shell variables used: can be $var or ${var}
	local($var);
	local($line) = $_;
	while (s/\$\{?(\w+)\}?/:/) {
		$var = $1;
		next if $var =~ /^\d+/;		# Ignore $1 and friends
		# Record variable as undeclared but do not issue a message right now.
		# That variable could be exported via ?V: (as $dflt in Myread) or be
		# defined by a special unit (like $inlibc by unit Inlibc).
		$shunknown{$unit} .= "$var " unless
			$lintextern{$var} || &declared($var) ||
			$shunknown{$unit} =~ /\b$var\b/;
		$shused{$unit} .= "\$$var " unless $shused{$unit} =~ /\$$var\b/;
	}
	# Now look at private files used by the unit (./file or ..../UU/file)
	# We look at things like '. ./myread' and `./loc ...` only.
	local($file);
	$_ = $line;
	while (
		s!(\.\s+|`\s*)(\S*UU|\.)/([^\$/`\s;]+)\s*!! ||
		s!(`\s*\$?)cat\s+(\./)?([^\$/`\s;]+)\s*`!! ||
		s!if(\s+)(\./)([^\$/`\s;]+)\s*!!
	) {
		$file = $3;
		# Found some ". ./file" or `./file` execution, `$cat file`, "if prog"...
		# Record file as used. Later on, we will make sure we had the right
		# to use that file: either we are in the unit that defines it, or we
		# include the unit that creates it in our dependencies, relying on ?F:.
		$fileused{$unit} .= "$file " unless
			$filetmp{$file} || $fileused{$unit} =~ /\b$file\b/;
		# Mark temporary file as being used, to spot useless local declarations
		$filetmp{$file} .= ' used'
			if defined $filetmp{$file} && $filetmp{$file} !~ /\bused/;
	}
	# Try to detect things like . myread or `loc` to warn that they
	# should rather use . ./myread and `./loc`. Also things like 'if prog',
	# or usage in conditional expressions such as || and &&. Be sure the file
	# name is always in $2...
	while (
		s!(\.\s+|`\s*)([^\$/`\s;]+)\s*!:!	||	# . myread or `loc`
		s!(if|\|\||&&)\s+([^\$/`\s;]+)\s*!:!	# if prog, || prog, && prog
	) {
		$file = $2;
		$filemisused{$unit} .= "$file " unless
			$filetmp{$file} || $filemisused{$unit} =~ /\b$file\b/;
		# Temporary files should be used with ./ anyway
		$filetmp{$file} .= ' misused'
			if defined $filetmp{$file} && $filetmp{$file} !~ /\bmisused/;
	}
	# Locate file creation, >>file or >file
	while (s!>>?\s*([^\$/`\s;]+)\s*!:!) {
		$file = $1;
		next if $file =~ /&\d+/;	# skip >&4 and friends
		$filecreated{$file}++;
	}
}

# Called at the end of each unit
sub p_end {
	local($last) = @_;				# Last processed line
	local($where) = "\"$file\"";
	unless ($makeseen{$unit}) {
		warn "$where: no ?MAKE: line describing dependencies.\n"
			unless $lintempty{$unit};
		return;
	}

	# Each unit should end with a blank line. Unfortunately, some units
	# may also end with an '@end' request and have the blank line above it.
	# Currently, we do not have enough information to correctly diagnose
	# whether it is valid or not so just skip it.
	# Same thing for U/Obsol_sh.U which ends with a shell comment.

	warn "$where: not ending with a blank line.\n" unless
		$last =~ /^\s*$/ || $last =~ /^\@end/ || $last =~ /^#|\?/;

	# For EMACS users. It would be fatal to the Configure script...
	warn "$where: last line not ending with a new-line character.\n"
		unless $last =~ /\n$/;

	# Make sure every shell symbol described in ?MAKE had a description
	foreach $sym (sort keys %defseen) {
		warn "$where: symbol '\$$sym' was not described.\n"
			unless $ssym{$sym};
	}
	# Ensure all the C symbols defined by ?H: lines have a description
	foreach $sym (sort keys %hcsym) {
		warn "$where: C symbol '$sym' was not described.\n"
			unless $csym{$sym};
	}
	# Ensure all the C symbols described by ?C: lines are defined in ?H:
	foreach $sym (sort keys %csym) {
		warn "$where: C symbol '$sym' was not defined by any ?H: line.\n"
			unless $hcsym{$sym};
	}
	# Make sure each defined symbol was set, unless it starts with an
	# upper-case letter in which case it is not a "true" shell symbol.
	# I don't care about the special symbols defined in %Except as I know
	# they are handled correctly.
	foreach $sym (sort keys %defseen) {
		warn "$where: variable '\$$sym' should have been set.\n"
			unless $symset{$sym} || substr($sym, 0, 1) =~ /^[A-Z]/;
	}
	# Make sure every non-special unit declared as wanted is indeed needed
	foreach $sym (sort keys %depseen) {
		warn "$where: unused dependency variable '\$$sym'.\n" unless
			$shused{$unit} =~ /\$$sym\b/ || substr($sym, 0, 1) =~ /^[A-Z]/ ||
			$lintchange{$sym} || $lintuse{$sym};
	}
	# Idem for conditionally wanted symbols
	foreach $sym (sort keys %condseen) {
		warn "$where: unused conditional variable '\$$sym'.\n" unless
			$shused{$unit} =~ /\$$sym\b/ || substr($sym, 0, 1) =~ /^[A-Z]/ ||
			$lintchange{$sym} || $lintuse{$sym};
	}
	# Idem for temporary symbols
	foreach $sym (sort keys %tempseen) {
		warn "$where: unused temporary variable '\$$sym'.\n" unless
			$shused{$unit} =~ /\$$sym\b/ || $symset{$sym} || $lintuse{$sym};
	}
	# Idem for local files
	foreach $file (sort keys %filetmp) {
		warn "$where: mis-used temporary file '$file'.\n" if
			$filetmp{$file} =~ /\bmisused/;
		warn "$where: unused temporary file '$file'.\n" unless
			$filetmp{$file} =~ /\bused/ || $filetmp{$file} =~ /\bmisused/;
	}
	# Make sure each private file listed as created on ?F: is really created.
	# When found, a private UU file is entered in the %filecreated array
	# with value 'a'. Each time a file creation occurs in the unit, an
	# increment is done on that value. Since 'a'++ -> 'b', a numeric value
	# in %filecreated means a non-local file, which is skipped. An 'a' means
	# the file was not created...
	local($value);
	foreach $file (sort keys %filecreated) {
		$value = $filecreated{$file};
		next if $value > 0;		# Skip non UU-files.
		warn "$where: file '$file' was not created.\n" if $value eq 'a';
	}
}

# An unknown control line sequence was found (held in $proc)
sub p_unknown {
	warn "\"$file\", line $.: unknown control sequence '?$proc:'.\n";
}

# Run sanity checks, to make sure every conditional symbol has a suitable
# default value. Also ensure every symbol was defined once.
sub sanity_checks {
	print "Sanity checks...\n";
	local($key, $value);
	local($w);
	local(%message);		# Record messages on a per-unit basis
	local(%said);			# Avoid duplicate messages
	# Warn about symbols ever used in conditional dependency with no default
	while (($key, $value) = each(%condsym)) {
		unless ($hasdefault{$key}) {
			$w = (split(' ', $shmaster{"\$$key"}))[0];
			$message{$w} .= "#$key ";
		}
	}
	# Warn about any undeclared variables. They are all listed in %shunknown,
	# being the values while the unit where they appear is the key. If the
	# symbol is defined by any of the special units included or made visible,
	# then no warning is issued.
	local($defined);		# True if symbol is defined in one unit
	local($where);			# List of units where symbol is defined
	local($myself);			# The name of the current unit if itself special
	local($visible);		# Symbol made visible via a ?V: line
	foreach $unit (sort keys %shunknown) {
		foreach $sym (split(' ', $shunknown{$unit})) {
			$defined = 0;
			$where = $shmaster{"\$$sym"};
			$defined = 1 if $tempmaster{"\$$sym"} =~ /$unit\b/;
			$myself = substr($unit, 0, 1) =~ /^[A-Z]/ ? $unit : '';
			# Symbol has to be either defined within one of the special units
			# listed in the dependencies or exported via a ?V: line.
			unless ($defined) {
				$defined = &visible($sym, $unit);
				$spneeded{$unit}++ if $defined;
			}
			$message{$unit} .= "\$$sym " unless $defined;
		}
	}

	# Warn about any undeclared files. Files used in one unit are all within
	# the %fileused table, indexed by unit. If a file is used, it must either
	# be in the unit that declared it (relying on %filemaster for that) or
	# the unit listed in %filemaster must be part of our dependency.
	%said = ();
	foreach $unit (sort keys %fileused) {
		foreach $file (split(' ', $fileused{$unit})) {
			$defined = 0;
			$where = $filemaster{$file};		# Where file is created
			$defined = 1 if $unit eq $where;	# We're in the unit defining it
			# Private UU files may be only be created by special units
			foreach $special (split(' ', $shspecial{$unit})) {
				last if $defined;
				$defined = 1 if $where eq $special;
			}
			# Exceptions to above rule possible via a ?LINT:create hint,
			# so parse all known dependencies for the unit...
			foreach $depend (split(' ', $shdepend{$unit})) {
				last if $defined;
				$defined = 1 if $where eq $depend;
			}
			$message{$unit} .= "\@$file " unless
				$defined || $said{"$unit/$file"}++;	# Unknown file
		}
	}
	undef %fileused;

	# Warn about any misused files, kept in %filemisused
	foreach $unit (sort keys %filemisused) {
		foreach $file (split(' ', $filemisused{$unit})) {
			next unless defined $filemaster{$file};	# Skip non UU-files
			$message{$unit} .= "\@\@$file ";		# Misused file
		}
	}
	undef %filemisused;

	# Warn about temporary files which could be created and inadvertently
	# override a private UU file (listed in %filemaster).
	foreach $tmpfile (keys %filesetin) {
		next unless defined $filemaster{$tmpfile};
		$where = $filemaster{$tmpfile};
		foreach $unit (split(' ', $filesetin{$tmpfile})) {
			$message{$unit} .= "\@\@\@$where:$tmpfile ";
		}
	}
	undef %filesetin;

	# Warn about any set variable which was not listed.
	foreach $unit (sort keys %shset) {
		symbol: foreach $sym (split(' ', $shset{$unit})) {
			next if $shvisible{$sym};
			$defined = 0;
			# Symbol has to be either defined within one of the special units
			# listed in the dependencies or exported read-write via a ?V: line.
			# If symbol is exported read-only, report the attempt to set it.
			$where = $shmaster{"\$$sym"};
			study $where;
			foreach $special (split(' ', $shspecial{$unit})) {
				$defined = 1 if $where =~ /\b$special\b/;
				last if $defined;
			}
			$visible = 0;
			$defined = $visible = &visible($sym, $unit) unless $defined;
			if ($visible && $shvisible{"\$$sym"} ne '') {
				# We are allowed to set a read-only symbol in the unit which
				# declared it...
				next symbol if $shvisible{"\$$sym"} eq $unit;
				$message{$unit} .= "\&$sym ";	# Read-only symbol set
				next symbol;
			}
			$message{$unit} .= "$sym " unless $defined;
		}
	}
	# Warn about any obsolete variable which may be used
	foreach $unit (sort keys %shused) {
		foreach $sym (split(' ', $shused{$unit})) {
			$message{$unit} .= "!$sym " if $Obsolete{$sym} ne '';
		}
	}

	# Warn about stale dependencies, and prepare successor and predecessor
	# tables for later topological sort.

	local($targets, $deps);
	local(%Succ);				# Successors
	local(%Prec);				# Predecessors

	# Split dependencies and build successors array.
	foreach $make (@make) {
		($targets, $deps) = $make =~ m|(.*):\s*(.*)|;
		$deps =~ s/\+(\w)/$1/g;		# Remove conditional targets
		foreach $target (split(' ', $targets)) {
			$Succ{$target} .= $deps . ' ';
		}
	}

	# Special setup for the End target, which normally has a $W dependency for
	# wanted symbols. In order to detect all the possible cycles, we forge a
	# huge dependency by making ALL the regular symbols (i.e. those whose first
	# letter is not uppercased) wanted.

	local($allwant) = '';
	{
		local($sym, $val);
		while (($sym, $val) = each %shmaster) {
			$sym =~ s/^\$//;
			$allwant .= "$sym " if $val ne '';
		}
	}

	$Succ{'End'} =~ s/\$W/$allwant/;

	# Initialize precursors, and spot symbols impossible to 'make', i.e. those
	# symbols listed in the successors and with no 'make' target. The data
	# structures %Prec and %Succ will also be used by the cycle lookup code,
	# in other words, the topological sort.
	foreach $target (keys %Succ) {
		$Prec{$target} += 0;	# Ensure key is recorded without disturbing.
		foreach $succ (split(' ', $Succ{$target})) {
			$Prec{$succ}++;		# Successor has one more precursor
			unless (defined $Succ{$succ} || $said{$succ}++) {
				foreach $unit (split(' ', $symdep{$succ})) {
					$message{$unit} .= "?$succ ";	# Stale ?MAKE: dependency
				}
			}
		}
	}
	undef %symdep;

	# Check all ?M: dependencies to spot stale ones
	%said = ();
	while (($key, $value) = each(%msym)) {
		next if $value eq '';	# Value is unit name where ?M: occurred
		foreach $sym (split(' ', $mdep{$key})) {	# Loop on C dependencies
			next if $cmaster{$sym} || $said{$sym};
			$message{$value} .= "??$sym ";	# Stale ?M: dependency
			$said{$sym}++;
		}
	}

	undef %said;
	undef %mdep;
	undef %msym;

	# Now actually emit all the warnings
	local($uv);			# Unit defining visible symbol or private file
	local($w);			# Were we are signaling an error
	foreach $unit (sort keys %message) {
		undef %said;
		$w = "\"$unit.U\"";
		foreach (split(' ', $message{$unit})) {
			if (s/^#//) {
				warn "$w: symbol '\$$_' has no default value.\n";
			} elsif (s/^\?\?//) {
				warn "$w: stale ?M: dependency '$_'.\n";
			} elsif (s/^\?//) {
				warn "$w: stale ?MAKE: dependency '$_'.\n";
			} elsif (s/^\$//) {
				if ($shmaster{"\$$_"} ne '') {
					warn "$w: symbol '\$$_' missing from ?MAKE.\n";
				} elsif (($uv = $shvisible{$_}) ne '') {
					warn "$w: missing $uv from ?MAKE for visible '\$$_'.\n";
				} elsif (($uv = $shvisible{"\$$_"}) ne '') {
					warn "$w: missing $uv from ?MAKE for visible '\$$_'.\n";
				} else {
					warn "\"$unit.U\": unknown symbol '\$$_'.\n";
				}
				++$said{$_};
			} elsif (s/^\&//) {
				warn "\"$unit.U\": read-only symbol '\$$_' is set.\n";
				++$said{$_};
			} elsif (s/^!//) {
				warn "\"$unit.U\": obsolete symbol '$_' is used.\n";
				++$said{$_};
			} elsif (s/^\@\@\@//) {
				$uv = '?';		# To spot format errors
				s/^(\w+):// && ($uv = $1);
				warn "$w: local file '$_' may override the one set by $uv.U.\n";
			} elsif (s/^\@\@//) {
				$uv = $filemaster{$_};
				warn "$w: you might not always get file '$_' from $uv.U.\n";
			} elsif (s/^\@//) {
				if ($uv = $filemaster{$_}) {
					warn "$w: missing $uv from ?MAKE for private file '$_'.\n";
				} else {
					warn "$w: unknown private file '$_'.\n";
				}
				++$said{"\@$_"};
			} else {
				warn "\"$unit.U\": undeclared symbol '\$$_' is set.\n"
					unless $said{$_};
			}
		}
	}

	# Memory cleanup
	undef %message;
	undef %said;
	undef %shused;
	undef %shset;
	undef %shspecial;
	undef %shvisible;
	undef %filemaster;

	# Spot multiply defined C symbols
	foreach $sym (keys %cmaster) {
		@sym = split(' ', $cmaster{$sym});
		if (@sym > 1) {
			warn "C symbol '$sym' is defined in the following units:\n";
			foreach (@sym) {
				print STDERR "\t$_.U\n";
			}
		}
	}
	undef %cmaster;		# Memory cleanup

	# Warn about multiply defined symbols. There are three kind of symbols:
	# target symbols, obsolete symbols and temporary symbols.
	# For each of these sets, we make sure the intersection with the other sets
	# is empty. Besides, we make sure target symbols are only defined once.

	local(@sym);
	foreach $sym (keys %shmaster) {
		@sym = split(' ', $shmaster{$sym});
		if (@sym > 1) {
			warn "Shell symbol '$sym' is defined in the following units:\n";
			foreach (@sym) {
				print STDERR "\t$_.U\n";
			}
		}
		$message{$sym} .= 'so ' if $Obsolete{$sym};
		$message{$sym} .= 'st ' if $tempmaster{$sym};
	}
	foreach $sym (keys %tempmaster) {
		$message{$sym} .= 'ot ' if $Obsolete{$sym};
	}
	local($_);
	while (($sym, $_) = each %message) {
		if (/so/) {
			if (/ot/) {
				warn "Shell symbol '$sym' is altogether:\n";
				@sym = split(' ', $shmaster{$sym});
				@sym = grep(s/$/.U/, @sym);
				print STDERR "...defined in: ", join(', ', @sym), "\n";
				print STDERR "...obsoleted by $Obsolete{$sym}.\n";
				@sym = split(' ', $tempmaster{$sym});
				@sym = grep(s/$/.U/, @sym);
				print STDERR "...used as temporary in:", join(', ', @sym), "\n";
			} else {
				warn "Shell symbol '$sym' is both defined and obsoleted:\n";
				@sym = split(' ', $shmaster{$sym});
				@sym = grep(s/$/.U/, @sym);
				print STDERR "...defined in: ", join(', ', @sym), "\n";
				print STDERR "...obsoleted by $Obsolete{$sym}.\n";
			}
		} elsif (/st/) {		# Cannot be ot as it would imply so
			warn "Shell symbol '$sym' is both defined and used as temporary:\n";
			@sym = split(' ', $shmaster{$sym});
			@sym = grep(s/$/.U/, @sym);
			print STDERR "...defined in: ", join(', ', @sym), "\n";
			@sym = split(' ', $tempmaster{$sym});
			@sym = grep(s/$/.U/, @sym);
			print STDERR "...used as temporary in:", join(', ', @sym), "\n";
		} elsif (/ot/) {
			warn "Shell symbol '$sym' obsoleted also used as temporary:\n";
			print STDERR "...obsoleted by $Obsolete{$sym}.\n";
			@sym = split(' ', $tempmaster{$sym});
			@sym = grep(s/$/.U/, @sym);
			print STDERR "...used as temporary in:", join(', ', @sym), "\n";
		}
	}

	# Spot multiply defined files, either private or public ones
	foreach $file (keys %prodfile) {
		@sym = split(' ', $prodfile{$file});
		if (@sym > 1) {
			warn "File '$file' is defined in the following units:\n";
			foreach (@sym) {
				print STDERR "\t$_\n";
			}
		}
	}
	undef %prodfile;


	# Memory cleanup (we still need %shmaster for tsort)
	undef %message;
	undef %tempmaster;
	undef %Obsolete;

	# Make sure there is no dependency cycle
	print "Looking for dependency cycles...\n";
	&tsort(*Succ, *Prec);	# Destroys info from %Prec
}

# Make sure last declaration ended correctly with a ?S:. or ?C:. line.
# The variable '$where' was correctly positionned by the calling routine.
sub check_last_declaration {
	warn "$where: definition of '\$$s_symbol' not closed by '?S:.'.\n"
		if $s_symbol ne '';
	warn "$where: definition of '$c_symbol' not closed by '?C:.'.\n"
		if $c_symbol ne '';
	warn "$where: magic definition of '$m_symbol' not closed by '?M:.'.\n"
		if $m_symbol ne '';
	$s_symbol = $c_symbol = $m_symbol = '';
}

# Make sure the variable is mentionned on the ?MAKE line, if possible in the
# definition section.
# The variable '$where' was correctly positionned by the calling routine.
sub check_definition {
	local($var) = @_;
	warn "$where: variable '\$$var' not even listed on ?MAKE: line.\n"
		unless $defseen{$var} || $condseen{$var} || $depseen{$var};
	warn "$where: variable '\$$var' is defined externally.\n"
		if !$lintextern{$var} && !$defseen{$var} && &wanted($var);
}

# Is symbol declared somewhere?
sub declared {
	&defined($_[0]) || &wanted($_[0]);
}

# Is symbol defined by unit?
sub defined {
	$tempseen{$_[0]} || $defseen{$_[0]} || $lintseen{$_[0]};
}

# Is symbol wanted by unit?
sub wanted {
	$depseen{$_[0]} || $condseen{$_[0]};
}

# Is symbol visible from the unit?
# Locate visible symbols throughout the special units. Each unit having
# some special dependencies (special units wanted) have an entry in the
# %shspecial array, listing all those special dependencies. And each
# symbol made visible by ONE special unit has an entry in the %shvisible
# array.
sub visible {
	local($symbol, $unit) = @_;
	local(%explored);				# Special units we've already explored
	&explore($symbol, $unit);		# Perform recursive search
}

# Recursively explore the dependencies to locate a visible symbol
sub explore {
	local($symbol, $unit) = @_;
	# If unit was already explored, we know it has not been found by following
	# that path.
	return 0 if defined $explored{$unit};
	$explored{$unit} = 0;			# Assume nothing found in this unit
	local($specials) = $shspecial{$unit};
	# Don't waste any time if unit does not have any special units listed
	# in its dependencies.
	return 0 unless $specials;
	foreach $special (split(' ', $specials)) {
		return 1 if (
			$shvisible{"\$$symbol"} eq $unit ||
			$shvisible{$symbol} eq $unit ||
			&explore($symbol, $special)
		);
	}
	0;
}

