tracers-codegen 0.1.0

Contains the compile-time code generation logic which powers the `probe` and `tracers` macros. Do not use this crate directly; see "tracers" for more information.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
//! This module provides functionality to scan the AST of a Rust source file and identify
//! `tracers` provider traits therein, as well as analyze those traits and produce `ProbeSpec`s for
//! each of the probes they contain.  Once the provider traits have been discovered, other modules
//! in this crate can then process them in various ways
use crate::hashing::HashCode;
use crate::serde_helpers;
use crate::spec::ProbeSpecification;
use crate::{TracersError, TracersResult};
use darling::FromMeta;
use heck::SnakeCase;
use proc_macro2::TokenStream;
use quote::quote;
use serde::{Deserialize, Serialize};
use std::fmt;
use syn::parse::{Parse, ParseStream, Result as ParseResult};
use syn::visit::Visit;
use syn::Token;
use syn::{ItemTrait, TraitItem};

/// Struct which contains the parsed and processed contents of the `tracer` attribute.
#[derive(Debug, FromMeta, Clone, Serialize, Deserialize, Default)]
pub(crate) struct TracerAttributeArgs {
    #[darling(default)]
    provider_name: Option<String>,
}

/// Implement parsing the arguments portion of a `#[tracer]` attribute.  This does _not_ parse the
/// whole attribute.  It parses only the part after the `tracer` name, which contains optional
/// parameters that control the behavior of the generated provider.
impl Parse for TracerAttributeArgs {
    fn parse(input: ParseStream) -> ParseResult<Self> {
        //This implementation mostly copied from
        //https://github.com/dtolnay/syn/blob/master/src/parse_macro_input.rs which for some reason
        //is hidden and only works with `proc_macro` not `proc_macro2`
        let mut metas: Vec<syn::NestedMeta> = vec![];

        loop {
            if input.is_empty() {
                break;
            }
            let value = input.parse()?;
            metas.push(value);
            if input.is_empty() {
                break;
            }
            input.parse::<Token![,]>()?;
        }

        TracerAttributeArgs::from_list(&metas).map_err(|e| input.error(e))
    }
}

impl TracerAttributeArgs {
    /// Parse the attribute args from a token stream.
    ///
    /// NB: This only works with the token  stream provided to an attribute-like proc macro.   The
    /// assumption is that this token stream is the body of the attribute macro, without it's
    /// actual name.  In other words, if you pass a token stream like `#[tracer(foo="bar")]` this
    /// will fail.  To parse a full `tracer` attribute, use `syn::parse2` to parse a
    /// `TracerAttribute`, and it will internally pick out the arguments and parse them into an
    /// instance of this struct
    pub(crate) fn from_token_stream(attr: TokenStream) -> TracersResult<Self> {
        syn::parse2(attr).map_err(|e| TracersError::syn_error("Error parsing attribute args", e))
    }

    /// Parse the attribute args from an already-parsed `syn::Attribute`.
    fn from_attribute(attr: syn::Attribute) -> TracersResult<Self> {
        //The `syn::Attribute` struct by itself isn't usable to get TracerAttributeArgs,
        //because it contains the ident `tracer` and in `tts` it contains everything after the
        //ident.  So if the attribute is `#[tracer(foo = "bar")]`, then `tts` contains `(foo =
        //"bar")`.  That won't parse with the `FromMeta` implementation Darling generates.  So
        //we need to break upon those parens.
        //
        //We'll call `Attribute::parse_meta` to get the attribute as a `Meta`, and then if the
        //meta looks like a list of values, then we'll call the Darling-generated code to parse
        //those values
        let meta = attr
            .parse_meta()
            .map_err(|e| TracersError::syn_error("Error parsing attribute metadata", e))?;

        let args = match meta {
            syn::Meta::Path(_) =>
            //This attribute is just the ident, `#[tracer]`, with no additional attributes
            {
                Ok(TracerAttributeArgs::default())
            }
            syn::Meta::NameValue(_) => Err(TracersError::syn_like_error(
                "Expected name/value pairs in ()",
                attr,
            )),
            syn::Meta::List(list) => {
                //There's a parens after the ident and zero or more attributes, eg
                //`#[tracer(foo = "bar")`
                TracerAttributeArgs::from_list(
                    &list
                        .nested
                        .into_pairs()
                        .map(syn::punctuated::Pair::into_value)
                        .collect::<Vec<_>>(),
                )
                .map_err(TracersError::darling_error)
            }
        }?;

        Ok(args)
    }
}

/// A bit of a hack to work around a limitation in the `syn` crate.  It doesn't expose an API to
/// parse an attribute directly; it expects you to implement the `Parse` trait to do it yourself.
/// So this does that, though it assumes it's only ever used on a `#[tracer]` attribute because it
/// attempts to parse possible `tracer` parameters in the attribute`
pub(crate) struct TracerAttribute {
    args: TracerAttributeArgs,
}

impl TracerAttribute {
    fn from_attribute(attr: syn::Attribute) -> TracersResult<Self> {
        Ok(TracerAttribute {
            args: TracerAttributeArgs::from_attribute(attr)?,
        })
    }
}

impl Parse for TracerAttribute {
    fn parse(input: ParseStream) -> ParseResult<Self> {
        // We're expecting an attribute like `#[tracer]` or `#[tracer(foo = "bar")`
        let mut attrs: Vec<syn::Attribute> = input.call(syn::Attribute::parse_outer)?;

        //We're expecting exactly one such attribute
        if let Some(tracer_attr) = attrs.pop() {
            Ok(TracerAttribute {
                args: TracerAttributeArgs::from_attribute(tracer_attr)
                    .map_err(TracersError::into_syn_error)?,
            })
        } else {
            Err(input.error("Expected exactly one attribute, `#[tracer]`"))
        }
    }
}

#[derive(Serialize, Deserialize, Clone)]
pub struct ProviderSpecification {
    name: String,
    hash: HashCode,
    #[serde(with = "serde_helpers::syn")]
    item_trait: ItemTrait,
    #[serde(with = "serde_helpers::token_stream")]
    token_stream: TokenStream,
    args: TracerAttributeArgs,
    probes: Vec<ProbeSpecification>,
}

impl fmt::Debug for ProviderSpecification {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        writeln!(
            f,
            "ProviderSpecification(
    name='{}',
    probes:",
            self.name
        )?;

        for probe in self.probes.iter() {
            writeln!(f, "        {:?},", probe)?;
        }

        write!(f, ")")
    }
}

impl ProviderSpecification {
    fn new(
        crate_name: &str,
        args: TracerAttributeArgs,
        item_trait: ItemTrait,
    ) -> TracersResult<ProviderSpecification> {
        let probes = find_probes(&item_trait)?;
        let token_stream = quote! { #item_trait };
        let hash = crate::hashing::hash(&item_trait);

        //If the name was overridden by the attribute, use that override, otherwise generate a name
        let name: String = match args.provider_name {
            Some(ref name) => name.clone(),
            None => Self::provider_name_from_trait(crate_name, &item_trait.ident),
        };
        Ok(ProviderSpecification {
            name,
            hash,
            item_trait,
            token_stream,
            args,
            probes,
        })
    }

    pub(crate) fn from_token_stream(
        crate_name: &str,
        args: TracerAttributeArgs,
        tokens: TokenStream,
    ) -> TracersResult<ProviderSpecification> {
        match syn::parse2::<syn::ItemTrait>(tokens) {
            Ok(item_trait) => Self::new(crate_name, args, item_trait),
            Err(e) => Err(TracersError::syn_error("Expected a trait", e)),
        }
    }

    pub(crate) fn from_trait(
        crate_name: &str,
        attr: TracerAttribute,
        item_trait: ItemTrait,
    ) -> TracersResult<ProviderSpecification> {
        Self::new(crate_name, attr.args, item_trait)
    }

    /// Computes the name of a provider given the name of the provider's trait.
    pub(crate) fn provider_name_from_trait(crate_name: &str, ident: &syn::Ident) -> String {
        // The provider name must be chosen carefully.  As of this writing (2019-04) the `bpftrace`
        // and `bcc` tools have, shall we say, "evolving" support for USDT.  As of now, with the
        // latest git version of `bpftrace`, the provider name can't have dots or colons.  For now,
        // then, the provider name is just the name of the provider trait, converted into
        // snake_case for consistency with USDT naming conventions.  If two modules in the same
        // process have the same provider name, they will conflict and some unspecified `bad
        // things` will happen.
        format!("{}_{}", crate_name, ident.to_string().to_snake_case())
    }

    pub(crate) fn name(&self) -> &str {
        &self.name
    }

    /// The name of this provider (in snake_case) combined with the hash of the provider's
    /// contents.  Eg: `my_provider_deadc0de1918df`
    pub(crate) fn name_with_hash(&self) -> String {
        format!("{}_{:x}", self.name, self.hash)
    }

    pub(crate) fn hash(&self) -> HashCode {
        self.hash
    }

    pub(crate) fn ident(&self) -> &syn::Ident {
        &self.item_trait.ident
    }

    pub(crate) fn item_trait(&self) -> &syn::ItemTrait {
        &self.item_trait
    }

    pub(crate) fn token_stream(&self) -> &TokenStream {
        &self.token_stream
    }

    pub(crate) fn probes(&self) -> &Vec<ProbeSpecification> {
        &self.probes
    }

    /// Consumes this spec and returns the same spec with all probes removed, and instead the
    /// probes vector is returned separately.  This is a convenient way to wrap
    /// ProviderSpecification in something else (in truth its designed for the
    /// `ProviderTraitGenerator` implementation classes)
    pub(crate) fn separate_probes(self) -> (ProviderSpecification, Vec<ProbeSpecification>) {
        let probes = self.probes;
        (
            ProviderSpecification {
                name: self.name,
                hash: self.hash,
                item_trait: self.item_trait,
                token_stream: self.token_stream,
                args: self.args,
                probes: Vec::new(),
            },
            probes,
        )
    }
}

/// Scans the AST of a Rust source file, finding all traits marked with the `tracer` attribute,
/// parses the contents of the trait, and deduces the provider spec from that.
///
/// Note that if any traits are encountered with the `tracer` attribute but which are in some way
/// invalid as providers, those traits will be silently ignored.  At compile time the `tracer`
/// attribute will cause a very detailed compile error so there's no chance the user will miss this
/// mistake.
pub(crate) fn find_providers(crate_name: &str, ast: &syn::File) -> Vec<ProviderSpecification> {
    //Construct an implementation of the `syn` crate's `Visit` trait which will examine all trait
    //declarations in the file looking for possible providers
    struct Visitor<'a> {
        crate_name: &'a str,
        providers: Vec<ProviderSpecification>,
    }

    impl<'ast> Visit<'ast> for Visitor<'ast> {
        fn visit_item_trait(&mut self, i: &'ast ItemTrait) {
            //First pass through to the default impl
            syn::visit::visit_item_trait(self, i);

            fn is_tracer_attribute(attr: &syn::Attribute) -> bool {
                match attr.path.segments.iter().last() {
                    Some(syn::PathSegment { ident, .. }) if *ident == "tracer" => true,
                    _ => false,
                }
            }

            //Check for the `tracer` or `tracers::tracer` attribute, splitting it out from the rest
            //of the attributes if present to ensure the
            //hash matches the same hash computed by the proc macro when it's invoked on this
            //trait during the compile stage
            let mut i = i.clone();
            let (mut tracer_attrs, other_attrs) = i
                .attrs
                .into_iter()
                .partition::<Vec<syn::Attribute>, _>(is_tracer_attribute);
            if let Some(tracer_attr) = tracer_attrs.pop() {
                //This looks like a provider trait.
                //Normally there should be only one `#[tracer]` but that's for the full compiler to
                //sort out.  We'll just assume the first one is the one we want
                i.attrs = other_attrs;
                if let Ok(provider) = ProviderSpecification::from_trait(
                    self.crate_name,
                    TracerAttribute::from_attribute(tracer_attr)
                        .expect("Failed parsing attribute metadata"),
                    i,
                ) {
                    self.providers.push(provider)
                }
            }
        }
    }

    let mut visitor = Visitor {
        crate_name,
        providers: Vec::new(),
    };
    visitor.visit_file(ast);

    visitor.providers
}

/// Looking at the methods defined on the trait, deduce from those methods the probes that we will
/// need to define, including their arg counts and arg types.
///
/// If the trait contains anything other than method declarations, or any of the declarations are
/// not suitable as probes, an error is returned
fn find_probes(item: &ItemTrait) -> TracersResult<Vec<ProbeSpecification>> {
    if item.generics.type_params().next() != None || item.generics.lifetimes().next() != None {
        return Err(TracersError::invalid_provider(
            "Probe traits must not take any lifetime or type parameters",
            item,
        ));
    }

    // Look at the methods on the trait and translate each one into a probe specification
    let mut specs: Vec<ProbeSpecification> = Vec::new();
    for f in item.items.iter() {
        match f {
            TraitItem::Method(ref m) => {
                specs.push(ProbeSpecification::from_method(item, m)?);
            }
            _ => {
                return Err(TracersError::invalid_provider(
                    "Probe traits must consist entirely of methods, no other contents",
                    f,
                ));
            }
        }
    }

    Ok(specs)
}

#[cfg(test)]
mod test {
    use super::*;
    use crate::testdata::*;
    use std::io::{BufReader, BufWriter};
    use syn::parse_quote;

    impl PartialEq<ProviderSpecification> for ProviderSpecification {
        fn eq(&self, other: &ProviderSpecification) -> bool {
            self.name == other.name && self.probes == other.probes
        }
    }

    /// Allows tests to compare a test case directly to a ProviderSpecification to ensure they match
    impl PartialEq<TestProviderTrait> for ProviderSpecification {
        fn eq(&self, other: &TestProviderTrait) -> bool {
            self.name == other.provider_name
                && other
                    .probes
                    .as_ref()
                    .map(|probes| &self.probes == probes)
                    .unwrap_or(false)
        }
    }

    fn get_filtered_test_traits(with_errors: bool) -> Vec<TestProviderTrait> {
        get_test_provider_traits(|t: &TestProviderTrait| t.expected_error.is_some() == with_errors)
    }

    #[test]
    fn find_providers_ignores_invalid_traits() {
        for test_trait in get_filtered_test_traits(true) {
            let trait_decl = test_trait.tokenstream;
            let test_file: syn::File = parse_quote! {
                #[tracer]
                #trait_decl
            };

            assert_eq!(
                None,
                find_providers(TEST_CRATE_NAME, &test_file).first(),
                "The invalid trait '{}' was returned by find_providers as valid",
                test_trait.description
            );
        }
    }

    #[test]
    fn find_providers_finds_valid_traits() {
        for test_trait in get_filtered_test_traits(false) {
            let trait_attr = test_trait.attr_tokenstream.clone();
            let trait_decl = test_trait.tokenstream.clone();
            let test_file: syn::File = parse_quote! {
                #trait_attr
                #trait_decl
            };

            let mut providers = find_providers(TEST_CRATE_NAME, &test_file);
            assert_ne!(
                0,
                providers.len(),
                "the test trait '{}' was not properly detected by find_provider",
                test_trait.description
            );

            assert_eq!(providers.pop().unwrap(), test_trait);
        }
    }

    #[test]
    fn find_probes_fails_with_invalid_traits() {
        for test_trait in get_filtered_test_traits(true) {
            let trait_decl = test_trait.tokenstream;
            let item_trait: syn::ItemTrait = parse_quote! {
                #[tracer]
                #trait_decl
            };

            let error = find_probes(&item_trait).err();
            assert_ne!(
                None, error,
                "The invalid trait '{}' was returned by find_probes as valid",
                test_trait.description
            );

            let expected_error_substring = test_trait.expected_error.unwrap();
            let message = error.unwrap().to_string();
            assert!(message.contains(expected_error_substring),
                "The invalid trait '{}' should produce an error containing '{}' but instead it produced '{}'",
                test_trait.description,
                expected_error_substring,
                message
            );
        }
    }

    #[test]
    fn find_probes_succeeds_with_valid_traits() {
        for test_trait in get_filtered_test_traits(false) {
            let trait_decl = test_trait.tokenstream;
            let item_trait: syn::ItemTrait = parse_quote! {
                #[tracer]
                #trait_decl
            };

            let probes = find_probes(&item_trait).unwrap();
            assert_eq!(probes, test_trait.probes.unwrap_or(Vec::new()));
        }
    }

    #[test]
    fn found_providers_have_same_hash() {
        //There are two ways for us to get a ProviderSpecification:
        // The first is from a TokenStream in an attribute proc macro.  That's how the `#[tracer]`
        // macro works.
        // The second is in calling `find_providers` to discover providers in a source file
        //
        // A fundamental assumption made in this project is that the hashes of a given provider
        // will be identical regardless of which of the two ways it was obtained.
        //
        // However, the attribute proc macro gets a token stream which does not include the
        // `#[tracer]` attribute itself.  The `find_providers` code must see this attribute because
        // that's how it identifies a provider trait.  This test ensures the hashes are always
        // corrected to match
        for test_trait in get_filtered_test_traits(false) {
            //Note: we remove the `#[tracer]` attribute from the token stream because that's what
            //the proc macro infrastructure does.  All other attributes are preserved, but the
            //attribute that triggers the macro is provided in a separate token stream which we
            //ignore
            let provider_from_ts = ProviderSpecification::from_trait(
                TEST_CRATE_NAME,
                syn::parse2(test_trait.attr_tokenstream.clone()).unwrap(),
                syn::parse2(test_trait.tokenstream.clone()).unwrap(),
            )
            .unwrap();

            let tracer_attr = test_trait.attr_tokenstream;
            let trait_decl = test_trait.tokenstream;
            let file: syn::File = parse_quote! {
                mod foo {
                    fn useless_func() -> bool { false }
                }

                trait NotAProvider {
                    fn probe0(not_a_probe_arg: usize);
                }

                #tracer_attr
                #trait_decl
            };

            let providers = find_providers(TEST_CRATE_NAME, &file);

            assert_eq!(1, providers.len());
            let provider_from_file = providers.get(0).unwrap();

            assert_eq!(provider_from_ts.name(), provider_from_file.name());
            assert_eq!(provider_from_ts.hash(), provider_from_file.hash());
        }
    }

    #[test]
    fn provider_serde_test() {
        //Go through all of the valid test traits, parse them in to a provider, then serialize and
        //deserialize to json to make sure the round trip serialization works
        for test_trait in get_filtered_test_traits(false) {
            println!("Parsing attribute: {}", test_trait.attr_tokenstream);
            let (attr, item_trait) = test_trait.get_attr_and_item_trait();
            let provider =
                ProviderSpecification::from_trait(TEST_CRATE_NAME, attr, item_trait).unwrap();
            let mut buffer = Vec::new();
            let writer = BufWriter::new(&mut buffer);
            serde_json::to_writer(writer, &provider).unwrap();

            let reader = BufReader::new(buffer.as_slice());

            let rt_provider: ProviderSpecification = match serde_json::from_reader(reader) {
                Ok(p) => p,
                Err(e) => {
                    panic!(
                        r###"Error deserializing provider:
                            Test case: {}
                            JSON: {}
                            Error: {}"###,
                        test_trait.description,
                        String::from_utf8(buffer).unwrap(),
                        e
                    );
                }
            };

            assert_eq!(
                provider, rt_provider,
                "test case: {}",
                test_trait.description
            );
        }
    }

    #[test]
    fn parses_tracer_attributes_test() {
        //Make sure the `#[tracer]` attribute on all valid test cases can parse correctly
        for test_trait in get_filtered_test_traits(false) {
            println!("Parsing attribute: {}", test_trait.attr_tokenstream);
            let _attribute: TracerAttribute =
                syn::parse2(test_trait.attr_tokenstream).expect("Expected valid tracer attribute");
        }
    }
}