Au cours de ce siècle, tout le monde a entendu parler des services cloud. De nombreuses entreprises ont maîtrisé ce segment de marché et créé leurs services cloud dans différentes directions. Notre équipe s'est également récemment intéressée à ces services en termes d'intégration de l'analyseur de code PVS-Studio avec eux. Nous pensons que nos lecteurs réguliers devinent déjà quel type de projet nous allons vérifier cette fois. Le choix s'est porté sur le code des services cloud de Huawei.
Présentation
Si vous suivez l'équipe PVS-Studio, vous avez probablement remarqué que récemment nous nous sommes beaucoup intéressés aux technologies cloud. Nous avons déjà publié plusieurs articles liés à ce sujet:
Et donc, quand je prenais un autre projet intéressant pour le prochain article, j'ai reçu une offre d'emploi de
Huawei par la poste. Lors de la collecte d'informations sur cette société, il s'est avéré qu'ils avaient leurs propres services cloud, mais l'essentiel est que le code source de ces services est disponible gratuitement sur GitHub. C'était la principale raison du choix de cette entreprise pour cet article. Comme l'a dit un sage chinois: "Les accidents ne sont pas accidentels."
Maintenant, parlons un peu de notre analyseur. PVS-Studio est un analyseur de code statique qui identifie les erreurs et les vulnérabilités dans le code source des programmes écrits en C, C ++, C # et Java. L'analyseur fonctionne sur les plateformes Windows, Linux et macOS. En plus des plugins pour des environnements de développement classiques tels que Visual Studio ou IntelliJ IDEA, l'analyseur a la capacité de s'intégrer avec SonarQube et Jenkins:
Analyse de projet
Au cours de la recherche d'informations pour l'article, j'ai découvert que Huawei dispose d'un
centre de développement où vous pouvez obtenir des informations, des guides et le code source de leurs services cloud. Divers langages de programmation ont été utilisés pour créer ces services, mais des langages tels que Go, Java et Python se sont avérés être les plus populaires.
Comme je suis spécialisé en Java, les projets ont été choisis en conséquence. Les sources des projets analysés dans l'article peuvent être obtenues dans le référentiel GitHub
huaweicloud .
Pour analyser les projets, je n'avais besoin d'effectuer que quelques actions:
- Obtenez des projets à partir du référentiel;
- Utilisez les instructions pour démarrer l'analyseur Java et démarrer l'analyse sur chacun des projets.
Après avoir analysé les projets, nous avons réussi à n'en distinguer que trois auxquels je voudrais prêter attention. Cela est dû au fait que la taille des projets Java restants était trop petite.
Résultats de l'analyse du projet (nombre d'avertissements et nombre de fichiers):
Il y a peu d'avertissements, donc en général on peut dire de la bonne qualité du code. De plus, toutes les opérations ne sont pas des erreurs valides. Cela est dû au fait que l'analyseur n'a parfois pas suffisamment d'informations pour distinguer le code correct du code incorrect. Par conséquent, les diagnostics de l'analyseur sont améliorés chaque jour à l'aide des informations reçues des utilisateurs. Voir également l'article "
Comment et pourquoi les analyseurs statiques combattent les faux positifs ".
Dans le processus d'analyse des projets, j'ai sélectionné les avertissements les plus intéressants, dont je parlerai dans cet article.
Ordre d'initialisation des champs
Le cycle d'initialisation de la classe
V6050 est présent. L'initialisation de 'INSTANCE' apparaît avant l'initialisation de 'LOG'. UntrustedSSL.java (32), UntrustedSSL.java (59), UntrustedSSL.java (33)
public class UntrustedSSL { private static final UntrustedSSL INSTANCE = new UntrustedSSL(); private static final Logger LOG = LoggerFactory.getLogger(UntrustedSSL.class); .... private UntrustedSSL() { try { .... } catch (Throwable t) { LOG.error(t.getMessage(), t);
Si une exception se produit dans le constructeur de la classe
UntrustedSSL , les informations sur cette exception sont enregistrées dans le
bloc catch à l'aide de l'enregistreur
LOG . Cependant, en raison de l'ordre d'initialisation des champs statiques, le
LOG au moment de l'initialisation du champ
INSTANCE n'est pas encore initialisé. Par conséquent, lors de la journalisation des informations d'exception dans le constructeur, une exception
NullPointerException sera levée. Cette exception est la cause d'une autre
exception ExceptionInInitializerError qui est levée si une exception s'est produite lors de l'initialisation du champ statique. Tout ce qui est nécessaire pour résoudre le problème décrit est de placer l'initialisation
LOG avant l'initialisation
INSTANCE .
Faute de frappe invisible
V6005 La variable 'this.metricSchema' est assignée à elle-même. OpenTSDBSchema.java (72)
public class OpenTSDBSchema { @JsonProperty("metric") private List<SchemaField> metricSchema; .... public void setMetricsSchema(List<SchemaField> metricsSchema) { this.metricSchema = metricSchema;
Les deux méthodes définissent le champ
metricSchema , mais les noms de méthode diffèrent par leur caractère. Le programmeur a nommé les arguments de ces méthodes en fonction du nom de la méthode. Par conséquent, dans la ligne pointée par l'analyseur, le champ
metricSchema est assigné à lui-même et l'argument de la méthode
metricsSchema n'est pas utilisé.
V6005 La variable 'suspend' est assignée à elle-même. SuspendTransferTaskRequest.java (77)
public class SuspendTransferTaskRequest { .... private boolean suspend; .... public void setSuspend(boolean suspend) { suspend = suspend; } .... }
Voici une erreur banale liée à la négligence, à cause de laquelle l'affectation de l'argument
suspendre à lui-même se produit. Par conséquent, la valeur de l'argument arrivé ne sera pas affectée au champ de
suspension , comme implicite. Correctement:
public void setSuspend(boolean suspend) { this.suspend = suspend; }
Conditions prédéterminées
Comme cela arrive souvent, la règle
V6007 prend de l' avance en termes de nombre d'avertissements.
V6007 L' expression «firewallPolicyId == null» est toujours fausse. FirewallPolicyServiceImpl.java (125)
public FirewallPolicy removeFirewallRuleFromPolicy(String firewallPolicyId, String firewallRuleId) { checkNotNull(firewallPolicyId); checkNotNull(firewallRuleId); checkState(!(firewallPolicyId == null && firewallRuleId == null), "Either a Firewall Policy or Firewall Rule identifier must be set"); .... }
Dans cette méthode, les arguments sont vérifiés pour
null par la méthode
checkNotNull :
@CanIgnoreReturnValue public static <T> T checkNotNull(T reference) { if (reference == null) { throw new NullPointerException(); } else { return reference; } }
Après avoir vérifié l'argument avec la méthode
checkNotNull , vous pouvez être sûr à 100% que l'argument passé à cette méthode n'est pas
nul . Étant donné que les deux arguments de la méthode
removeFirewallRuleFromPolicy sont vérifiés par la méthode
checkNotNull , il est inutile de vérifier à nouveau les arguments pour
les valeurs nulles . Cependant, une expression est passée à la méthode
checkState comme premier argument, où les arguments
firewallPolicyId et
firewallRuleId sont revérifiés pour
null .
Un avertissement similaire est émis pour
firewallRuleId :
- V6007 L'expression 'firewallRuleId == null' est toujours fausse. FirewallPolicyServiceImpl.java (125)
V6007 L' expression 'filteringParams! = Null' est toujours vraie. NetworkPolicyServiceImpl.java (60)
private Invocation<NetworkServicePolicies> buildInvocation(Map<String, String> filteringParams) { .... if (filteringParams == null) { return servicePoliciesInvocation; } if (filteringParams != null) {
Dans cette méthode, si l'argument
filteringParams est
nul , la méthode se ferme et renvoie une valeur. Par conséquent, le test pointé par l'analyseur sera toujours vrai, ce qui signifie que ce test n'a pas de sens.
La même chose se produit dans 13 classes:
- V6007 L'expression 'filteringParams! = Null' est toujours vraie. PolicyRuleServiceImpl.java (58)
- V6007 L'expression 'filteringParams! = Null' est toujours vraie. GroupServiceImpl.java (58)
- V6007 L'expression 'filteringParams! = Null' est toujours vraie. ExternalSegmentServiceImpl.java (57)
- V6007 L'expression 'filteringParams! = Null' est toujours vraie. L3policyServiceImpl.java (57)
- V6007 L'expression 'filteringParams! = Null' est toujours vraie. PolicyRuleSetServiceImpl.java (58)
- et ainsi de suite ...
Référence nulle
V6008 Déréférence nulle potentielle de 'm.blockDeviceMapping'. NovaServerCreate.java (390)
@Override public ServerCreateBuilder blockDevice(BlockDeviceMappingCreate blockDevice) { if (blockDevice != null && m.blockDeviceMapping == null) { m.blockDeviceMapping = Lists.newArrayList(); } m.blockDeviceMapping.add(blockDevice);
Dans cette méthode, l'initialisation du champ de référence
m.blockDeviceMapping ne se produira pas si l'argument
blockDevice est
nul . Ce champ est initialisé uniquement dans cette méthode, donc lorsque la méthode
add est appelée, le champ
m.blockDeviceMapping lèvera une
NullPointerException .
V6008 Déréférence nulle potentielle de 'FileId.get (chemin)' dans la fonction '<init>'. TrackedFile.java (140), TrackedFile.java (115)
public TrackedFile(FileFlow<?> flow, Path path) throws IOException { this(flow, path, FileId.get(path), ....); }
Le résultat de la méthode statique
FileId.get (chemin) est transmis au constructeur de la classe
TrackedFile comme troisième argument. Mais cette méthode peut retourner
null :
public static FileId get(Path file) throws IOException { if (!Files.exists(file)) { return null; } .... }
Dans le constructeur appelé par
ce biais, l'argument
id ne change pas tant qu'il n'est pas utilisé pour la première fois:
public TrackedFile(...., ...., FileId id, ....) throws IOException { .... FileId newId = FileId.get(path); if (!id.equals(newId)) { .... } }
Par conséquent, si
null est passé à la méthode comme troisième argument, une exception se produit.
Une situation similaire se produit dans un autre cas:
- V6008 Déréférence nulle potentielle de «tampon». PublishingQueue.java (518)
V6008 Déréférence nulle potentielle de 'dataTmpFile'. CacheManager.java (91)
@Override public void putToCache(PutRecordsRequest putRecordsRequest) { .... if (dataTmpFile == null || !dataTmpFile.exists()) { try { dataTmpFile.createNewFile();
Et encore NPE. Un certain nombre de vérifications dans l'instruction conditionnelle permettent à l'objet
dataTmpFile d'être null pour un déréférencement ultérieur. Je pense qu'il y a deux fautes de frappe ici, et le chèque devrait en fait être comme ceci:
if (dataTmpFile != null && !dataTmpFile.exists())
Sous-chaînes et nombres négatifs
V6009 La fonction 'substring' pourrait recevoir la valeur '-1' alors qu'une valeur non négative est attendue. Inspectez l'argument: 2. RemoveVersionProjectIdFromURL.java (37)
@Override public String apply(String url) { String urlRmovePojectId = url.substring(0, url.lastIndexOf("/")); return urlRmovePojectId.substring(0, urlRmovePojectId.lastIndexOf("/")); }
Il est entendu que l'URL est transmise à cette méthode sous forme de chaîne, qui n'est en aucun cas validée. Après cette chaîne est découpée plusieurs fois à l'aide de la méthode
lastIndexOf . Si
lastIndexOf ne trouve pas de correspondance dans la chaîne, il renverra -1. Cela
lèvera une
StringIndexOutOfBoundsException , car les arguments de la méthode de
sous -
chaîne doivent être des nombres non négatifs. Pour que la méthode fonctionne correctement, vous devez ajouter la validation de l'argument d'entrée ou vérifier que les résultats des méthodes
lastIndexOf ne sont pas des nombres négatifs.
Voici d'autres endroits où cela se produit:
- V6009 La fonction 'substring' pourrait recevoir la valeur '-1' alors qu'une valeur non négative est attendue. Inspectez l'argument: 2. RemoveProjectIdFromURL.java (37)
- V6009 La fonction 'substring' pourrait recevoir la valeur '-1' alors qu'une valeur non négative est attendue. Inspectez l'argument: 2. RemoveVersionProjectIdFromURL.java (38)
Résultat oublié
V6010 La valeur de retour de la fonction 'concat' doit être utilisée. AKSK.java (278)
public static String buildCanonicalHost(URL url) { String host = url.getHost(); int port = url.getPort(); if (port > -1) { host.concat(":" + Integer.toString(port)); } return host; }
Lors de l'écriture de ce code, il n'a pas été pris en compte que l'appel de la méthode
concat ne changera pas la chaîne
hôte en raison de l'immuabilité des objets de type
String . Pour que la méthode fonctionne correctement, vous devez affecter le résultat de la méthode
concat à la variable
hôte dans le bloc
if . Correctement:
if (port > -1) { host = host.concat(":" + Integer.toString(port)); }
Variables inutilisées
V6021 L'URL variable n'est pas utilisée. TriggerV2Service.java (95)
public ActionResponse deleteAllTriggersForFunction(String functionUrn) { checkArgument(!Strings.isNullOrEmpty(functionUrn), ....); String url = ClientConstants.FGS_TRIGGERS_V2 + ClientConstants.URI_SEP + functionUrn; return deleteWithResponse(uri(triggersUrlFmt, functionUrn)).execute(); }
Dans cette méthode, la variable
url n'est pas utilisée après l'initialisation. Très probablement, la variable
url doit être passée à la méthode
uri comme deuxième argument au lieu de
functionUrn , car la variable
functionUrn est impliquée dans l'initialisation de la variable
url .
Argument non utilisé dans le constructeur
V6022 Le paramètre 'returnType' n'est pas utilisé dans le corps du constructeur. HttpRequest.java (68)
public class HttpReQuest<R> { .... Class<R> returnType; .... public HttpRequest(...., Class<R> returnType)
Dans ce constructeur, ils ont oublié d'utiliser l'argument
returnType et d'assigner sa valeur au champ
returnType . Par conséquent, lorsque la méthode
getReturnType est
appelée , l'objet créé par ce constructeur renverra la valeur par défaut
null , bien qu'il était très probablement destiné à obtenir l'objet qui a été précédemment transmis au constructeur.
Même fonctionnalité
V6032 Il est étrange que le corps de la méthode 'enable' soit entièrement équivalent au corps d'une autre méthode 'disable'. ServiceAction.java (32), ServiceAction.java (36)
public class ServiceAction implements ModelEntity { private String binary; private String host; private ServiceAction(String binary, String host) { this.binary = binary; this.host = host; } public static ServiceAction enable(String binary, String host) {
La présence de deux méthodes identiques n'est pas une erreur, mais le fait que deux méthodes effectuent la même action est au moins étrange. En regardant les noms des méthodes ci-dessus, nous pouvons supposer qu'elles effectuent les actions opposées. En fait, les deux méthodes font la même chose: elles créent et renvoient un objet
ServiceAction . Très probablement, la méthode
disable a été créée en copiant le code de la méthode
enable , mais a oublié de changer le corps de la méthode.
J'ai oublié de vérifier la chose principale
V6060 La référence "params" a été utilisée avant d'être vérifiée par rapport à null. DomainService.java (49), DomainService.java (46)
public Domains list(Map<String, String> params) { Preconditions.checkNotNull(params.get("page_size"), ....); Preconditions.checkNotNull(params.get("page_number"), ....); Invocation<Domains> domainInvocation = get(Domains.class, uri("/domains")); if (params != null) {
Dans cette méthode, nous avons décidé de vérifier le contenu d'une structure de type
Map pour l'inégalité
nulle . Pour ce faire, l'argument
params appelle deux fois la méthode
get , dont le résultat est passé à la méthode
checkNotNull . Tout semble logique, mais peu importe comment! Dans
if , l'argument
params est vérifié pour
null . Après quoi, on peut supposer que l'argument d'entrée peut être
nul , mais avant cette vérification, la méthode
get était déjà appelée deux fois sur les
paramètres . Si
null est passé comme argument à cette méthode, la première fois que la méthode
get est appelée, une exception sera levée.
Une situation similaire se produit à trois autres endroits:
- V6060 La référence "params" a été utilisée avant d'être vérifiée par rapport à null. DomainService.java (389), DomainService.java (387)
- V6060 La référence "params" a été utilisée avant d'être vérifiée par rapport à null. DomainService.java (372), DomainService.java (369)
- V6060 La référence "params" a été utilisée avant d'être vérifiée par rapport à null. DomainService.java (353), DomainService.java (350)
Conclusion
De nos jours, les grandes entreprises modernes ne peuvent tout simplement pas se passer de l'utilisation des services cloud. Un grand nombre de personnes utilisent ces services, donc même la moindre erreur dans le service peut entraîner des problèmes pour de nombreuses personnes, ainsi que des coûts supplémentaires pour l'entreprise afin de corriger les conséquences de cette erreur. Lors du développement, il est toujours nécessaire de prendre en compte le facteur humain, car toute personne fait tôt ou tard des erreurs, et cet article en est un exemple. C'est pourquoi vous devez utiliser tous les outils possibles pour améliorer la qualité du code.
PVS-Studio informera certainement Huawei des résultats de la vérification de leurs services cloud afin que les développeurs de cette société puissent les étudier plus en détail.
L'utilisation ponctuelle de l'analyse de code statique démontrée dans l'article ne peut pas montrer tous ses avantages. Des informations plus détaillées sur la façon d'utiliser correctement l'analyse statique peuvent être trouvées
ici et
ici . Vous pouvez télécharger l'analyseur PVS-Studio
ici .

Si vous souhaitez partager cet article avec un public anglophone, veuillez utiliser le lien vers la traduction: Valery Komarov.
Huawei Cloud: c'est nuageux dans PVS-Studio aujourd'hui .