diff --git a/Makefile.am b/Makefile.am index 5b52d1a3..d0b66882 100644 --- a/Makefile.am +++ b/Makefile.am @@ -40,6 +40,7 @@ EXTRA_DIST = TODO BUGS BUGFORM FAQ INSTALL \ email-clients.txt checkpatch.pl update-changelog.pl \ script/check-cmdline-options-doc.sh \ script/check-translation-sources.sh \ + script/check-wmaker-loaddef-callbacks.sh \ script/generate-mapfile-from-header.sh \ script/generate-po-from-template.sh \ script/generate-txt-from-texi.sh \ diff --git a/script/check-wmaker-loaddef-callbacks.sh b/script/check-wmaker-loaddef-callbacks.sh new file mode 100755 index 00000000..1cc95410 --- /dev/null +++ b/script/check-wmaker-loaddef-callbacks.sh @@ -0,0 +1,470 @@ +#!/bin/sh +########################################################################### +# +# Window Maker window manager +# +# Copyright (c) 2015 Christophe CURIS +# Copyright (c) 2015 Window Maker Team +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program. If not, see . +# +########################################################################### +# +# check-wmaker-loaddef-callbacks.sh: +# Compare the type defined in a variable against the type use for the +# associated call-back function. +# +# To load the configuration file, Window Maker is using a list of the known +# keywords in a structure with the name of the keyword, the call-back +# function that converts the string (from file) into the appropriate type, +# and a pointer to the variable where the result is saved. +# +# Because the structure requires a little bit of genericity to be usefull, +# it is not possible to provide the C compiler with the information to +# check that the variable from the pointer has the same type as what the +# conversion call-back assumes, so this script does that check for us. +# +# Unfortunately, the script cannot be completely generic and smart, but +# it still tries to be, despite being made explicitely for the case of the +# structures "staticOptionList" and "optionList" from Window Maker's source +# file "src/defaults.c" +# +########################################################################### +# +# For portability, we stick to the same sh+awk constraint as Autotools to +# limit problems, see for example: +# http://www.gnu.org/software/autoconf/manual/autoconf-2.69/html_node/Portable-Shell.html +# +########################################################################### + +# Report an error on stderr and exit with status 2 to tell make that we could +# not do what we were asked +arg_error() { + echo "`basename $0`: $@" >&2 + exit 2 +} + +# print help and exit with success status +print_help() { + echo "$0: check variable type against call-back expectation for WMaker's defaults.c" + echo "Usage: $0 options..." + echo "valid options are:" + echo " --callback \"name=type\" : specify that function 'name' expects a variable of 'type'" + echo " --field-callback idx : index (from 1) of the callback function in the struct" + echo " --field-value-ptr idx : index (from 1) of the pointer-to-value in the struct" + echo " --source file : C source file with the array to check" + echo " --structure name : name of the variable with the array of struct to check" + echo " --struct-def name=file : specify to get definition of struct 'name' from 'file'" + exit 0 +} + +# Extract command line arguments +while [ $# -gt 0 ]; do + case $1 in + --callback) + shift + deflist="$1," + while [ -n "$deflist" ]; do + name_and_type=`echo "$deflist" | cut -d, -f1 | sed -e 's/^[ \t]*//;s/[ \t]*$//' ` + deflist=`echo "$deflist" | cut -d, -f2-` + echo "$name_and_type" | grep '^[A-Za-z][A-Za-z_0-9]*[ \t]*=[^=][^=]*$' > /dev/null || \ + arg_error "invalid callback function type specification '$name_and_type' (options: --callback)" + name=`echo "$name_and_type" | sed -e 's/=.*$// ; s/[ \t]//g' ` + type=`echo "$name_and_type" | sed -e 's/^[^=]*=// ; s/^[ \t]*// ; s/[ \t][ \t]*/ /g' ` + awk_callback_types="$awk_callback_types + callback[\"$name\"] = \"$type\";" + done + ;; + + --field-callback) + shift + [ -z "$field_callback" ] || arg_error "field number specified more than once (option: --field-callback)" + field_callback="$1" + ;; + + --field-value-ptr) + shift + [ -z "$field_value" ] || arg_error "field number specified more than once (option: --field-value-ptr)" + field_value="$1" + ;; + + --source) + shift + [ -z "$source_file" ] || arg_error "only 1 source file can be used (option: --source)" + source_file="$1" + ;; + + --structure) + shift + [ -z "$struct_name" ] || arg_error "only 1 structure can be checked (option: --structure)" + struct_name="$1" + ;; + + --struct-def) + shift + echo "$1" | grep '^[A-Za-z][A-Z_a-z0-9]*=' > /dev/null || arg_error "invalid syntax in \"$1\" for --struct-def" + [ -r "`echo $1 | sed -e 's/^[^=]*=//' `" ] || arg_error "file not readable in struct-def \"$1\"" + list_struct_def="$list_struct_def +$1" + ;; + + -h|-help|--help) print_help ;; + -*) arg_error "unknow option '$1'" ;; + + *) + arg_error "argument '$1' is not understood" + ;; + esac + shift +done + +# Check consistency of command-line +[ -z "$source_file" ] && arg_error "no source file given (option: --source)" +[ -z "$struct_name" ] && arg_error "no variable name given for the array to check (option: --structure)" +[ -z "$field_value" ] && arg_error "index of the value pointer in the struct no specified (option: --field-value-ptr)" +[ -z "$field_callback" ] && arg_error "index of the call-back in the struct no specified (option: --field-callback)" + +echo "$field_value" | grep '^[1-9][0-9]*$' > /dev/null || arg_error "invalid index for the value pointer, expecting a number (option: --field-value-ptr)" +echo "$field_callback" | grep '^[1-9][0-9]*$' > /dev/null || arg_error "invalid index for the call-back function, expecting a number (option: --field-callback)" + +########################################################################### + +# This AWK script is extracting the types associated with the field members +# from a specific structure defined in the parsed C source file +awk_extract_struct=' +# Parse all the lines until the end of current structure is found +function parse_structure(prefix) { + while (getline) { + + # Discard C comments, with support for multi-line comments + while (1) { + idx = index($0, "/*"); + if (idx == 0) { break; } + + comment = substr($0, idx+2); + $0 = substr($0, 1, idx-1); + while (1) { + idx = index(comment, "*/"); + if (idx > 0) { break; } + getline comment; + } + $0 = $0 substr(comment, idx+2); + } + + # skip line that define nothing interresting + if (/^[ \t]*$/) { continue; } + gsub(/^[ \t]+/, ""); + if (/^#/) { continue; } + gsub(/[ \t]+$/, ""); + + # end of current structure: extract the name and return it + if (/^[ \t]*\}/) { + gsub(/^\}[ \t]*/, ""); + name = $0; + gsub(/[ \t]*;.*$/, "", name); + gsub(/^[^;]*;/, ""); + return name; + } + + # Handle structure inside structure + if (/^(struct|union)[ \t]+\{/) { + name = parse_structure(prefix ",!"); + + # Update the name tracking the content of this struct to contain the name + # of the struct itself + match_prefix = "^" prefix ",!:"; + for (var_id in member) { + if (var_id !~ match_prefix) { continue; } + new_id = var_id; + gsub(/,!:/, ":" name ".", new_id); + member[new_id] = member[var_id]; + delete member[var_id]; + } + continue; + } + + if (!/;$/) { + print "Warning: line " FILENAME ":" NR " not understood inside struct" > "/dev/stderr"; + continue; + } + + # Skip the lines that define a bit-field because they cannot be safely + # pointed to anyway + if (/:/) { continue; } + + # It looks like a valid line, separate the name from the type + gsub(/;$/, ""); + name = $0; + gsub(/([A-Z_a-z][A-Z_a-z0-9]*([ \t]*,[ \t]*)*)*$/, ""); + name = substr(name, length($0) + 1); + + # In some rare case we cannot extract the name, that is likely a function pointer type + if (name == "") { continue; } + + # Remove the sign specification because it is not a real problem + gsub(/\<(un)?signed\>/, " "); + if (/^[^A-Za-z]*$/) { + # If there is no more character, that means that the sign declaration was the only type specified, so + # we use the "int" type which is what C will use + $0 = "int " $0; + } + + # Pack the type to have something consistent + gsub(/^[ \t]+/, ""); + gsub(/[ \t]+$/, ""); + gsub(/[ \t]*\*[ \t]*/, "*"); + gsub(/[ \t]+/, " "); + + # Save this information in an array + nb_vars = split(name, var_list, /[ \t]*,[ \t]*/); + for (i = 1; i <= nb_vars; i++) { + member[prefix ":" var_list[i] ] = $0; + } + } +} + +# The name of the variable is at the end, so find all structure definition +/^([a-z]*[ \t][ \t]*)*struct[ \t]/ { + + # Discard all words to find the first ; or { + gsub(/^([A-Za-z_0-9]*[ \t][ \t]*)+/, ""); + + # If not an { it is probably not what we are looking for + if (/^[^\{]/) { next; } + + # Read everything until we find the end of the structure; we assume a + # definition is limited to one line + name = parse_structure("@"); + + # If the name is what we expect, generate the appropriate stuff + if (name == expected_name) { + struct_found++; + for (i in member) { + $0 = i; + gsub(/^@:/, expected_name "."); + print " variable[\"" $0 "\"] = \"" member[i] "\";"; + } + } + + # Purge the array to not mix fields between the different structures + for (i in member) { delete member[i]; } +} + +# Check that everything was ok +END { + if (struct_found == 0) { + print "Error: structure \"" expected_name "\" was not found in " FILENAME > "/dev/stderr"; + exit 1; + } else if (struct_found > 1) { + print "Error: structure \"" expected_name "\" was defined more than once in " FILENAME > "/dev/stderr"; + exit 1; + } +} + +# Do not print anything else than what is generated while parsing structures +{ } +' + +# Extract the information for all the structures specified on the command line +awk_array_types=`echo "$list_struct_def" | while + IFS="=" read name file +do + [ -z "$name" ] && continue + + awk_script=" +BEGIN { + struct_found = 0; + expected_name = \"$name\"; +} +$awk_extract_struct" + + echo " # $file" + + awk "$awk_script" "$file" + [ $? -ne 0 ] && exit $? + +done` + +########################################################################### + +# Parse the source file to extract the list of call-back functions that are +# used; take the opportunity to extract information about the variable +# being pointed to now to avoid re-parsing too many times the file +awk_check_callbacks=' +# Search the final } for the current element in the array, then split the +# elements into the array "entry_elements" and remove that content from $0 +function get_array_element_and_split() { + nb_elements = 1; + entry_elements[nb_elements] = ""; + + $0 = substr($0, 2); + count_braces = 1; + while (count_braces > 0) { + char = substr($0, 1, 1); + $0 = substr($0, 2); + if (char == "{") { + count_braces++; + } else if (char == "}") { + count_braces--; + } else if (char ~ /[ \t]/) { + # Just discard + } else if (char == ",") { + if (count_braces == 1) { nb_elements++; entry_elements[nb_elements] = ""; } + } else if (char == "\"") { + entry_elements[nb_elements] = entry_elements[nb_elements] extract_string_to_element(); + } else if (char == "/") { + if (substr($0, 1, 1) == "/") { + getline; + while (/^#/) { getline; } + } else if (substr($0, 1, 1) == "*") { + $0 = substr($0, 2); + skip_long_comment(); + } else { + entry_elements[nb_elements] = entry_elements[nb_elements] char; + } + } else if (char == "") { + getline; + while (/^#/) { print "skip: " $0; getline; } + } else { + entry_elements[nb_elements] = entry_elements[nb_elements] char; + } + } +} + +# When a string enclosed in "" is encountered as part of the elements of the +# array, it requires special treatment, not to extract the information (we are +# unlikely to care about these fields) but to avoid mis-parsing the fields +function extract_string_to_element() { + content = "\""; + while (1) { + char = substr($0, 1, 1); + $0 = substr($0, 2); + if (char == "\\") { + content = content char substr($0, 1, 1); + $0 = substr($0, 2); + } else if (char == "\"") { + break; + } else if (char == "") { + getline; + } else { + content = content char; + } + } + return content "\""; +} + +# Wherever a long C comment (/* comment */) is encounter, it is discarded +function skip_long_comment() { + while (1) { + idx = index($0, "*/"); + if (idx > 0) { + $0 = substr($0, idx + 2); + break; + } + getline; + } +} + +# Search for the definition of an array with the good name +/^[ \t]*([A-Z_a-z][A-Z_a-z0-9*]*[ \t]+)+'$struct_name'[ \t]*\[\][ \t]*=[ \t]*\{/ { + struct_found++; + + $0 = substr($0, index($0, "{") + 1); + + # Parse all the elements of the array + while (1) { + + # Search for start of an element + while (1) { + gsub(/^[ \t]+/, ""); + if (substr($0, 1, 1) == "{") { break; } + if (substr($0, 1, 1) == "}") { break; } + if (substr($0, 1, 1) == "#") { getline; continue; } + if ($0 == "") { getline; continue; } + + # Remove comments + if (substr($0, 1, 2) == "//") { getline; continue; } + if (substr($0, 1, 2) == "/*") { + $0 = substr($0, 3); + skip_long_comment(); + } else { + print "Warning: line " NR " not understood in " FILENAME ", skipped" > "/dev/stderr"; + getline; + } + } + + # Did we find the end of the array? + if (substr($0, 1, 1) == "}") { break; } + + # Grab the whole content of the entry + entry_start_line = NR; + get_array_element_and_split(); + gsub(/^[ \t]*,/, ""); + + # Extract the 2 fields we are interrested in + if ((entry_elements[src_fvalue] != "NULL") && (entry_elements[src_ffunct] != "NULL")) { + + if (substr(entry_elements[src_fvalue], 1, 1) == "&") { + entry_elements[src_fvalue] = substr(entry_elements[src_fvalue], 2); + } else { + print "Warning: value field used in entry at line " entry_start_line " does not looke like a pointer" > "/dev/stderr"; + } + + if (variable[entry_elements[src_fvalue]] == "") { + print "Warning: type is not known for \"" entry_elements[src_fvalue] "\" at line " entry_start_line ", cannot check" > "/dev/stderr"; + } else if (callback[entry_elements[src_ffunct]] == "") { + print "Error: expected type for callback function \"" entry_elements[src_ffunct] "\" is not known, from " FILENAME ":" entry_start_line > "/dev/stderr"; + error_count++; + } else if (callback[entry_elements[src_ffunct]] != variable[entry_elements[src_fvalue]]) { + print "Error: type mismatch between function and variable in " FILENAME ":" entry_start_line > "/dev/stderr"; + print " Function: " entry_elements[src_ffunct] " expects \"" callback[entry_elements[src_ffunct]] "\"" > "/dev/stderr"; + print " Variable: " entry_elements[src_fvalue] " has type \"" variable[entry_elements[src_fvalue]] "\"" > "/dev/stderr"; + error_count++; + } + + } + } +} + +# Final checks +END { + if (error_count > 0) { exit 1; } + if (struct_found == 0) { + print "Error: structure \"'$struct_name'\" was not found in " FILENAME > "/dev/stderr"; + exit 1; + } else if (struct_found > 1) { + print "Error: structure \"'$struct_name'\" was defined more than once in " FILENAME > "/dev/stderr"; + exit 1; + } +} + +# Do not print anything else than what is generated while parsing the structure +{ } +' + +awk_script="BEGIN { +$awk_array_types +$awk_callback_types + + # Info on structure to be checked + src_fvalue = $field_value; + src_ffunct = $field_callback; + + # For checks + struct_found = 0; + error_count = 0; +} +$awk_check_callbacks" + +awk "$awk_script" "$source_file" || exit $? diff --git a/src/Makefile.am b/src/Makefile.am index 7fe9db72..771282e8 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -165,3 +165,32 @@ wmaker_LDADD = \ @XLIBS@ \ @LIBM@ \ @INTLIBS@ + +###################################################################### + +# Create a 'silent rule' for our make check the same way automake does +AM_V_CHKOPTS = $(am__v_CHKOPTS_$(V)) +am__v_CHKOPTS_ = $(am__v_CHKOPTS_$(AM_DEFAULT_VERBOSITY)) +am__v_CHKOPTS_0 = @echo " CHK $@" ; +am__v_CHKOPTS_1 = + +check-local: defaults-callbacks-static defaults-callbacks-dynamic + +# Check that the callback functions used to load the configuration match +# the type of the variable where the value will be stored +defaults-callbacks-static: + $(AM_V_CHKOPTS)$(top_srcdir)/script/check-wmaker-loaddef-callbacks.sh \ + --source "$(srcdir)/defaults.c" --structure "staticOptionList" \ + --field-value-ptr 4 --field-callback 5 \ + --struct-def "wPreferences=$(srcdir)/WindowMaker.h" \ + --callback "getBool=char, getEnum=char, getInt=int" \ + --callback "getModMask=int" + +defaults-callbacks-dynamic: + $(AM_V_CHKOPTS)$(top_srcdir)/script/check-wmaker-loaddef-callbacks.sh \ + --source "$(srcdir)/defaults.c" --structure "optionList" \ + --field-value-ptr 4 --field-callback 5 \ + --struct-def "wPreferences=$(srcdir)/WindowMaker.h" \ + --struct-def "legacy_minipreview_config=$(srcdir)/defaults.c" \ + --callback "getBool=char, getEnum=char, getInt=int" \ + --callback "getPathList=char*, getCoord=WCoord"