From 11f4d62a062a5c80fad414c457e2a7dbc9a1d6cc Mon Sep 17 00:00:00 2001 From: Brian Anderson Date: Wed, 14 Jan 2015 19:27:45 -0800 Subject: [PATCH] Add a lint for library features Does a sanity check of the version numbers. --- mk/tests.mk | 1 + src/etc/featureck.py | 254 +++++++++++++++++++++++++++++++ src/librustc/middle/stability.rs | 6 +- src/libsyntax/feature_gate.rs | 131 +++++++++------- 4 files changed, 330 insertions(+), 62 deletions(-) create mode 100644 src/etc/featureck.py diff --git a/mk/tests.mk b/mk/tests.mk index 02cc745803f..33890652124 100644 --- a/mk/tests.mk +++ b/mk/tests.mk @@ -300,6 +300,7 @@ tidy: | grep '^$(S)src/libbacktrace' -v \ | grep '^$(S)src/rust-installer' -v \ | xargs $(CFG_PYTHON) $(S)src/etc/check-binaries.py + $(CFG_PYTHON) $(S)src/etc/featureck.py $(S)src/ endif diff --git a/src/etc/featureck.py b/src/etc/featureck.py new file mode 100644 index 00000000000..06ef2d7c605 --- /dev/null +++ b/src/etc/featureck.py @@ -0,0 +1,254 @@ +# Copyright 2015 The Rust Project Developers. See the COPYRIGHT +# file at the top-level directory of this distribution and at +# http://rust-lang.org/COPYRIGHT. +# +# Licensed under the Apache License, Version 2.0 or the MIT license +# , at your +# option. This file may not be copied, modified, or distributed +# except according to those terms. + +# This script does a tree-wide sanity checks against stability +# attributes, currently: +# * For all feature_name/level pairs the 'since' field is the same +# * That no features are both stable and unstable. +# * That lib features don't have the same name as lang features +# unless they are on the 'joint_features' whitelist +# * That features that exist in both lang and lib and are stable +# since the same version +# * Prints information about features + +import sys, os, re + +src_dir = sys.argv[1] + +# Features that are allowed to exist in both the language and the library +joint_features = [ "on_unimpleented" ] + +# Grab the list of language features from the compiler +language_gate_statuses = [ "Active", "Deprecated", "Removed", "Accepted" ] +feature_gate_source = os.path.join(src_dir, "libsyntax", "feature_gate.rs") +language_features = [] +language_feature_names = [] +with open(feature_gate_source, 'r') as f: + for line in f: + original_line = line + line = line.strip() + is_feature_line = False + for status in language_gate_statuses: + if status in line and line.startswith("("): + is_feature_line = True + + if is_feature_line: + line = line.replace("(", "").replace("),", "").replace(")", "") + parts = line.split(",") + if len(parts) != 3: + print "unexpected number of components in line: " + original_line + sys.exit(1) + feature_name = parts[0].strip().replace('"', "") + since = parts[1].strip().replace('"', "") + status = parts[2].strip() + assert len(feature_name) > 0 + assert len(since) > 0 + assert len(status) > 0 + + language_feature_names += [feature_name] + language_features += [(feature_name, since, status)] + +assert len(language_features) > 0 + +errors = False + +lib_features = { } +lib_features_and_level = { } +for (dirpath, dirnames, filenames) in os.walk(src_dir): + # Don't look for feature names in tests + if "src/test" in dirpath: + continue + + # Takes a long time to traverse LLVM + if "src/llvm" in dirpath: + continue + + for filename in filenames: + if not filename.endswith(".rs"): + continue + + path = os.path.join(dirpath, filename) + with open(path, 'r') as f: + line_num = 0 + for line in f: + line_num += 1 + level = None + if "[unstable(" in line: + level = "unstable" + elif "[stable(" in line: + level = "stable" + elif "[deprecated(" in line: + level = "deprecated" + else: + continue + + # This is a stability attribute. For the purposes of this + # script we expect both the 'feature' and 'since' attributes on + # the same line, e.g. + # `#[unstable(feature = "foo", since = "1.0.0")]` + + p = re.compile('feature *= *"(\w*)".*since *= *"([\w\.]*)"') + m = p.search(line) + if not m is None: + feature_name = m.group(1) + since = m.group(2) + lib_features[feature_name] = feature_name + if lib_features_and_level.get((feature_name, level)) is None: + # Add it to the observed features + lib_features_and_level[(feature_name, level)] = (since, path, line_num, line) + else: + # Verify that for this combination of feature_name and level the 'since' + # attribute matches. + (expected_since, source_path, source_line_num, source_line) = \ + lib_features_and_level.get((feature_name, level)) + if since != expected_since: + print "mismatch in " + level + " feature '" + feature_name + "'" + print "line " + str(source_line_num) + " of " + source_path + ":" + print source_line + print "line " + str(line_num) + " of " + path + ":" + print line + errors = True + + # Verify that this lib feature doesn't duplicate a lang feature + if feature_name in language_feature_names: + print "lib feature '" + feature_name + "' duplicates a lang feature" + print "line " + str(line_num) + " of " + path + ":" + print line + errors = True + + else: + print "misformed stability attribute" + print "line " + str(line_num) + " of " + path + ":" + print line + errors = True + +# Merge data about both lists +# name, lang, lib, status, stable since, partially deprecated + +language_feature_stats = {} + +for f in language_features: + name = f[0] + lang = True + lib = False + status = "unstable" + stable_since = None + partially_deprecated = False + + if f[2] == "Accepted": + status = "stable" + if status == "stable": + stable_since = f[1] + + language_feature_stats[name] = (name, lang, lib, status, stable_since, \ + partially_deprecated) + +lib_feature_stats = {} + +for f in lib_features: + name = f + lang = False + lib = True + status = "unstable" + stable_since = None + partially_deprecated = False + + is_stable = lib_features_and_level.get((name, "stable")) is not None + is_unstable = lib_features_and_level.get((name, "unstable")) is not None + is_deprecated = lib_features_and_level.get((name, "deprecated")) is not None + + if is_stable and is_unstable: + print "feature '" + name + "' is both stable and unstable" + errors = True + + if is_stable: + status = "stable" + stable_since = lib_features_and_level[(name, "stable")][0] + elif is_unstable: + status = "unstable" + stable_since = lib_features_and_level[(name, "unstable")][0] + elif is_deprecated: + status = "deprecated" + + if (is_stable or is_unstable) and is_deprecated: + partially_deprecated = True + + lib_feature_stats[name] = (name, lang, lib, status, stable_since, \ + partially_deprecated) + +# Check for overlap in two sets +merged_stats = { } + +for name in lib_feature_stats: + if language_feature_stats.get(name) is not None: + if not name in joint_features: + print "feature '" + name + "' is both a lang and lib feature but not whitelisted" + errors = True + lang_status = lang_feature_stats[name][3] + lib_status = lib_feature_stats[name][3] + lang_stable_since = lang_feature_stats[name][4] + lib_stable_since = lib_feature_stats[name][4] + lang_partially_deprecated = lang_feature_stats[name][5] + lib_partially_deprecated = lib_feature_stats[name][5] + + if lang_status != lib_status and lib_status != "deprecated": + print "feature '" + name + "' has lang status " + lang_status + \ + " but lib status " + lib_status + errors = True + + partially_deprecated = lang_partially_deprecated or lib_partially_deprecated + if lib_status == "deprecated" and lang_status != "deprecated": + partially_deprecated = True + + if lang_stable_since != lib_stable_since: + print "feature '" + name + "' has lang stable since " + lang_stable_since + \ + " but lib stable since " + lib_stable_since + errors = True + + merged_stats[name] = (name, True, True, lang_status, lang_stable_since, \ + partially_deprecated) + + del language_feature_stats[name] + del lib_feature_stats[name] + +if errors: + sys.exit(1) + +# Finally, display the stats +stats = {} +stats.update(language_feature_stats) +stats.update(lib_feature_stats) +stats.update(merged_stats) +lines = [] +for s in stats: + s = stats[s] + type_ = "lang" + if s[1] and s[2]: + type_ = "lang/lib" + elif s[2]: + type_ = "lib" + line = s[0] + ",\t\t\t" + type_ + ",\t" + s[3] + ",\t" + str(s[4]) + line = "{: <32}".format(s[0]) + \ + "{: <8}".format(type_) + \ + "{: <12}".format(s[3]) + \ + "{: <8}".format(str(s[4])) + if s[5]: + line += "(partially deprecated)" + lines += [line] + +lines.sort() + +print +print "Rust feature summary:" +print +for line in lines: + print line +print + diff --git a/src/librustc/middle/stability.rs b/src/librustc/middle/stability.rs index 4d69be7cbc2..0554533a4aa 100644 --- a/src/librustc/middle/stability.rs +++ b/src/librustc/middle/stability.rs @@ -24,7 +24,7 @@ use syntax::ast::{TypeMethod, Method, Generics, StructField, TypeTraitItem}; use syntax::ast_util::is_local; use syntax::attr::{Stability, AttrMetaMethods}; use syntax::visit::{FnKind, FkMethod, Visitor}; -use syntax::feature_gate::emit_feature_err; +use syntax::feature_gate::emit_feature_warn; use util::nodemap::{NodeMap, DefIdMap, FnvHashSet}; use util::ppaux::Repr; @@ -221,8 +221,8 @@ impl<'a, 'tcx> Checker<'a, 'tcx> { None => format!("use of unstable library feature '{}'", feature.get()) }; - emit_feature_err(&self.tcx.sess.parse_sess.span_diagnostic, - feature.get(), span, &msg[]); + emit_feature_warn(&self.tcx.sess.parse_sess.span_diagnostic, + feature.get(), span, &msg[]); } } Some(..) => { diff --git a/src/libsyntax/feature_gate.rs b/src/libsyntax/feature_gate.rs index a5a2935d808..43af53aa2d2 100644 --- a/src/libsyntax/feature_gate.rs +++ b/src/libsyntax/feature_gate.rs @@ -17,6 +17,10 @@ //! //! Features are enabled in programs via the crate-level attributes of //! `#![feature(...)]` with a comma-separated list of features. +//! +//! For the purpose of future feature-tracking, once code for detection of feature +//! gate usage is added, *do not remove it again* even once the feature +//! becomes stable. use self::Status::*; use abi::RustIntrinsic; @@ -33,77 +37,82 @@ use parse::token::{self, InternedString}; use std::slice; use std::ascii::AsciiExt; +// If you change this list without updating src/doc/reference.md, @cmr will be sad +// Don't ever remove anything from this list; set them to 'Removed'. +// The version numbers here correspond to the version in which the current status +// was set. This is most important for knowing when a particular feature became +// stable (active). +// NB: The featureck.py script parses this information directly out of the source +// so take care when modifying it. +static KNOWN_FEATURES: &'static [(&'static str, &'static str, Status)] = &[ + ("globs", "1.0.0", Accepted), + ("macro_rules", "1.0.0", Accepted), + ("struct_variant", "1.0.0", Accepted), + ("asm", "1.0.0", Active), + ("managed_boxes", "1.0.0", Removed), + ("non_ascii_idents", "1.0.0", Active), + ("thread_local", "1.0.0", Active), + ("link_args", "1.0.0", Active), + ("phase", "1.0.0", Removed), + ("plugin_registrar", "1.0.0", Active), + ("log_syntax", "1.0.0", Active), + ("trace_macros", "1.0.0", Active), + ("concat_idents", "1.0.0", Active), + ("unsafe_destructor", "1.0.0", Active), + ("intrinsics", "1.0.0", Active), + ("lang_items", "1.0.0", Active), -// if you change this list without updating src/doc/reference.md, @cmr will be sad -static KNOWN_FEATURES: &'static [(&'static str, Status)] = &[ - ("globs", Accepted), - ("macro_rules", Accepted), - ("struct_variant", Accepted), - ("asm", Active), - ("managed_boxes", Removed), - ("non_ascii_idents", Active), - ("thread_local", Active), - ("link_args", Active), - ("phase", Removed), - ("plugin_registrar", Active), - ("log_syntax", Active), - ("trace_macros", Active), - ("concat_idents", Active), - ("unsafe_destructor", Active), - ("intrinsics", Active), - ("lang_items", Active), + ("simd", "1.0.0", Active), + ("default_type_params", "1.0.0", Accepted), + ("quote", "1.0.0", Active), + ("link_llvm_intrinsics", "1.0.0", Active), + ("linkage", "1.0.0", Active), + ("struct_inherit", "1.0.0", Removed), - ("simd", Active), - ("default_type_params", Accepted), - ("quote", Active), - ("link_llvm_intrinsics", Active), - ("linkage", Active), - ("struct_inherit", Removed), + ("quad_precision_float", "1.0.0", Removed), - ("quad_precision_float", Removed), + ("rustc_diagnostic_macros", "1.0.0", Active), + ("unboxed_closures", "1.0.0", Active), + ("import_shadowing", "1.0.0", Active), + ("advanced_slice_patterns", "1.0.0", Active), + ("tuple_indexing", "1.0.0", Accepted), + ("associated_types", "1.0.0", Accepted), + ("visible_private_types", "1.0.0", Active), + ("slicing_syntax", "1.0.0", Active), + ("box_syntax", "1.0.0", Active), + ("on_unimplemented", "1.0.0", Active), + ("simd_ffi", "1.0.0", Active), - ("rustc_diagnostic_macros", Active), - ("unboxed_closures", Active), - ("import_shadowing", Active), - ("advanced_slice_patterns", Active), - ("tuple_indexing", Accepted), - ("associated_types", Accepted), - ("visible_private_types", Active), - ("slicing_syntax", Active), - ("box_syntax", Active), - ("on_unimplemented", Active), - ("simd_ffi", Active), + ("if_let", "1.0.0", Accepted), + ("while_let", "1.0.0", Accepted), - ("if_let", Accepted), - ("while_let", Accepted), - - ("plugin", Active), - ("start", Active), - ("main", Active), + ("plugin", "1.0.0", Active), + ("start", "1.0.0", Active), + ("main", "1.0.0", Active), // A temporary feature gate used to enable parser extensions needed // to bootstrap fix for #5723. - ("issue_5723_bootstrap", Accepted), + ("issue_5723_bootstrap", "1.0.0", Accepted), // A way to temporarily opt out of opt in copy. This will *never* be accepted. - ("opt_out_copy", Removed), + ("opt_out_copy", "1.0.0", Removed), // A way to temporarily opt out of the new orphan rules. This will *never* be accepted. - ("old_orphan_check", Deprecated), + ("old_orphan_check", "1.0.0", Deprecated), // A way to temporarily opt out of the new impl rules. This will *never* be accepted. - ("old_impl_check", Deprecated), + ("old_impl_check", "1.0.0", Deprecated), // OIBIT specific features - ("optin_builtin_traits", Active), + ("optin_builtin_traits", "1.0.0", Active), // int and uint are now deprecated - ("int_uint", Active), + ("int_uint", "1.0.0", Active), // These are used to test this portion of the compiler, they don't actually // mean anything - ("test_accepted_feature", Accepted), - ("test_removed_feature", Removed), + ("test_accepted_feature", "1.0.0", Accepted), + ("test_removed_feature", "1.0.0", Removed), ]; enum Status { @@ -164,10 +173,7 @@ impl<'a> Context<'a> { fn warn_feature(&self, feature: &str, span: Span, explain: &str) { if !self.has_feature(feature) { - self.span_handler.span_warn(span, explain); - self.span_handler.span_help(span, &format!("add #![feature({})] to the \ - crate attributes to silence this warning", - feature)[]); + emit_feature_warn(self.span_handler, feature, span, explain); } } fn has_feature(&self, feature: &str) -> bool { @@ -182,6 +188,13 @@ pub fn emit_feature_err(diag: &SpanHandler, feature: &str, span: Span, explain: feature)[]); } +pub fn emit_feature_warn(diag: &SpanHandler, feature: &str, span: Span, explain: &str) { + diag.span_warn(span, explain); + diag.span_help(span, &format!("add #![feature({})] to the \ + crate attributes to silence this warning", + feature)[]); +} + struct MacroVisitor<'a> { context: &'a Context<'a> } @@ -510,21 +523,21 @@ fn check_crate_inner(cm: &CodeMap, span_handler: &SpanHandler, krate: &ast::C } }; match KNOWN_FEATURES.iter() - .find(|& &(n, _)| name == n) { - Some(&(name, Active)) => { + .find(|& &(n, _, _)| name == n) { + Some(&(name, _, Active)) => { cx.features.push(name); } - Some(&(name, Deprecated)) => { + Some(&(name, _, Deprecated)) => { cx.features.push(name); span_handler.span_warn( mi.span, "feature is deprecated and will only be available \ for a limited time, please rewrite code that relies on it"); } - Some(&(_, Removed)) => { + Some(&(_, _, Removed)) => { span_handler.span_err(mi.span, "feature has been removed"); } - Some(&(_, Accepted)) => { + Some(&(_, _, Accepted)) => { span_handler.span_warn(mi.span, "feature has been added to Rust, \ directive not necessary"); }