Macros for SAS Application Developers
https://github.com/sasjs/core
Loading...
Searching...
No Matches
mp_derivedvarcheck.sas
Go to the documentation of this file.
1/**
2 @file
3 @brief Validates derived variable definitions, one row per rule
4 @details Reads a table of derived variable definitions and writes one row per
5 definition that breaks a rule, with the reason in REASON. Nothing is
6 executed, no view is built and no target table is needed: the macro is a
7 scan of definitions, so it can run at submit time - or against any table of
8 expressions in a code scanning tool - long before the definitions are used.
9
10 The input is the shape a control table carries, which is the shape
11 mddl_derivedvars.sas creates:
12
13 @li VAR_NM - the name of the variable to create
14 @li VAR_TYPE - C or N
15 @li VAR_LEN - a positive integer, at most 32767 for character and 8 for
16 numeric
17 @li VAR_FMT - an optional format name
18 @li VAR_LOGIC - the expression, which must be a single expression with no
19 macro triggers and no comments, at most 4000 characters - the limit of the
20 statement buffer and of the control table, so a longer value would be cut
21 down silently
22 @li VAR_LABEL - optional, plain text of at most 256 characters. A line
23 break is allowed and becomes a space when the label is used
24
25 Extra columns are ignored, so a caller can scan the control table itself, a
26 staged submission, or a table of expressions assembled from a program.
27
28 <h4>Why a function name is refused and a character is not</h4>
29
30 VAR_LOGIC is concatenated into a generated assignment, so an expression that
31 can submit a program of its own (DOSUBL) or reach the file system, the
32 environment or the macro symbol table is a way into the session. Blocked
33 characters cannot cover that: BYTE(59) builds the semicolon at run time, so
34 a payload carries no blocked character in its source text - while the name of
35 the function it calls is part of the expression this scan reads, and cannot
36 be constructed. Quoted literals are removed before the check, so a refused
37 word that is only data is not read as a call.
38
39 The refused names are DOSUBL, JAVAOBJ, JAVAMETA, SYMGET, SYMPUT, SYMPUTX,
40 SYSGET, GETOPTION, PATHNAME, FILEEXIST, FOPEN, FREAD, FGET, FCLOSE, DOPEN,
41 DREAD, DNUM, MOPEN, SLEEP, FILENAME, FDELETE, FCOPY, DCREATE and DLCREATE.
42 Add your own with denyfuncs=.
43
44 <h4> SAS Macros </h4>
45 @li mf_existds.sas
46 @li mf_existvar.sas
47 @li mp_abort.sas
48
49 <h4> Related Macros </h4>
50 @li mp_derivedvars.sas
51 @li mddl_derivedvars.sas
52
53 @param [in] inds= The definitions to check
54 @param [out] outds= (work.dv_check) One row per definition that breaks a
55 rule: NAME (the upcased VAR_NM) and REASON. Empty when every row is valid
56 @param [in] physlist= (0) Space separated list of the columns of the target
57 table, unquoted and case insensitive - what %mf_getvarlist(&libds) returns.
58 Supplied, a VAR_NM that collides with one of them is refused; when the
59 target is not at hand, leave it at 0 and that rule is not applied
60 @param [in] reserved= (0) Space separated list of names that may not be used
61 @param [in] denyfuncs= (0) Space separated list of further function names
62 to refuse, for a site that allows more of the SAS vocabulary than the
63 default. Each entry must be a function name - letters, digits and
64 underscores - so the pattern the check builds stays a pattern of names
65
66 @version 1.0
67 @author 4GL Apps Ltd
68
69**/
70
71%macro mp_derivedvarcheck(inds=,
72 outds=work.dv_check,
73 physlist=0,
74 reserved=0,
75 denyfuncs=0
76);
77
78%local deny denycnt physcnt resv_list phys_list i w bad_deny denyrx w_ok;
79
80%mp_abort(iftrue= (%length(&inds)=0)
81 ,mac=&sysmacroname
82 ,msg=%str(Please provide the definitions to check in inds)
83)
84%mp_abort(iftrue= (%mf_existds(libds=&inds) ne 1)
85 ,mac=&sysmacroname
86 ,msg=%str(Definitions not found: &inds)
87)
88
89/* the capabilities that are refused - a function name cannot be assembled at
90 run time, so naming them is a control the expression cannot evade. The
91 file system is covered in both directions: the read vocabulary (FOPEN,
92 FREAD, FGET, FCLOSE) and the write, copy and delete vocabulary
93 (FILENAME, FDELETE, FCOPY, DCREATE, DLCREATE) */
94%let deny=DOSUBL|JAVAOBJ|JAVAMETA|SYMGET|SYMPUTX?|SYSGET|GETOPTION;
95%let deny=&deny.|PATHNAME|FILEEXIST|FOPEN|FREAD|FGET|FCLOSE;
96%let deny=&deny.|DOPEN|DREAD|DNUM|MOPEN|SLEEP;
97%let deny=&deny.|FILENAME|FDELETE|FCOPY|DCREATE|DLCREATE;
98%let bad_deny=0;
99/* a token has to be a function name: a pattern metacharacter here would make
100 the PRXMATCH below fail, and a failed match passes every row */
101%let denyrx=%str(/^[A-Z][A-Z0-9_]*$/);
102%let denycnt=%sysfunc(countw(&denyfuncs,%str( )));
103%if "%upcase(&denyfuncs)" ne "0" %then %do i=1 %to &denycnt;
104 %let w=%upcase(%scan(&denyfuncs,&i,%str( )));
105 %let w_ok=%sysfunc(prxmatch(&denyrx,&w));
106 %if &w_ok=0 %then %let bad_deny=1;
107 %else %let deny=&deny.|&w;
108%end;
109%mp_abort(iftrue= (&bad_deny=1)
110 ,mac=&sysmacroname
111 ,msg=%str(denyfuncs must be a space separated list of function names)
112)
113
114/* the target columns, as a quoted list - supplied only when the caller has the
115 table to hand */
116%let phys_list=;
117%let physcnt=%sysfunc(countw(&physlist,%str( )));
118%if %length(&physlist)>0 and "%upcase(&physlist)" ne "0"
119 %then %do i=1 %to &physcnt;
120 %let w=%scan(&physlist,&i,%str( ));
121 %let phys_list=&phys_list.%str(,)%str(%")%upcase(&w)%str(%");
122%end;
123%if %length(&phys_list)>0 %then %let phys_list=%substr(&phys_list,2);
124
125/* build a quoted list of reserved names, or an empty list */
126%let resv_list=;
127%if "&reserved" ne "0" %then %do i=1 %to %sysfunc(countw(&reserved,%str( )));
128 %let w=%scan(&reserved,&i,%str( ));
129 %let resv_list=&resv_list.%str(,)%str(%")%upcase(&w)%str(%");
130%end;
131%if %length(&resv_list)>0 %then %let resv_list=%substr(&resv_list,2);
132
133data &outds(keep=name reason);
134 set &inds;
135 length name $32 reason $200 l $4000;
136 call missing(reason);
137 name=upcase(strip(var_nm));
138
139 /* quoted literals are data, not calls - strip them before the vocabulary
140 check so a label or a value that names a refused function is allowed */
141 l=prxchange("s/'([^']|'')*'//",-1,var_logic);
142 l=prxchange('s/\"([^\"]|\"\")*\"//',-1,l);
143
144 if name='' then reason='VAR_NM is required';
145 else if verify(strip(name),'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_')>0
146 then reason='VAR_NM must be a valid SAS name';
147 else if verify(substr(name,1,1),'0123456789')=0
148 then reason='VAR_NM must not start with a digit';
149 else if name in ('_N_','_ERROR_','_IORC_')
150 then reason='VAR_NM is an automatic variable of the data step';
151 %if %length(&phys_list)>0 %then %do;
152 else if name in (%unquote(&phys_list))
153 then reason='VAR_NM collides with a column in the target table';
154 %end;
155 %if %length(&resv_list)>0 %then %do;
156 else if name in (%unquote(&resv_list))
157 then reason='VAR_NM is reserved';
158 %end;
159 else if upcase(strip(var_type)) not in ('C','N')
160 then reason='VAR_TYPE must be C or N';
161 else if missing(var_len) or var_len ne int(var_len) or var_len<1
162 then reason='VAR_LEN must be a positive integer';
163 else if upcase(strip(var_type))='C' and var_len>32767
164 then reason='VAR_LEN exceeds 32767 for a character variable';
165 else if upcase(strip(var_type))='N' and var_len>8
166 then reason='VAR_LEN exceeds 8 for a numeric variable';
167 else if index(var_fmt,';')>0 or indexc(var_fmt,'&%')>0
168 or index(var_fmt,'/*')>0 or index(var_fmt,'*/')>0
169 then reason='VAR_FMT must be a format name, without ; & % or /* */';
170 else if verify(strip(upcase(var_fmt))
171 ,'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_$.')>0
172 then reason='VAR_FMT must be a format name';
173 %if %mf_existvar(&inds,VAR_LABEL) gt 0 %then %do;
174 /* VAR_LABEL is free text - any script, any character - because the quotes
175 in it are escaped when the attrib statement is built. A line break is
176 flattened to a space, which is what a label has always become, so only
177 the remaining control characters are refused */
178 else if anycntrl(translate(var_label,' ','0D0A'x))>0
179 then reason='VAR_LABEL must be plain text - no control characters';
180 else if klength(var_label)>256
181 then reason='VAR_LABEL exceeds 256 characters';
182 %end;
183 else if missing(var_logic)
184 then reason='VAR_LOGIC is required';
185 else if klength(var_logic)>4000
186 then reason='VAR_LOGIC exceeds 4000 characters';
187 else if index(var_logic,';')>0
188 then reason='VAR_LOGIC must be a single expression - remove the ;';
189 else if indexc(var_logic,'&%')>0
190 then reason='VAR_LOGIC must not contain macro triggers';
191 else if index(var_logic,'/*')>0 or index(var_logic,'*/')>0
192 then reason='VAR_LOGIC must not contain comments';
193 else if prxmatch("/\b(&deny)\s*\(/",upcase(l))>0
194 then reason='VAR_LOGIC may not call a function that submits code or'
195 ||' reaches the file system, the environment or the macro symbol table';
196
197 if missing(reason)=0 then output;
198run;
199
200%mend mp_derivedvarcheck;